Merge branch 'main' into specialize-non-generic

This commit is contained in:
Shunsuke Shibayama 2025-12-12 10:34:14 +09:00
commit ea59e420aa
306 changed files with 11421 additions and 6042 deletions

View File

@ -24,6 +24,8 @@ env:
PACKAGE_NAME: ruff PACKAGE_NAME: ruff
PYTHON_VERSION: "3.14" PYTHON_VERSION: "3.14"
NEXTEST_PROFILE: ci NEXTEST_PROFILE: ci
# Enable mdtests that require external dependencies
MDTEST_EXTERNAL: "1"
jobs: jobs:
determine_changes: determine_changes:
@ -296,7 +298,7 @@ jobs:
# sync, not just public items. Eventually we should do this for all # sync, not just public items. Eventually we should do this for all
# crates; for now add crates here as they are warning-clean to prevent # crates; for now add crates here as they are warning-clean to prevent
# regression. # regression.
- run: cargo doc --no-deps -p ty_python_semantic -p ty -p ty_test -p ruff_db --document-private-items - run: cargo doc --no-deps -p ty_python_semantic -p ty -p ty_test -p ruff_db -p ruff_python_formatter --document-private-items
env: env:
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025). # Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
RUSTDOCFLAGS: "-D warnings" RUSTDOCFLAGS: "-D warnings"

View File

@ -47,6 +47,7 @@ jobs:
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with: with:
shared-key: "mypy-primer"
workspaces: "ruff" workspaces: "ruff"
- name: Install Rust toolchain - name: Install Rust toolchain
@ -86,6 +87,7 @@ jobs:
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with: with:
workspaces: "ruff" workspaces: "ruff"
shared-key: "mypy-primer"
- name: Install Rust toolchain - name: Install Rust toolchain
run: rustup show run: rustup show
@ -105,3 +107,54 @@ jobs:
with: with:
name: mypy_primer_memory_diff name: mypy_primer_memory_diff
path: mypy_primer_memory.diff path: mypy_primer_memory.diff
# Runs mypy twice against the same ty version to catch any non-deterministic behavior (ideally).
# The job is disabled for now because there are some non-deterministic diagnostics.
mypy_primer_same_revision:
name: Run mypy_primer on same revision
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
timeout-minutes: 20
# TODO: Enable once we fixed the non-deterministic diagnostics
if: false
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "ruff"
shared-key: "mypy-primer"
- name: Install Rust toolchain
run: rustup show
- name: Run determinism check
env:
BASE_REVISION: ${{ github.event.pull_request.head.sha }}
PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/good.txt
CLICOLOR_FORCE: "1"
DIFF_FILE: mypy_primer_determinism.diff
run: |
cd ruff
scripts/mypy_primer.sh
- name: Check for non-determinism
run: |
# Remove ANSI color codes for checking
sed -e 's/\x1b\[[0-9;]*m//g' mypy_primer_determinism.diff > mypy_primer_determinism_clean.diff
# Check if there are any differences (non-determinism)
if [ -s mypy_primer_determinism_clean.diff ]; then
echo "ERROR: Non-deterministic output detected!"
echo "The following differences were found when running ty twice on the same commit:"
cat mypy_primer_determinism_clean.diff
exit 1
else
echo "✓ Output is deterministic"
fi

View File

@ -1,5 +1,47 @@
# Changelog # Changelog
## 0.14.9
Released on 2025-12-11.
### Preview features
- \[`ruff`\] New `RUF100` diagnostics for unused range suppressions ([#21783](https://github.com/astral-sh/ruff/pull/21783))
- \[`pylint`\] Detect subclasses of builtin exceptions (`PLW0133`) ([#21382](https://github.com/astral-sh/ruff/pull/21382))
### Bug fixes
- Fix comment placement in lambda parameters ([#21868](https://github.com/astral-sh/ruff/pull/21868))
- Skip over trivia tokens after re-lexing ([#21895](https://github.com/astral-sh/ruff/pull/21895))
- \[`flake8-bandit`\] Fix false positive when using non-standard `CSafeLoader` path (S506). ([#21830](https://github.com/astral-sh/ruff/pull/21830))
- \[`flake8-bugbear`\] Accept immutable slice default arguments (`B008`) ([#21823](https://github.com/astral-sh/ruff/pull/21823))
### Rule changes
- \[`pydocstyle`\] Suppress `D417` for parameters with `Unpack` annotations ([#21816](https://github.com/astral-sh/ruff/pull/21816))
### Performance
- Use `memchr` for computing line indexes ([#21838](https://github.com/astral-sh/ruff/pull/21838))
### Documentation
- Document `*.pyw` is included by default in preview ([#21885](https://github.com/astral-sh/ruff/pull/21885))
- Document range suppressions, reorganize suppression docs ([#21884](https://github.com/astral-sh/ruff/pull/21884))
- Update mkdocs-material to 9.7.0 (Insiders now free) ([#21797](https://github.com/astral-sh/ruff/pull/21797))
### Contributors
- [@Avasam](https://github.com/Avasam)
- [@MichaReiser](https://github.com/MichaReiser)
- [@charliermarsh](https://github.com/charliermarsh)
- [@amyreese](https://github.com/amyreese)
- [@phongddo](https://github.com/phongddo)
- [@prakhar1144](https://github.com/prakhar1144)
- [@mahiro72](https://github.com/mahiro72)
- [@ntBre](https://github.com/ntBre)
- [@LoicRiegel](https://github.com/LoicRiegel)
## 0.14.8 ## 0.14.8
Released on 2025-12-04. Released on 2025-12-04.

40
Cargo.lock generated
View File

@ -1016,7 +1016,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users", "redox_users",
"windows-sys 0.59.0", "windows-sys 0.61.0",
] ]
[[package]] [[package]]
@ -1108,7 +1108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.61.0",
] ]
[[package]] [[package]]
@ -1238,9 +1238,9 @@ dependencies = [
[[package]] [[package]]
name = "get-size-derive2" name = "get-size-derive2"
version = "0.7.2" version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff47daa61505c85af126e9dd64af6a342a33dc0cccfe1be74ceadc7d352e6efd" checksum = "ab21d7bd2c625f2064f04ce54bcb88bc57c45724cde45cba326d784e22d3f71a"
dependencies = [ dependencies = [
"attribute-derive", "attribute-derive",
"quote", "quote",
@ -1249,14 +1249,15 @@ dependencies = [
[[package]] [[package]]
name = "get-size2" name = "get-size2"
version = "0.7.2" version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac7bb8710e1f09672102be7ddf39f764d8440ae74a9f4e30aaa4820dcdffa4af" checksum = "879272b0de109e2b67b39fcfe3d25fdbba96ac07e44a254f5a0b4d7ff55340cb"
dependencies = [ dependencies = [
"compact_str", "compact_str",
"get-size-derive2", "get-size-derive2",
"hashbrown 0.16.1", "hashbrown 0.16.1",
"indexmap", "indexmap",
"ordermap",
"smallvec", "smallvec",
] ]
@ -1763,7 +1764,7 @@ dependencies = [
"portable-atomic", "portable-atomic",
"portable-atomic-util", "portable-atomic-util",
"serde_core", "serde_core",
"windows-sys 0.59.0", "windows-sys 0.61.0",
] ]
[[package]] [[package]]
@ -2233,9 +2234,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "ordermap" name = "ordermap"
version = "0.5.12" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b100f7dd605611822d30e182214d3c02fdefce2d801d23993f6b6ba6ca1392af" checksum = "ed637741ced8fb240855d22a2b4f208dab7a06bcce73380162e5253000c16758"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -2859,7 +2860,7 @@ dependencies = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.14.8" version = "0.14.9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argfile", "argfile",
@ -3117,7 +3118,7 @@ dependencies = [
[[package]] [[package]]
name = "ruff_linter" name = "ruff_linter"
version = "0.14.8" version = "0.14.9"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"anyhow", "anyhow",
@ -3348,6 +3349,7 @@ dependencies = [
"compact_str", "compact_str",
"get-size2", "get-size2",
"insta", "insta",
"itertools 0.14.0",
"memchr", "memchr",
"ruff_annotate_snippets", "ruff_annotate_snippets",
"ruff_python_ast", "ruff_python_ast",
@ -3473,7 +3475,7 @@ dependencies = [
[[package]] [[package]]
name = "ruff_wasm" name = "ruff_wasm"
version = "0.14.8" version = "0.14.9"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"console_log", "console_log",
@ -3571,7 +3573,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.59.0", "windows-sys 0.61.0",
] ]
[[package]] [[package]]
@ -3589,7 +3591,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]] [[package]]
name = "salsa" name = "salsa"
version = "0.24.0" version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0#59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0" source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1"
dependencies = [ dependencies = [
"boxcar", "boxcar",
"compact_str", "compact_str",
@ -3600,6 +3602,7 @@ dependencies = [
"indexmap", "indexmap",
"intrusive-collections", "intrusive-collections",
"inventory", "inventory",
"ordermap",
"parking_lot", "parking_lot",
"portable-atomic", "portable-atomic",
"rustc-hash", "rustc-hash",
@ -3613,12 +3616,12 @@ dependencies = [
[[package]] [[package]]
name = "salsa-macro-rules" name = "salsa-macro-rules"
version = "0.24.0" version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0#59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0" source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1"
[[package]] [[package]]
name = "salsa-macros" name = "salsa-macros"
version = "0.24.0" version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0#59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0" source = "git+https://github.com/salsa-rs/salsa.git?rev=55e5e7d32fa3fc189276f35bb04c9438f9aedbd1#55e5e7d32fa3fc189276f35bb04c9438f9aedbd1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3972,7 +3975,7 @@ dependencies = [
"getrandom 0.3.4", "getrandom 0.3.4",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.59.0", "windows-sys 0.61.0",
] ]
[[package]] [[package]]
@ -4557,6 +4560,7 @@ dependencies = [
"anyhow", "anyhow",
"camino", "camino",
"colored 3.0.0", "colored 3.0.0",
"dunce",
"insta", "insta",
"memchr", "memchr",
"path-slash", "path-slash",
@ -5025,7 +5029,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.61.0",
] ]
[[package]] [[package]]

View File

@ -88,7 +88,7 @@ etcetera = { version = "0.11.0" }
fern = { version = "0.7.0" } fern = { version = "0.7.0" }
filetime = { version = "0.2.23" } filetime = { version = "0.2.23" }
getrandom = { version = "0.3.1" } getrandom = { version = "0.3.1" }
get-size2 = { version = "0.7.0", features = [ get-size2 = { version = "0.7.3", features = [
"derive", "derive",
"smallvec", "smallvec",
"hashbrown", "hashbrown",
@ -129,7 +129,7 @@ memchr = { version = "2.7.1" }
mimalloc = { version = "0.1.39" } mimalloc = { version = "0.1.39" }
natord = { version = "1.0.9" } natord = { version = "1.0.9" }
notify = { version = "8.0.0" } notify = { version = "8.0.0" }
ordermap = { version = "0.5.0" } ordermap = { version = "1.0.0" }
path-absolutize = { version = "3.1.1" } path-absolutize = { version = "3.1.1" }
path-slash = { version = "0.2.1" } path-slash = { version = "0.2.1" }
pathdiff = { version = "0.2.1" } pathdiff = { version = "0.2.1" }
@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" } rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" } rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml` # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0", default-features = false, features = [ salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "55e5e7d32fa3fc189276f35bb04c9438f9aedbd1", default-features = false, features = [
"compact_str", "compact_str",
"macros", "macros",
"salsa_unstable", "salsa_unstable",

View File

@ -147,8 +147,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex" powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version. # For a specific version.
curl -LsSf https://astral.sh/ruff/0.14.8/install.sh | sh curl -LsSf https://astral.sh/ruff/0.14.9/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.8/install.ps1 | iex" powershell -c "irm https://astral.sh/ruff/0.14.9/install.ps1 | iex"
``` ```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@ -181,7 +181,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml ```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.14.8 rev: v0.14.9
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff-check - id: ruff-check

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ruff" name = "ruff"
version = "0.14.8" version = "0.14.9"
publish = true publish = true
authors = { workspace = true } authors = { workspace = true }
edition = { workspace = true } edition = { workspace = true }

View File

@ -1440,6 +1440,78 @@ def function():
Ok(()) Ok(())
} }
#[test]
fn ignore_noqa() -> Result<()> {
let fixture = CliTest::new()?;
fixture.write_file(
"ruff.toml",
r#"
[lint]
select = ["F401"]
"#,
)?;
fixture.write_file(
"noqa.py",
r#"
import os # noqa: F401
# ruff: disable[F401]
import sys
"#,
)?;
// without --ignore-noqa
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py"),
@r"
success: false
exit_code: 1
----- stdout -----
noqa.py:5:8: F401 [*] `sys` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
");
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.args(["--preview"]),
@r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
");
// with --ignore-noqa --preview
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.args(["--ignore-noqa", "--preview"]),
@r"
success: false
exit_code: 1
----- stdout -----
noqa.py:2:8: F401 [*] `os` imported but unused
noqa.py:5:8: F401 [*] `sys` imported but unused
Found 2 errors.
[*] 2 fixable with the `--fix` option.
----- stderr -----
");
Ok(())
}
#[test] #[test]
fn add_noqa() -> Result<()> { fn add_noqa() -> Result<()> {
let fixture = CliTest::new()?; let fixture = CliTest::new()?;
@ -1632,6 +1704,100 @@ def unused(x): # noqa: ANN001, ARG001, D103
Ok(()) Ok(())
} }
#[test]
fn add_noqa_existing_file_level_noqa() -> Result<()> {
let fixture = CliTest::new()?;
fixture.write_file(
"ruff.toml",
r#"
[lint]
select = ["F401"]
"#,
)?;
fixture.write_file(
"noqa.py",
r#"
# ruff: noqa F401
import os
"#,
)?;
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.arg("--preview")
.args(["--add-noqa"])
.arg("-")
.pass_stdin(r#"
"#), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
let test_code =
fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file");
insta::assert_snapshot!(test_code, @r"
# ruff: noqa F401
import os
");
Ok(())
}
#[test]
fn add_noqa_existing_range_suppression() -> Result<()> {
let fixture = CliTest::new()?;
fixture.write_file(
"ruff.toml",
r#"
[lint]
select = ["F401"]
"#,
)?;
fixture.write_file(
"noqa.py",
r#"
# ruff: disable[F401]
import os
"#,
)?;
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.arg("--preview")
.args(["--add-noqa"])
.arg("-")
.pass_stdin(r#"
"#), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
let test_code =
fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file");
insta::assert_snapshot!(test_code, @r"
# ruff: disable[F401]
import os
");
Ok(())
}
#[test] #[test]
fn add_noqa_multiline_comment() -> Result<()> { fn add_noqa_multiline_comment() -> Result<()> {
let fixture = CliTest::new()?; let fixture = CliTest::new()?;

View File

@ -194,7 +194,7 @@ static SYMPY: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17", max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312, python_version: PythonVersion::PY312,
}, },
13000, 13030,
); );
static TANJUN: Benchmark = Benchmark::new( static TANJUN: Benchmark = Benchmark::new(

View File

@ -166,28 +166,8 @@ impl Diagnostic {
/// Returns the primary message for this diagnostic. /// Returns the primary message for this diagnostic.
/// ///
/// A diagnostic always has a message, but it may be empty. /// A diagnostic always has a message, but it may be empty.
///
/// NOTE: At present, this routine will return the first primary
/// annotation's message as the primary message when the main diagnostic
/// message is empty. This is meant to facilitate an incremental migration
/// in ty over to the new diagnostic data model. (The old data model
/// didn't distinguish between messages on the entire diagnostic and
/// messages attached to a particular span.)
pub fn primary_message(&self) -> &str { pub fn primary_message(&self) -> &str {
if !self.inner.message.as_str().is_empty() { self.inner.message.as_str()
return self.inner.message.as_str();
}
// FIXME: As a special case, while we're migrating ty
// to the new diagnostic data model, we'll look for a primary
// message from the primary annotation. This is because most
// ty diagnostics are created with an empty diagnostic
// message and instead attach the message to the annotation.
// Fixing this will require touching basically every diagnostic
// in ty, so we do it this way for now to match the old
// semantics. ---AG
self.primary_annotation()
.and_then(|ann| ann.get_message())
.unwrap_or_default()
} }
/// Introspects this diagnostic and returns what kind of "primary" message /// Introspects this diagnostic and returns what kind of "primary" message
@ -199,18 +179,6 @@ impl Diagnostic {
/// contains *essential* information or context for understanding the /// contains *essential* information or context for understanding the
/// diagnostic. /// diagnostic.
/// ///
/// The reason why we don't just always return both the main diagnostic
/// message and the primary annotation message is because this was written
/// in the midst of an incremental migration of ty over to the new
/// diagnostic data model. At time of writing, diagnostics were still
/// constructed in the old model where the main diagnostic message and the
/// primary annotation message were not distinguished from each other. So
/// for now, we carefully return what kind of messages this diagnostic
/// contains. In effect, if this diagnostic has a non-empty main message
/// *and* a non-empty primary annotation message, then the diagnostic is
/// 100% using the new diagnostic data model and we can format things
/// appropriately.
///
/// The type returned implements the `std::fmt::Display` trait. In most /// The type returned implements the `std::fmt::Display` trait. In most
/// cases, just converting it to a string (or printing it) will do what /// cases, just converting it to a string (or printing it) will do what
/// you want. /// you want.
@ -224,11 +192,10 @@ impl Diagnostic {
.primary_annotation() .primary_annotation()
.and_then(|ann| ann.get_message()) .and_then(|ann| ann.get_message())
.unwrap_or_default(); .unwrap_or_default();
match (main.is_empty(), annotation.is_empty()) { if annotation.is_empty() {
(false, true) => ConciseMessage::MainDiagnostic(main), ConciseMessage::MainDiagnostic(main)
(true, false) => ConciseMessage::PrimaryAnnotation(annotation), } else {
(false, false) => ConciseMessage::Both { main, annotation }, ConciseMessage::Both { main, annotation }
(true, true) => ConciseMessage::Empty,
} }
} }
@ -693,18 +660,6 @@ impl SubDiagnostic {
/// contains *essential* information or context for understanding the /// contains *essential* information or context for understanding the
/// diagnostic. /// diagnostic.
/// ///
/// The reason why we don't just always return both the main diagnostic
/// message and the primary annotation message is because this was written
/// in the midst of an incremental migration of ty over to the new
/// diagnostic data model. At time of writing, diagnostics were still
/// constructed in the old model where the main diagnostic message and the
/// primary annotation message were not distinguished from each other. So
/// for now, we carefully return what kind of messages this diagnostic
/// contains. In effect, if this diagnostic has a non-empty main message
/// *and* a non-empty primary annotation message, then the diagnostic is
/// 100% using the new diagnostic data model and we can format things
/// appropriately.
///
/// The type returned implements the `std::fmt::Display` trait. In most /// The type returned implements the `std::fmt::Display` trait. In most
/// cases, just converting it to a string (or printing it) will do what /// cases, just converting it to a string (or printing it) will do what
/// you want. /// you want.
@ -714,11 +669,10 @@ impl SubDiagnostic {
.primary_annotation() .primary_annotation()
.and_then(|ann| ann.get_message()) .and_then(|ann| ann.get_message())
.unwrap_or_default(); .unwrap_or_default();
match (main.is_empty(), annotation.is_empty()) { if annotation.is_empty() {
(false, true) => ConciseMessage::MainDiagnostic(main), ConciseMessage::MainDiagnostic(main)
(true, false) => ConciseMessage::PrimaryAnnotation(annotation), } else {
(false, false) => ConciseMessage::Both { main, annotation }, ConciseMessage::Both { main, annotation }
(true, true) => ConciseMessage::Empty,
} }
} }
} }
@ -888,6 +842,10 @@ impl Annotation {
pub fn hide_snippet(&mut self, yes: bool) { pub fn hide_snippet(&mut self, yes: bool) {
self.hide_snippet = yes; self.hide_snippet = yes;
} }
pub fn is_primary(&self) -> bool {
self.is_primary
}
} }
/// Tags that can be associated with an annotation. /// Tags that can be associated with an annotation.
@ -1508,28 +1466,10 @@ pub enum DiagnosticFormat {
pub enum ConciseMessage<'a> { pub enum ConciseMessage<'a> {
/// A diagnostic contains a non-empty main message and an empty /// A diagnostic contains a non-empty main message and an empty
/// primary annotation message. /// primary annotation message.
///
/// This strongly suggests that the diagnostic is using the
/// "new" data model.
MainDiagnostic(&'a str), MainDiagnostic(&'a str),
/// A diagnostic contains an empty main message and a non-empty
/// primary annotation message.
///
/// This strongly suggests that the diagnostic is using the
/// "old" data model.
PrimaryAnnotation(&'a str),
/// A diagnostic contains a non-empty main message and a non-empty /// A diagnostic contains a non-empty main message and a non-empty
/// primary annotation message. /// primary annotation message.
///
/// This strongly suggests that the diagnostic is using the
/// "new" data model.
Both { main: &'a str, annotation: &'a str }, Both { main: &'a str, annotation: &'a str },
/// A diagnostic contains an empty main message and an empty
/// primary annotation message.
///
/// This indicates that the diagnostic is probably using the old
/// model.
Empty,
/// A custom concise message has been provided. /// A custom concise message has been provided.
Custom(&'a str), Custom(&'a str),
} }
@ -1540,13 +1480,9 @@ impl std::fmt::Display for ConciseMessage<'_> {
ConciseMessage::MainDiagnostic(main) => { ConciseMessage::MainDiagnostic(main) => {
write!(f, "{main}") write!(f, "{main}")
} }
ConciseMessage::PrimaryAnnotation(annotation) => {
write!(f, "{annotation}")
}
ConciseMessage::Both { main, annotation } => { ConciseMessage::Both { main, annotation } => {
write!(f, "{main}: {annotation}") write!(f, "{main}: {annotation}")
} }
ConciseMessage::Empty => Ok(()),
ConciseMessage::Custom(message) => { ConciseMessage::Custom(message) => {
write!(f, "{message}") write!(f, "{message}")
} }

View File

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

View File

@ -199,6 +199,9 @@ def bytes_okay(value=bytes(1)):
def int_okay(value=int("12")): def int_okay(value=int("12")):
pass pass
# Allow immutable slice()
def slice_okay(value=slice(1,2)):
pass
# Allow immutable complex() value # Allow immutable complex() value
def complex_okay(value=complex(1,2)): def complex_okay(value=complex(1,2)):

View File

@ -218,3 +218,26 @@ def should_not_fail(payload, Args):
Args: Args:
The other arguments. The other arguments.
""" """
# Test cases for Unpack[TypedDict] kwargs
from typing import TypedDict
from typing_extensions import Unpack
class User(TypedDict):
id: int
name: str
def function_with_unpack_args_should_not_fail(query: str, **kwargs: Unpack[User]):
"""Function with Unpack kwargs.
Args:
query: some arg
"""
def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]):
"""Function with Unpack kwargs but missing query arg documentation.
Args:
**kwargs: keyword arguments
"""

View File

@ -2,15 +2,40 @@ from abc import ABC, abstractmethod
from contextlib import suppress from contextlib import suppress
class MyError(Exception):
...
class MySubError(MyError):
...
class MyValueError(ValueError):
...
class MyUserWarning(UserWarning):
...
# Violation test cases with builtin errors: PLW0133
# Test case 1: Useless exception statement # Test case 1: Useless exception statement
def func(): def func():
AssertionError("This is an assertion error") # PLW0133 AssertionError("This is an assertion error") # PLW0133
MyError("This is a custom error") # PLW0133
MySubError("This is a custom error") # PLW0133
MyValueError("This is a custom value error") # PLW0133
# Test case 2: Useless exception statement in try-except block # Test case 2: Useless exception statement in try-except block
def func(): def func():
try: try:
Exception("This is an exception") # PLW0133 Exception("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
except Exception as err: except Exception as err:
pass pass
@ -19,6 +44,9 @@ def func():
def func(): def func():
if True: if True:
RuntimeError("This is an exception") # PLW0133 RuntimeError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
# Test case 4: Useless exception statement in class # Test case 4: Useless exception statement in class
@ -26,12 +54,18 @@ def func():
class Class: class Class:
def __init__(self): def __init__(self):
TypeError("This is an exception") # PLW0133 TypeError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
# Test case 5: Useless exception statement in function # Test case 5: Useless exception statement in function
def func(): def func():
def inner(): def inner():
IndexError("This is an exception") # PLW0133 IndexError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
inner() inner()
@ -40,6 +74,9 @@ def func():
def func(): def func():
while True: while True:
KeyError("This is an exception") # PLW0133 KeyError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
# Test case 7: Useless exception statement in abstract class # Test case 7: Useless exception statement in abstract class
@ -48,27 +85,58 @@ def func():
@abstractmethod @abstractmethod
def method(self): def method(self):
NotImplementedError("This is an exception") # PLW0133 NotImplementedError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
# Test case 8: Useless exception statement inside context manager # Test case 8: Useless exception statement inside context manager
def func(): def func():
with suppress(AttributeError): with suppress(Exception):
AttributeError("This is an exception") # PLW0133 AttributeError("This is an exception") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
# Test case 9: Useless exception statement in parentheses # Test case 9: Useless exception statement in parentheses
def func(): def func():
(RuntimeError("This is an exception")) # PLW0133 (RuntimeError("This is an exception")) # PLW0133
(MyError("This is an exception")) # PLW0133
(MySubError("This is an exception")) # PLW0133
(MyValueError("This is an exception")) # PLW0133
# Test case 10: Useless exception statement in continuation # Test case 10: Useless exception statement in continuation
def func(): def func():
x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133 x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
x = 1; (MyError("This is an exception")); y = 2 # PLW0133
x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
# Test case 11: Useless warning statement # Test case 11: Useless warning statement
def func(): def func():
UserWarning("This is an assertion error") # PLW0133 UserWarning("This is a user warning") # PLW0133
MyUserWarning("This is a custom user warning") # PLW0133
# Test case 12: Useless exception statement at module level
import builtins
builtins.TypeError("still an exception even though it's an Attribute") # PLW0133
PythonFinalizationError("Added in Python 3.13") # PLW0133
MyError("This is an exception") # PLW0133
MySubError("This is an exception") # PLW0133
MyValueError("This is an exception") # PLW0133
UserWarning("This is a user warning") # PLW0133
MyUserWarning("This is a custom user warning") # PLW0133
# Non-violation test cases: PLW0133 # Non-violation test cases: PLW0133
@ -119,10 +187,3 @@ def func():
def func(): def func():
with suppress(AttributeError): with suppress(AttributeError):
raise AttributeError("This is an exception") # OK raise AttributeError("This is an exception") # OK
import builtins
builtins.TypeError("still an exception even though it's an Attribute")
PythonFinalizationError("Added in Python 3.13")

View File

@ -0,0 +1,88 @@
def f():
# These should both be ignored by the range suppression.
# ruff: disable[E741, F841]
I = 1
# ruff: enable[E741, F841]
def f():
# These should both be ignored by the implicit range suppression.
# Should also generate an "unmatched suppression" warning.
# ruff:disable[E741,F841]
I = 1
def f():
# Neither warning is ignored, and an "unmatched suppression"
# should be generated.
I = 1
# ruff: enable[E741, F841]
def f():
# One should be ignored by the range suppression, and
# the other logged to the user.
# ruff: disable[E741]
I = 1
# ruff: enable[E741]
def f():
# Test interleaved range suppressions. The first and last
# lines should each log a different warning, while the
# middle line should be completely silenced.
# ruff: disable[E741]
l = 0
# ruff: disable[F841]
O = 1
# ruff: enable[E741]
I = 2
# ruff: enable[F841]
def f():
# Neither of these are ignored and warnings are
# logged to user
# ruff: disable[E501]
I = 1
# ruff: enable[E501]
def f():
# These should both be ignored by the range suppression,
# and an unusued noqa diagnostic should be logged.
# ruff:disable[E741,F841]
I = 1 # noqa: E741,F841
# ruff:enable[E741,F841]
def f():
# TODO: Duplicate codes should be counted as duplicate, not unused
# ruff: disable[F841, F841]
foo = 0
def f():
# Overlapping range suppressions, one should be marked as used,
# and the other should trigger an unused suppression diagnostic
# ruff: disable[F841]
# ruff: disable[F841]
foo = 0
def f():
# Multiple codes but only one is used
# ruff: disable[E741, F401, F841]
foo = 0
def f():
# Multiple codes but only two are used
# ruff: disable[E741, F401, F841]
I = 0
def f():
# Multiple codes but none are used
# ruff: disable[E741, F401, F841]
print("hello")

View File

@ -437,6 +437,15 @@ impl<'a> Checker<'a> {
} }
} }
/// Returns the [`Tokens`] for the parsed source file.
///
///
/// Unlike [`Self::tokens`], this method always returns
/// the tokens for the current file, even when within a parsed type annotation.
pub(crate) fn source_tokens(&self) -> &'a Tokens {
self.parsed.tokens()
}
/// The [`Locator`] for the current file, which enables extraction of source code from byte /// The [`Locator`] for the current file, which enables extraction of source code from byte
/// offsets. /// offsets.
pub(crate) const fn locator(&self) -> &'a Locator<'a> { pub(crate) const fn locator(&self) -> &'a Locator<'a> {

View File

@ -12,17 +12,20 @@ use crate::fix::edits::delete_comment;
use crate::noqa::{ use crate::noqa::{
Code, Directive, FileExemption, FileNoqaDirectives, NoqaDirectives, NoqaMapping, Code, Directive, FileExemption, FileNoqaDirectives, NoqaDirectives, NoqaMapping,
}; };
use crate::preview::is_range_suppressions_enabled;
use crate::registry::Rule; use crate::registry::Rule;
use crate::rule_redirects::get_redirect_target; use crate::rule_redirects::get_redirect_target;
use crate::rules::pygrep_hooks; use crate::rules::pygrep_hooks;
use crate::rules::ruff; use crate::rules::ruff;
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA}; use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA};
use crate::settings::LinterSettings; use crate::settings::LinterSettings;
use crate::suppression::Suppressions;
use crate::{Edit, Fix, Locator}; use crate::{Edit, Fix, Locator};
use super::ast::LintContext; use super::ast::LintContext;
/// RUF100 /// RUF100
#[expect(clippy::too_many_arguments)]
pub(crate) fn check_noqa( pub(crate) fn check_noqa(
context: &mut LintContext, context: &mut LintContext,
path: &Path, path: &Path,
@ -31,6 +34,7 @@ pub(crate) fn check_noqa(
noqa_line_for: &NoqaMapping, noqa_line_for: &NoqaMapping,
analyze_directives: bool, analyze_directives: bool,
settings: &LinterSettings, settings: &LinterSettings,
suppressions: &Suppressions,
) -> Vec<usize> { ) -> Vec<usize> {
// Identify any codes that are globally exempted (within the current file). // Identify any codes that are globally exempted (within the current file).
let file_noqa_directives = let file_noqa_directives =
@ -40,7 +44,7 @@ pub(crate) fn check_noqa(
let mut noqa_directives = let mut noqa_directives =
NoqaDirectives::from_commented_ranges(comment_ranges, &settings.external, path, locator); NoqaDirectives::from_commented_ranges(comment_ranges, &settings.external, path, locator);
if file_noqa_directives.is_empty() && noqa_directives.is_empty() { if file_noqa_directives.is_empty() && noqa_directives.is_empty() && suppressions.is_empty() {
return Vec::new(); return Vec::new();
} }
@ -60,11 +64,19 @@ pub(crate) fn check_noqa(
continue; continue;
} }
// Apply file-level suppressions first
if exemption.contains_secondary_code(code) { if exemption.contains_secondary_code(code) {
ignored_diagnostics.push(index); ignored_diagnostics.push(index);
continue; continue;
} }
// Apply ranged suppressions next
if is_range_suppressions_enabled(settings) && suppressions.check_diagnostic(diagnostic) {
ignored_diagnostics.push(index);
continue;
}
// Apply end-of-line noqa suppressions last
let noqa_offsets = diagnostic let noqa_offsets = diagnostic
.parent() .parent()
.into_iter() .into_iter()
@ -107,6 +119,9 @@ pub(crate) fn check_noqa(
} }
} }
// Diagnostics for unused/invalid range suppressions
suppressions.check_suppressions(context, locator);
// Enforce that the noqa directive was actually used (RUF100), unless RUF100 was itself // Enforce that the noqa directive was actually used (RUF100), unless RUF100 was itself
// suppressed. // suppressed.
if context.is_rule_enabled(Rule::UnusedNOQA) if context.is_rule_enabled(Rule::UnusedNOQA)
@ -128,8 +143,13 @@ pub(crate) fn check_noqa(
Directive::All(directive) => { Directive::All(directive) => {
if matches.is_empty() { if matches.is_empty() {
let edit = delete_comment(directive.range(), locator); let edit = delete_comment(directive.range(), locator);
let mut diagnostic = context let mut diagnostic = context.report_diagnostic(
.report_diagnostic(UnusedNOQA { codes: None }, directive.range()); UnusedNOQA {
codes: None,
kind: ruff::rules::UnusedNOQAKind::Noqa,
},
directive.range(),
);
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Unnecessary); diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Unnecessary);
diagnostic.set_fix(Fix::safe_edit(edit)); diagnostic.set_fix(Fix::safe_edit(edit));
} }
@ -224,6 +244,7 @@ pub(crate) fn check_noqa(
.map(|code| (*code).to_string()) .map(|code| (*code).to_string())
.collect(), .collect(),
}), }),
kind: ruff::rules::UnusedNOQAKind::Noqa,
}, },
directive.range(), directive.range(),
); );

View File

@ -3,14 +3,13 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use ruff_python_ast::AnyNodeRef; use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::{self, Tokens, parenthesized_range};
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Parameters, Stmt}; use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Parameters, Stmt};
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_trivia::textwrap::dedent_to; use ruff_python_trivia::textwrap::dedent_to;
use ruff_python_trivia::{ use ruff_python_trivia::{
CommentRanges, PythonWhitespace, SimpleTokenKind, SimpleTokenizer, has_leading_content, PythonWhitespace, SimpleTokenKind, SimpleTokenizer, has_leading_content, is_python_whitespace,
is_python_whitespace,
}; };
use ruff_source_file::{LineRanges, NewlineWithTrailingNewline, UniversalNewlines}; use ruff_source_file::{LineRanges, NewlineWithTrailingNewline, UniversalNewlines};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
@ -209,7 +208,7 @@ pub(crate) fn remove_argument<T: Ranged>(
arguments: &Arguments, arguments: &Arguments,
parentheses: Parentheses, parentheses: Parentheses,
source: &str, source: &str,
comment_ranges: &CommentRanges, tokens: &Tokens,
) -> Result<Edit> { ) -> Result<Edit> {
// Partition into arguments before and after the argument to remove. // Partition into arguments before and after the argument to remove.
let (before, after): (Vec<_>, Vec<_>) = arguments let (before, after): (Vec<_>, Vec<_>) = arguments
@ -224,7 +223,7 @@ pub(crate) fn remove_argument<T: Ranged>(
.context("Unable to find argument")?; .context("Unable to find argument")?;
let parenthesized_range = let parenthesized_range =
parenthesized_range(arg.value().into(), arguments.into(), comment_ranges, source) token::parenthesized_range(arg.value().into(), arguments.into(), tokens)
.unwrap_or(arg.range()); .unwrap_or(arg.range());
if !after.is_empty() { if !after.is_empty() {
@ -270,24 +269,13 @@ pub(crate) fn remove_argument<T: Ranged>(
/// ///
/// The new argument will be inserted before the first existing keyword argument in `arguments`, if /// The new argument will be inserted before the first existing keyword argument in `arguments`, if
/// there are any present. Otherwise, the new argument is added to the end of the argument list. /// there are any present. Otherwise, the new argument is added to the end of the argument list.
pub(crate) fn add_argument( pub(crate) fn add_argument(argument: &str, arguments: &Arguments, tokens: &Tokens) -> Edit {
argument: &str,
arguments: &Arguments,
comment_ranges: &CommentRanges,
source: &str,
) -> Edit {
if let Some(ast::Keyword { range, value, .. }) = arguments.keywords.first() { if let Some(ast::Keyword { range, value, .. }) = arguments.keywords.first() {
let keyword = parenthesized_range(value.into(), arguments.into(), comment_ranges, source) let keyword = parenthesized_range(value.into(), arguments.into(), tokens).unwrap_or(*range);
.unwrap_or(*range);
Edit::insertion(format!("{argument}, "), keyword.start()) Edit::insertion(format!("{argument}, "), keyword.start())
} else if let Some(last) = arguments.arguments_source_order().last() { } else if let Some(last) = arguments.arguments_source_order().last() {
// Case 1: existing arguments, so append after the last argument. // Case 1: existing arguments, so append after the last argument.
let last = parenthesized_range( let last = parenthesized_range(last.value().into(), arguments.into(), tokens)
last.value().into(),
arguments.into(),
comment_ranges,
source,
)
.unwrap_or(last.range()); .unwrap_or(last.range());
Edit::insertion(format!(", {argument}"), last.end()) Edit::insertion(format!(", {argument}"), last.end())
} else { } else {

View File

@ -32,6 +32,7 @@ use crate::rules::ruff::rules::test_rules::{self, TEST_RULES, TestRule};
use crate::settings::types::UnsafeFixes; use crate::settings::types::UnsafeFixes;
use crate::settings::{LinterSettings, TargetVersion, flags}; use crate::settings::{LinterSettings, TargetVersion, flags};
use crate::source_kind::SourceKind; use crate::source_kind::SourceKind;
use crate::suppression::Suppressions;
use crate::{Locator, directives, fs}; use crate::{Locator, directives, fs};
pub(crate) mod float; pub(crate) mod float;
@ -128,6 +129,7 @@ pub fn check_path(
source_type: PySourceType, source_type: PySourceType,
parsed: &Parsed<ModModule>, parsed: &Parsed<ModModule>,
target_version: TargetVersion, target_version: TargetVersion,
suppressions: &Suppressions,
) -> Vec<Diagnostic> { ) -> Vec<Diagnostic> {
// Aggregate all diagnostics. // Aggregate all diagnostics.
let mut context = LintContext::new(path, locator.contents(), settings); let mut context = LintContext::new(path, locator.contents(), settings);
@ -339,6 +341,7 @@ pub fn check_path(
&directives.noqa_line_for, &directives.noqa_line_for,
parsed.has_valid_syntax(), parsed.has_valid_syntax(),
settings, settings,
suppressions,
); );
if noqa.is_enabled() { if noqa.is_enabled() {
for index in ignored.iter().rev() { for index in ignored.iter().rev() {
@ -400,6 +403,9 @@ pub fn add_noqa_to_path(
&indexer, &indexer,
); );
// Parse range suppression comments
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
// Generate diagnostics, ignoring any existing `noqa` directives. // Generate diagnostics, ignoring any existing `noqa` directives.
let diagnostics = check_path( let diagnostics = check_path(
path, path,
@ -414,6 +420,7 @@ pub fn add_noqa_to_path(
source_type, source_type,
&parsed, &parsed,
target_version, target_version,
&suppressions,
); );
// Add any missing `# noqa` pragmas. // Add any missing `# noqa` pragmas.
@ -427,6 +434,7 @@ pub fn add_noqa_to_path(
&directives.noqa_line_for, &directives.noqa_line_for,
stylist.line_ending(), stylist.line_ending(),
reason, reason,
&suppressions,
) )
} }
@ -461,6 +469,9 @@ pub fn lint_only(
&indexer, &indexer,
); );
// Parse range suppression comments
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
// Generate diagnostics. // Generate diagnostics.
let diagnostics = check_path( let diagnostics = check_path(
path, path,
@ -475,6 +486,7 @@ pub fn lint_only(
source_type, source_type,
&parsed, &parsed,
target_version, target_version,
&suppressions,
); );
LinterResult { LinterResult {
@ -566,6 +578,9 @@ pub fn lint_fix<'a>(
&indexer, &indexer,
); );
// Parse range suppression comments
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
// Generate diagnostics. // Generate diagnostics.
let diagnostics = check_path( let diagnostics = check_path(
path, path,
@ -580,6 +595,7 @@ pub fn lint_fix<'a>(
source_type, source_type,
&parsed, &parsed,
target_version, target_version,
&suppressions,
); );
if iterations == 0 { if iterations == 0 {
@ -769,6 +785,7 @@ mod tests {
use crate::registry::Rule; use crate::registry::Rule;
use crate::settings::LinterSettings; use crate::settings::LinterSettings;
use crate::source_kind::SourceKind; use crate::source_kind::SourceKind;
use crate::suppression::Suppressions;
use crate::test::{TestedNotebook, assert_notebook_path, test_contents, test_snippet}; use crate::test::{TestedNotebook, assert_notebook_path, test_contents, test_snippet};
use crate::{Locator, assert_diagnostics, directives, settings}; use crate::{Locator, assert_diagnostics, directives, settings};
@ -944,6 +961,7 @@ mod tests {
&locator, &locator,
&indexer, &indexer,
); );
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
let mut diagnostics = check_path( let mut diagnostics = check_path(
path, path,
None, None,
@ -957,6 +975,7 @@ mod tests {
source_type, source_type,
&parsed, &parsed,
target_version, target_version,
&suppressions,
); );
diagnostics.sort_by(Diagnostic::ruff_start_ordering); diagnostics.sort_by(Diagnostic::ruff_start_ordering);
diagnostics diagnostics

View File

@ -20,12 +20,14 @@ use crate::Locator;
use crate::fs::relativize_path; use crate::fs::relativize_path;
use crate::registry::Rule; use crate::registry::Rule;
use crate::rule_redirects::get_redirect_target; use crate::rule_redirects::get_redirect_target;
use crate::suppression::Suppressions;
/// Generates an array of edits that matches the length of `messages`. /// Generates an array of edits that matches the length of `messages`.
/// Each potential edit in the array is paired, in order, with the associated diagnostic. /// Each potential edit in the array is paired, in order, with the associated diagnostic.
/// Each edit will add a `noqa` comment to the appropriate line in the source to hide /// Each edit will add a `noqa` comment to the appropriate line in the source to hide
/// the diagnostic. These edits may conflict with each other and should not be applied /// the diagnostic. These edits may conflict with each other and should not be applied
/// simultaneously. /// simultaneously.
#[expect(clippy::too_many_arguments)]
pub fn generate_noqa_edits( pub fn generate_noqa_edits(
path: &Path, path: &Path,
diagnostics: &[Diagnostic], diagnostics: &[Diagnostic],
@ -34,11 +36,19 @@ pub fn generate_noqa_edits(
external: &[String], external: &[String],
noqa_line_for: &NoqaMapping, noqa_line_for: &NoqaMapping,
line_ending: LineEnding, line_ending: LineEnding,
suppressions: &Suppressions,
) -> Vec<Option<Edit>> { ) -> Vec<Option<Edit>> {
let file_directives = FileNoqaDirectives::extract(locator, comment_ranges, external, path); let file_directives = FileNoqaDirectives::extract(locator, comment_ranges, external, path);
let exemption = FileExemption::from(&file_directives); let exemption = FileExemption::from(&file_directives);
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator); let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator);
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for); let comments = find_noqa_comments(
diagnostics,
locator,
&exemption,
&directives,
noqa_line_for,
suppressions,
);
build_noqa_edits_by_diagnostic(comments, locator, line_ending, None) build_noqa_edits_by_diagnostic(comments, locator, line_ending, None)
} }
@ -725,6 +735,7 @@ pub(crate) fn add_noqa(
noqa_line_for: &NoqaMapping, noqa_line_for: &NoqaMapping,
line_ending: LineEnding, line_ending: LineEnding,
reason: Option<&str>, reason: Option<&str>,
suppressions: &Suppressions,
) -> Result<usize> { ) -> Result<usize> {
let (count, output) = add_noqa_inner( let (count, output) = add_noqa_inner(
path, path,
@ -735,6 +746,7 @@ pub(crate) fn add_noqa(
noqa_line_for, noqa_line_for,
line_ending, line_ending,
reason, reason,
suppressions,
); );
fs::write(path, output)?; fs::write(path, output)?;
@ -751,6 +763,7 @@ fn add_noqa_inner(
noqa_line_for: &NoqaMapping, noqa_line_for: &NoqaMapping,
line_ending: LineEnding, line_ending: LineEnding,
reason: Option<&str>, reason: Option<&str>,
suppressions: &Suppressions,
) -> (usize, String) { ) -> (usize, String) {
let mut count = 0; let mut count = 0;
@ -760,7 +773,14 @@ fn add_noqa_inner(
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator); let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator);
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for); let comments = find_noqa_comments(
diagnostics,
locator,
&exemption,
&directives,
noqa_line_for,
suppressions,
);
let edits = build_noqa_edits_by_line(comments, locator, line_ending, reason); let edits = build_noqa_edits_by_line(comments, locator, line_ending, reason);
@ -859,6 +879,7 @@ fn find_noqa_comments<'a>(
exemption: &'a FileExemption, exemption: &'a FileExemption,
directives: &'a NoqaDirectives, directives: &'a NoqaDirectives,
noqa_line_for: &NoqaMapping, noqa_line_for: &NoqaMapping,
suppressions: &'a Suppressions,
) -> Vec<Option<NoqaComment<'a>>> { ) -> Vec<Option<NoqaComment<'a>>> {
// List of noqa comments, ordered to match up with `messages` // List of noqa comments, ordered to match up with `messages`
let mut comments_by_line: Vec<Option<NoqaComment<'a>>> = vec![]; let mut comments_by_line: Vec<Option<NoqaComment<'a>>> = vec![];
@ -875,6 +896,12 @@ fn find_noqa_comments<'a>(
continue; continue;
} }
// Apply ranged suppressions next
if suppressions.check_diagnostic(message) {
comments_by_line.push(None);
continue;
}
// Is the violation ignored by a `noqa` directive on the parent line? // Is the violation ignored by a `noqa` directive on the parent line?
if let Some(parent) = message.parent() { if let Some(parent) = message.parent() {
if let Some(directive_line) = if let Some(directive_line) =
@ -1253,6 +1280,7 @@ mod tests {
use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon}; use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon};
use crate::rules::pyflakes::rules::UnusedVariable; use crate::rules::pyflakes::rules::UnusedVariable;
use crate::rules::pyupgrade::rules::PrintfStringFormatting; use crate::rules::pyupgrade::rules::PrintfStringFormatting;
use crate::suppression::Suppressions;
use crate::{Edit, Violation}; use crate::{Edit, Violation};
use crate::{Locator, generate_noqa_edits}; use crate::{Locator, generate_noqa_edits};
@ -2848,6 +2876,7 @@ mod tests {
&noqa_line_for, &noqa_line_for,
LineEnding::Lf, LineEnding::Lf,
None, None,
&Suppressions::default(),
); );
assert_eq!(count, 0); assert_eq!(count, 0);
assert_eq!(output, format!("{contents}")); assert_eq!(output, format!("{contents}"));
@ -2872,6 +2901,7 @@ mod tests {
&noqa_line_for, &noqa_line_for,
LineEnding::Lf, LineEnding::Lf,
None, None,
&Suppressions::default(),
); );
assert_eq!(count, 1); assert_eq!(count, 1);
assert_eq!(output, "x = 1 # noqa: F841\n"); assert_eq!(output, "x = 1 # noqa: F841\n");
@ -2903,6 +2933,7 @@ mod tests {
&noqa_line_for, &noqa_line_for,
LineEnding::Lf, LineEnding::Lf,
None, None,
&Suppressions::default(),
); );
assert_eq!(count, 1); assert_eq!(count, 1);
assert_eq!(output, "x = 1 # noqa: E741, F841\n"); assert_eq!(output, "x = 1 # noqa: E741, F841\n");
@ -2934,6 +2965,7 @@ mod tests {
&noqa_line_for, &noqa_line_for,
LineEnding::Lf, LineEnding::Lf,
None, None,
&Suppressions::default(),
); );
assert_eq!(count, 0); assert_eq!(count, 0);
assert_eq!(output, "x = 1 # noqa"); assert_eq!(output, "x = 1 # noqa");
@ -2956,6 +2988,7 @@ print(
let messages = [PrintfStringFormatting let messages = [PrintfStringFormatting
.into_diagnostic(TextRange::new(12.into(), 79.into()), &source_file)]; .into_diagnostic(TextRange::new(12.into(), 79.into()), &source_file)];
let comment_ranges = CommentRanges::default(); let comment_ranges = CommentRanges::default();
let suppressions = Suppressions::default();
let edits = generate_noqa_edits( let edits = generate_noqa_edits(
path, path,
&messages, &messages,
@ -2964,6 +2997,7 @@ print(
&[], &[],
&noqa_line_for, &noqa_line_for,
LineEnding::Lf, LineEnding::Lf,
&suppressions,
); );
assert_eq!( assert_eq!(
edits, edits,
@ -2987,6 +3021,7 @@ bar =
[UselessSemicolon.into_diagnostic(TextRange::new(4.into(), 5.into()), &source_file)]; [UselessSemicolon.into_diagnostic(TextRange::new(4.into(), 5.into()), &source_file)];
let noqa_line_for = NoqaMapping::default(); let noqa_line_for = NoqaMapping::default();
let comment_ranges = CommentRanges::default(); let comment_ranges = CommentRanges::default();
let suppressions = Suppressions::default();
let edits = generate_noqa_edits( let edits = generate_noqa_edits(
path, path,
&messages, &messages,
@ -2995,6 +3030,7 @@ bar =
&[], &[],
&noqa_line_for, &noqa_line_for,
LineEnding::Lf, LineEnding::Lf,
&suppressions,
); );
assert_eq!( assert_eq!(
edits, edits,

View File

@ -9,6 +9,11 @@ use crate::settings::LinterSettings;
// Rule-specific behavior // Rule-specific behavior
// https://github.com/astral-sh/ruff/pull/21382
pub(crate) const fn is_custom_exception_checking_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/15541 // https://github.com/astral-sh/ruff/pull/15541
pub(crate) const fn is_suspicious_function_reference_enabled(settings: &LinterSettings) -> bool { pub(crate) const fn is_suspicious_function_reference_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled() settings.preview.is_enabled()
@ -286,3 +291,8 @@ pub(crate) const fn is_s310_resolve_string_literal_bindings_enabled(
) -> bool { ) -> bool {
settings.preview.is_enabled() settings.preview.is_enabled()
} }
// https://github.com/astral-sh/ruff/pull/21623
pub(crate) const fn is_range_suppressions_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@ -91,8 +91,8 @@ pub(crate) fn fastapi_redundant_response_model(checker: &Checker, function_def:
response_model_arg, response_model_arg,
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.source(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::unsafe_edit) .map(Fix::unsafe_edit)
}); });

View File

@ -74,12 +74,7 @@ pub(crate) fn map_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
checker checker
.report_diagnostic(MapWithoutExplicitStrict, call.range()) .report_diagnostic(MapWithoutExplicitStrict, call.range())
.set_fix(Fix::applicable_edit( .set_fix(Fix::applicable_edit(
add_argument( add_argument("strict=False", &call.arguments, checker.tokens()),
"strict=False",
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
),
Applicability::Unsafe, Applicability::Unsafe,
)); ));
} }

View File

@ -3,7 +3,7 @@ use std::fmt::Write;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_docstring_stmt; use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::name::QualifiedName; use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, ParameterWithDefault}; use ruff_python_ast::{self as ast, Expr, ParameterWithDefault};
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::function_type::is_stub; use ruff_python_semantic::analyze::function_type::is_stub;
@ -166,12 +166,7 @@ fn move_initialization(
return None; return None;
} }
let range = match parenthesized_range( let range = match parenthesized_range(default.into(), parameter.into(), checker.tokens()) {
default.into(),
parameter.into(),
checker.comment_ranges(),
checker.source(),
) {
Some(range) => range, Some(range) => range,
None => default.range(), None => default.range(),
}; };
@ -194,12 +189,7 @@ fn move_initialization(
"{} = {}", "{} = {}",
parameter.parameter.name(), parameter.parameter.name(),
locator.slice( locator.slice(
parenthesized_range( parenthesized_range(default.into(), parameter.into(), checker.tokens())
default.into(),
parameter.into(),
checker.comment_ranges(),
checker.source()
)
.unwrap_or(default.range()) .unwrap_or(default.range())
) )
); );

View File

@ -92,12 +92,7 @@ pub(crate) fn no_explicit_stacklevel(checker: &Checker, call: &ast::ExprCall) {
} }
let mut diagnostic = checker.report_diagnostic(NoExplicitStacklevel, call.func.range()); let mut diagnostic = checker.report_diagnostic(NoExplicitStacklevel, call.func.range());
let edit = add_argument( let edit = add_argument("stacklevel=2", &call.arguments, checker.tokens());
"stacklevel=2",
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
);
diagnostic.set_fix(Fix::unsafe_edit(edit)); diagnostic.set_fix(Fix::unsafe_edit(edit));
} }

View File

@ -70,12 +70,7 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
checker checker
.report_diagnostic(ZipWithoutExplicitStrict, call.range()) .report_diagnostic(ZipWithoutExplicitStrict, call.range())
.set_fix(Fix::applicable_edit( .set_fix(Fix::applicable_edit(
add_argument( add_argument("strict=False", &call.arguments, checker.tokens()),
"strict=False",
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
),
Applicability::Unsafe, Applicability::Unsafe,
)); ));
} }

View File

@ -236,227 +236,227 @@ help: Replace with `None`; initialize within function
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:239:20 --> B006_B008.py:242:20
| |
237 | # B006 and B008 240 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008. 241 | # We should handle arbitrary nesting of these B008.
239 | def nested_combo(a=[float(3), dt.datetime.now()]): 242 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
240 | pass 243 | pass
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
236 | 239 |
237 | # B006 and B008 240 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008. 241 | # We should handle arbitrary nesting of these B008.
- def nested_combo(a=[float(3), dt.datetime.now()]): - def nested_combo(a=[float(3), dt.datetime.now()]):
239 + def nested_combo(a=None): 242 + def nested_combo(a=None):
240 | pass 243 | pass
241 | 244 |
242 | 245 |
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:276:27 --> B006_B008.py:279:27
| |
275 | def mutable_annotations( 278 | def mutable_annotations(
276 | a: list[int] | None = [], 279 | a: list[int] | None = [],
| ^^ | ^^
277 | b: Optional[Dict[int, int]] = {}, 280 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
273 | 276 |
274 | 277 |
275 | def mutable_annotations( 278 | def mutable_annotations(
- a: list[int] | None = [], - a: list[int] | None = [],
276 + a: list[int] | None = None, 279 + a: list[int] | None = None,
277 | b: Optional[Dict[int, int]] = {}, 280 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:277:35 --> B006_B008.py:280:35
| |
275 | def mutable_annotations( 278 | def mutable_annotations(
276 | a: list[int] | None = [], 279 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {}, 280 | b: Optional[Dict[int, int]] = {},
| ^^ | ^^
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
274 | 277 |
275 | def mutable_annotations( 278 | def mutable_annotations(
276 | a: list[int] | None = [], 279 | a: list[int] | None = [],
- b: Optional[Dict[int, int]] = {}, - b: Optional[Dict[int, int]] = {},
277 + b: Optional[Dict[int, int]] = None, 280 + b: Optional[Dict[int, int]] = None,
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ): 283 | ):
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:278:62 --> B006_B008.py:281:62
| |
276 | a: list[int] | None = [], 279 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {}, 280 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ | ^^^^^
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ): 283 | ):
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
275 | def mutable_annotations( 278 | def mutable_annotations(
276 | a: list[int] | None = [], 279 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {}, 280 | b: Optional[Dict[int, int]] = {},
- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), - c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
278 + c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None, 281 + c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ): 283 | ):
281 | pass 284 | pass
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:279:80 --> B006_B008.py:282:80
| |
277 | b: Optional[Dict[int, int]] = {}, 280 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ | ^^^^^
280 | ): 283 | ):
281 | pass 284 | pass
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
276 | a: list[int] | None = [], 279 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {}, 280 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), - d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 + d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None, 282 + d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
280 | ): 283 | ):
281 | pass 284 | pass
282 | 285 |
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:284:52 --> B006_B008.py:287:52
| |
284 | def single_line_func_wrong(value: dict[str, str] = {}): 287 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ | ^^
285 | """Docstring""" 288 | """Docstring"""
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
281 | pass 284 | pass
282 | 285 |
283 |
- def single_line_func_wrong(value: dict[str, str] = {}):
284 + def single_line_func_wrong(value: dict[str, str] = None):
285 | """Docstring"""
286 | 286 |
287 | - def single_line_func_wrong(value: dict[str, str] = {}):
287 + def single_line_func_wrong(value: dict[str, str] = None):
288 | """Docstring"""
289 |
290 |
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:288:52 --> B006_B008.py:291:52
| |
288 | def single_line_func_wrong(value: dict[str, str] = {}): 291 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ | ^^
289 | """Docstring""" 292 | """Docstring"""
290 | ... 293 | ...
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
285 | """Docstring""" 288 | """Docstring"""
286 | 289 |
287 | 290 |
- def single_line_func_wrong(value: dict[str, str] = {}): - def single_line_func_wrong(value: dict[str, str] = {}):
288 + def single_line_func_wrong(value: dict[str, str] = None): 291 + def single_line_func_wrong(value: dict[str, str] = None):
289 | """Docstring""" 292 | """Docstring"""
290 | ... 293 | ...
291 | 294 |
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:293:52 --> B006_B008.py:296:52
| |
293 | def single_line_func_wrong(value: dict[str, str] = {}): 296 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ | ^^
294 | """Docstring"""; ... 297 | """Docstring"""; ...
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
290 | ... 293 | ...
291 | 294 |
292 |
- def single_line_func_wrong(value: dict[str, str] = {}):
293 + def single_line_func_wrong(value: dict[str, str] = None):
294 | """Docstring"""; ...
295 | 295 |
296 | - def single_line_func_wrong(value: dict[str, str] = {}):
296 + def single_line_func_wrong(value: dict[str, str] = None):
297 | """Docstring"""; ...
298 |
299 |
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:297:52 --> B006_B008.py:300:52
| |
297 | def single_line_func_wrong(value: dict[str, str] = {}): 300 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ | ^^
298 | """Docstring"""; \ 301 | """Docstring"""; \
299 | ... 302 | ...
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
294 | """Docstring"""; ... 297 | """Docstring"""; ...
295 | 298 |
296 | 299 |
- def single_line_func_wrong(value: dict[str, str] = {}): - def single_line_func_wrong(value: dict[str, str] = {}):
297 + def single_line_func_wrong(value: dict[str, str] = None): 300 + def single_line_func_wrong(value: dict[str, str] = None):
298 | """Docstring"""; \ 301 | """Docstring"""; \
299 | ... 302 | ...
300 | 303 |
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:302:52 --> B006_B008.py:305:52
| |
302 | def single_line_func_wrong(value: dict[str, str] = { 305 | def single_line_func_wrong(value: dict[str, str] = {
| ____________________________________________________^ | ____________________________________________________^
303 | | # This is a comment 306 | | # This is a comment
304 | | }): 307 | | }):
| |_^ | |_^
305 | """Docstring""" 308 | """Docstring"""
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
299 | ... 302 | ...
300 | 303 |
301 | 304 |
- def single_line_func_wrong(value: dict[str, str] = { - def single_line_func_wrong(value: dict[str, str] = {
- # This is a comment - # This is a comment
- }): - }):
302 + def single_line_func_wrong(value: dict[str, str] = None): 305 + def single_line_func_wrong(value: dict[str, str] = None):
303 | """Docstring""" 306 | """Docstring"""
304 | 307 |
305 | 308 |
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 Do not use mutable data structures for argument defaults B006 Do not use mutable data structures for argument defaults
--> B006_B008.py:308:52 --> B006_B008.py:311:52
| |
308 | def single_line_func_wrong(value: dict[str, str] = {}) \ 311 | def single_line_func_wrong(value: dict[str, str] = {}) \
| ^^ | ^^
309 | : \ 312 | : \
310 | """Docstring""" 313 | """Docstring"""
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:313:52 --> B006_B008.py:316:52
| |
313 | def single_line_func_wrong(value: dict[str, str] = {}): 316 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ | ^^
314 | """Docstring without newline""" 317 | """Docstring without newline"""
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
310 | """Docstring""" 313 | """Docstring"""
311 | 314 |
312 | 315 |
- def single_line_func_wrong(value: dict[str, str] = {}): - def single_line_func_wrong(value: dict[str, str] = {}):
313 + def single_line_func_wrong(value: dict[str, str] = None): 316 + def single_line_func_wrong(value: dict[str, str] = None):
314 | """Docstring without newline""" 317 | """Docstring without newline"""
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior

View File

@ -53,39 +53,39 @@ B008 Do not perform function call in argument defaults; instead, perform the cal
| |
B008 Do not perform function call `dt.datetime.now` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable B008 Do not perform function call `dt.datetime.now` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
--> B006_B008.py:239:31 --> B006_B008.py:242:31
| |
237 | # B006 and B008 240 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008. 241 | # We should handle arbitrary nesting of these B008.
239 | def nested_combo(a=[float(3), dt.datetime.now()]): 242 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^
240 | pass 243 | pass
| |
B008 Do not perform function call `map` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable B008 Do not perform function call `map` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
--> B006_B008.py:245:22 --> B006_B008.py:248:22
| |
243 | # Don't flag nested B006 since we can't guarantee that 246 | # Don't flag nested B006 since we can't guarantee that
244 | # it isn't made mutable by the outer operation. 247 | # it isn't made mutable by the outer operation.
245 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])): 248 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
246 | pass 249 | pass
| |
B008 Do not perform function call `random.randint` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable B008 Do not perform function call `random.randint` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
--> B006_B008.py:250:19 --> B006_B008.py:253:19
| |
249 | # B008-ception. 252 | # B008-ception.
250 | def nested_b008(a=random.randint(0, dt.datetime.now().year)): 253 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
251 | pass 254 | pass
| |
B008 Do not perform function call `dt.datetime.now` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable B008 Do not perform function call `dt.datetime.now` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
--> B006_B008.py:250:37 --> B006_B008.py:253:37
| |
249 | # B008-ception. 252 | # B008-ception.
250 | def nested_b008(a=random.randint(0, dt.datetime.now().year)): 253 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^
251 | pass 254 | pass
| |

View File

@ -236,227 +236,227 @@ help: Replace with `None`; initialize within function
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:239:20 --> B006_B008.py:242:20
| |
237 | # B006 and B008 240 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008. 241 | # We should handle arbitrary nesting of these B008.
239 | def nested_combo(a=[float(3), dt.datetime.now()]): 242 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
240 | pass 243 | pass
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
236 | 239 |
237 | # B006 and B008 240 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008. 241 | # We should handle arbitrary nesting of these B008.
- def nested_combo(a=[float(3), dt.datetime.now()]): - def nested_combo(a=[float(3), dt.datetime.now()]):
239 + def nested_combo(a=None): 242 + def nested_combo(a=None):
240 | pass 243 | pass
241 | 244 |
242 | 245 |
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:276:27 --> B006_B008.py:279:27
| |
275 | def mutable_annotations( 278 | def mutable_annotations(
276 | a: list[int] | None = [], 279 | a: list[int] | None = [],
| ^^ | ^^
277 | b: Optional[Dict[int, int]] = {}, 280 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
273 | 276 |
274 | 277 |
275 | def mutable_annotations( 278 | def mutable_annotations(
- a: list[int] | None = [], - a: list[int] | None = [],
276 + a: list[int] | None = None, 279 + a: list[int] | None = None,
277 | b: Optional[Dict[int, int]] = {}, 280 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:277:35 --> B006_B008.py:280:35
| |
275 | def mutable_annotations( 278 | def mutable_annotations(
276 | a: list[int] | None = [], 279 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {}, 280 | b: Optional[Dict[int, int]] = {},
| ^^ | ^^
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
274 | 277 |
275 | def mutable_annotations( 278 | def mutable_annotations(
276 | a: list[int] | None = [], 279 | a: list[int] | None = [],
- b: Optional[Dict[int, int]] = {}, - b: Optional[Dict[int, int]] = {},
277 + b: Optional[Dict[int, int]] = None, 280 + b: Optional[Dict[int, int]] = None,
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ): 283 | ):
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:278:62 --> B006_B008.py:281:62
| |
276 | a: list[int] | None = [], 279 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {}, 280 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ | ^^^^^
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ): 283 | ):
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
275 | def mutable_annotations( 278 | def mutable_annotations(
276 | a: list[int] | None = [], 279 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {}, 280 | b: Optional[Dict[int, int]] = {},
- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), - c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
278 + c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None, 281 + c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ): 283 | ):
281 | pass 284 | pass
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:279:80 --> B006_B008.py:282:80
| |
277 | b: Optional[Dict[int, int]] = {}, 280 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ | ^^^^^
280 | ): 283 | ):
281 | pass 284 | pass
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
276 | a: list[int] | None = [], 279 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {}, 280 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), 281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(), - d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 + d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None, 282 + d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
280 | ): 283 | ):
281 | pass 284 | pass
282 | 285 |
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:284:52 --> B006_B008.py:287:52
| |
284 | def single_line_func_wrong(value: dict[str, str] = {}): 287 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ | ^^
285 | """Docstring""" 288 | """Docstring"""
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
281 | pass 284 | pass
282 | 285 |
283 |
- def single_line_func_wrong(value: dict[str, str] = {}):
284 + def single_line_func_wrong(value: dict[str, str] = None):
285 | """Docstring"""
286 | 286 |
287 | - def single_line_func_wrong(value: dict[str, str] = {}):
287 + def single_line_func_wrong(value: dict[str, str] = None):
288 | """Docstring"""
289 |
290 |
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:288:52 --> B006_B008.py:291:52
| |
288 | def single_line_func_wrong(value: dict[str, str] = {}): 291 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ | ^^
289 | """Docstring""" 292 | """Docstring"""
290 | ... 293 | ...
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
285 | """Docstring""" 288 | """Docstring"""
286 | 289 |
287 | 290 |
- def single_line_func_wrong(value: dict[str, str] = {}): - def single_line_func_wrong(value: dict[str, str] = {}):
288 + def single_line_func_wrong(value: dict[str, str] = None): 291 + def single_line_func_wrong(value: dict[str, str] = None):
289 | """Docstring""" 292 | """Docstring"""
290 | ... 293 | ...
291 | 294 |
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:293:52 --> B006_B008.py:296:52
| |
293 | def single_line_func_wrong(value: dict[str, str] = {}): 296 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ | ^^
294 | """Docstring"""; ... 297 | """Docstring"""; ...
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
290 | ... 293 | ...
291 | 294 |
292 |
- def single_line_func_wrong(value: dict[str, str] = {}):
293 + def single_line_func_wrong(value: dict[str, str] = None):
294 | """Docstring"""; ...
295 | 295 |
296 | - def single_line_func_wrong(value: dict[str, str] = {}):
296 + def single_line_func_wrong(value: dict[str, str] = None):
297 | """Docstring"""; ...
298 |
299 |
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:297:52 --> B006_B008.py:300:52
| |
297 | def single_line_func_wrong(value: dict[str, str] = {}): 300 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ | ^^
298 | """Docstring"""; \ 301 | """Docstring"""; \
299 | ... 302 | ...
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
294 | """Docstring"""; ... 297 | """Docstring"""; ...
295 | 298 |
296 | 299 |
- def single_line_func_wrong(value: dict[str, str] = {}): - def single_line_func_wrong(value: dict[str, str] = {}):
297 + def single_line_func_wrong(value: dict[str, str] = None): 300 + def single_line_func_wrong(value: dict[str, str] = None):
298 | """Docstring"""; \ 301 | """Docstring"""; \
299 | ... 302 | ...
300 | 303 |
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:302:52 --> B006_B008.py:305:52
| |
302 | def single_line_func_wrong(value: dict[str, str] = { 305 | def single_line_func_wrong(value: dict[str, str] = {
| ____________________________________________________^ | ____________________________________________________^
303 | | # This is a comment 306 | | # This is a comment
304 | | }): 307 | | }):
| |_^ | |_^
305 | """Docstring""" 308 | """Docstring"""
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
299 | ... 302 | ...
300 | 303 |
301 | 304 |
- def single_line_func_wrong(value: dict[str, str] = { - def single_line_func_wrong(value: dict[str, str] = {
- # This is a comment - # This is a comment
- }): - }):
302 + def single_line_func_wrong(value: dict[str, str] = None): 305 + def single_line_func_wrong(value: dict[str, str] = None):
303 | """Docstring""" 306 | """Docstring"""
304 | 307 |
305 | 308 |
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
B006 Do not use mutable data structures for argument defaults B006 Do not use mutable data structures for argument defaults
--> B006_B008.py:308:52 --> B006_B008.py:311:52
| |
308 | def single_line_func_wrong(value: dict[str, str] = {}) \ 311 | def single_line_func_wrong(value: dict[str, str] = {}) \
| ^^ | ^^
309 | : \ 312 | : \
310 | """Docstring""" 313 | """Docstring"""
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
B006 [*] Do not use mutable data structures for argument defaults B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:313:52 --> B006_B008.py:316:52
| |
313 | def single_line_func_wrong(value: dict[str, str] = {}): 316 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ | ^^
314 | """Docstring without newline""" 317 | """Docstring without newline"""
| |
help: Replace with `None`; initialize within function help: Replace with `None`; initialize within function
310 | """Docstring""" 313 | """Docstring"""
311 | 314 |
312 | 315 |
- def single_line_func_wrong(value: dict[str, str] = {}): - def single_line_func_wrong(value: dict[str, str] = {}):
313 + def single_line_func_wrong(value: dict[str, str] = None): 316 + def single_line_func_wrong(value: dict[str, str] = None):
314 | """Docstring without newline""" 317 | """Docstring without newline"""
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior

View File

@ -2,8 +2,8 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::ExprGenerator; use ruff_python_ast::ExprGenerator;
use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::TokenKind; use ruff_python_ast::token::TokenKind;
use ruff_python_ast::token::parenthesized_range;
use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -142,12 +142,8 @@ pub(crate) fn unnecessary_generator_list(checker: &Checker, call: &ast::ExprCall
if *parenthesized { if *parenthesized {
// The generator's range will include the innermost parentheses, but it could be // The generator's range will include the innermost parentheses, but it could be
// surrounded by additional parentheses. // surrounded by additional parentheses.
let range = parenthesized_range( let range =
argument.into(), parenthesized_range(argument.into(), (&call.arguments).into(), checker.tokens())
(&call.arguments).into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(argument.range()); .unwrap_or(argument.range());
// The generator always parenthesizes the expression; trim the parentheses. // The generator always parenthesizes the expression; trim the parentheses.

View File

@ -2,8 +2,8 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::ExprGenerator; use ruff_python_ast::ExprGenerator;
use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::TokenKind; use ruff_python_ast::token::TokenKind;
use ruff_python_ast::token::parenthesized_range;
use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -147,12 +147,8 @@ pub(crate) fn unnecessary_generator_set(checker: &Checker, call: &ast::ExprCall)
if *parenthesized { if *parenthesized {
// The generator's range will include the innermost parentheses, but it could be // The generator's range will include the innermost parentheses, but it could be
// surrounded by additional parentheses. // surrounded by additional parentheses.
let range = parenthesized_range( let range =
argument.into(), parenthesized_range(argument.into(), (&call.arguments).into(), checker.tokens())
(&call.arguments).into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(argument.range()); .unwrap_or(argument.range());
// The generator always parenthesizes the expression; trim the parentheses. // The generator always parenthesizes the expression; trim the parentheses.

View File

@ -1,7 +1,7 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::token::TokenKind; use ruff_python_ast::token::TokenKind;
use ruff_python_ast::token::parenthesized_range;
use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -89,12 +89,8 @@ pub(crate) fn unnecessary_list_comprehension_set(checker: &Checker, call: &ast::
// If the list comprehension is parenthesized, remove the parentheses in addition to // If the list comprehension is parenthesized, remove the parentheses in addition to
// removing the brackets. // removing the brackets.
let replacement_range = parenthesized_range( let replacement_range =
argument.into(), parenthesized_range(argument.into(), (&call.arguments).into(), checker.tokens())
(&call.arguments).into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or_else(|| argument.range()); .unwrap_or_else(|| argument.range());
let span = argument.range().add_start(one).sub_end(one); let span = argument.range().add_start(one).sub_end(one);

View File

@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, Operator}; use ruff_python_ast::{self as ast, Expr, Operator};
use ruff_python_trivia::is_python_whitespace; use ruff_python_trivia::is_python_whitespace;
use ruff_source_file::LineRanges; use ruff_source_file::LineRanges;
@ -88,13 +88,7 @@ pub(crate) fn explicit(checker: &Checker, expr: &Expr) {
checker.report_diagnostic(ExplicitStringConcatenation, expr.range()); checker.report_diagnostic(ExplicitStringConcatenation, expr.range());
let is_parenthesized = |expr: &Expr| { let is_parenthesized = |expr: &Expr| {
parenthesized_range( parenthesized_range(expr.into(), bin_op.into(), checker.tokens()).is_some()
expr.into(),
bin_op.into(),
checker.comment_ranges(),
checker.source(),
)
.is_some()
}; };
// If either `left` or `right` is parenthesized, generating // If either `left` or `right` is parenthesized, generating
// a fix would be too involved. Just report the diagnostic. // a fix would be too involved. Just report the diagnostic.

View File

@ -111,7 +111,6 @@ pub(crate) fn exc_info_outside_except_handler(checker: &Checker, call: &ExprCall
} }
let arguments = &call.arguments; let arguments = &call.arguments;
let source = checker.source();
let mut diagnostic = checker.report_diagnostic(ExcInfoOutsideExceptHandler, exc_info.range); let mut diagnostic = checker.report_diagnostic(ExcInfoOutsideExceptHandler, exc_info.range);
@ -120,8 +119,8 @@ pub(crate) fn exc_info_outside_except_handler(checker: &Checker, call: &ExprCall
exc_info, exc_info,
arguments, arguments,
Parentheses::Preserve, Parentheses::Preserve,
source, checker.source(),
checker.comment_ranges(), checker.tokens(),
)?; )?;
Ok(Fix::unsafe_edit(edit)) Ok(Fix::unsafe_edit(edit))
}); });

View File

@ -2,7 +2,7 @@ use itertools::Itertools;
use rustc_hash::{FxBuildHasher, FxHashSet}; use rustc_hash::{FxBuildHasher, FxHashSet};
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr}; use ruff_python_ast::{self as ast, Expr};
use ruff_python_stdlib::identifiers::is_identifier; use ruff_python_stdlib::identifiers::is_identifier;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -129,8 +129,8 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) {
keyword, keyword,
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.source(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::safe_edit) .map(Fix::safe_edit)
}); });
@ -158,8 +158,7 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) {
parenthesized_range( parenthesized_range(
value.into(), value.into(),
dict.into(), dict.into(),
checker.comment_ranges(), checker.tokens()
checker.locator().contents(),
) )
.unwrap_or(value.range()) .unwrap_or(value.range())
) )

View File

@ -73,11 +73,11 @@ pub(crate) fn unnecessary_range_start(checker: &Checker, call: &ast::ExprCall) {
let mut diagnostic = checker.report_diagnostic(UnnecessaryRangeStart, start.range()); let mut diagnostic = checker.report_diagnostic(UnnecessaryRangeStart, start.range());
diagnostic.try_set_fix(|| { diagnostic.try_set_fix(|| {
remove_argument( remove_argument(
&start, start,
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.source(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::safe_edit) .map(Fix::safe_edit)
}); });

View File

@ -160,20 +160,16 @@ fn generate_fix(
) -> anyhow::Result<Fix> { ) -> anyhow::Result<Fix> {
let locator = checker.locator(); let locator = checker.locator();
let source = locator.contents(); let source = locator.contents();
let tokens = checker.tokens();
let deletion = remove_argument( let deletion = remove_argument(
generic_base, generic_base,
arguments, arguments,
Parentheses::Preserve, Parentheses::Preserve,
source, source,
checker.comment_ranges(), tokens,
)?; )?;
let insertion = add_argument( let insertion = add_argument(locator.slice(generic_base), arguments, tokens);
locator.slice(generic_base),
arguments,
checker.comment_ranges(),
source,
);
Ok(Fix::unsafe_edits(deletion, [insertion])) Ok(Fix::unsafe_edits(deletion, [insertion]))
} }

View File

@ -5,7 +5,7 @@ use ruff_python_ast::{
helpers::{pep_604_union, typing_optional}, helpers::{pep_604_union, typing_optional},
name::Name, name::Name,
operator_precedence::OperatorPrecedence, operator_precedence::OperatorPrecedence,
parenthesize::parenthesized_range, token::{Tokens, parenthesized_range},
}; };
use ruff_python_semantic::analyze::typing::{traverse_literal, traverse_union}; use ruff_python_semantic::analyze::typing::{traverse_literal, traverse_union};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -243,12 +243,8 @@ fn create_fix(
let union_expr = pep_604_union(&[new_literal_expr, none_expr]); let union_expr = pep_604_union(&[new_literal_expr, none_expr]);
// Check if we need parentheses to preserve operator precedence // Check if we need parentheses to preserve operator precedence
let content = if needs_parentheses_for_precedence( let content =
semantic, if needs_parentheses_for_precedence(semantic, literal_expr, checker.tokens()) {
literal_expr,
checker.comment_ranges(),
checker.source(),
) {
format!("({})", checker.generator().expr(&union_expr)) format!("({})", checker.generator().expr(&union_expr))
} else { } else {
checker.generator().expr(&union_expr) checker.generator().expr(&union_expr)
@ -278,8 +274,7 @@ enum UnionKind {
fn needs_parentheses_for_precedence( fn needs_parentheses_for_precedence(
semantic: &ruff_python_semantic::SemanticModel, semantic: &ruff_python_semantic::SemanticModel,
literal_expr: &Expr, literal_expr: &Expr,
comment_ranges: &ruff_python_trivia::CommentRanges, tokens: &Tokens,
source: &str,
) -> bool { ) -> bool {
// Get the parent expression to check if we're in a context that needs parentheses // Get the parent expression to check if we're in a context that needs parentheses
let Some(parent_expr) = semantic.current_expression_parent() else { let Some(parent_expr) = semantic.current_expression_parent() else {
@ -287,14 +282,7 @@ fn needs_parentheses_for_precedence(
}; };
// Check if the literal expression is already parenthesized // Check if the literal expression is already parenthesized
if parenthesized_range( if parenthesized_range(literal_expr.into(), parent_expr.into(), tokens).is_some() {
literal_expr.into(),
parent_expr.into(),
comment_ranges,
source,
)
.is_some()
{
return false; // Already parenthesized, don't add more return false; // Already parenthesized, don't add more
} }

View File

@ -10,7 +10,7 @@ use libcst_native::{
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::Truthiness; use ruff_python_ast::helpers::Truthiness;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::visitor::Visitor; use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{ use ruff_python_ast::{
self as ast, AnyNodeRef, Arguments, BoolOp, ExceptHandler, Expr, Keyword, Stmt, UnaryOp, self as ast, AnyNodeRef, Arguments, BoolOp, ExceptHandler, Expr, Keyword, Stmt, UnaryOp,
@ -303,8 +303,7 @@ pub(crate) fn unittest_assertion(
parenthesized_range( parenthesized_range(
expr.into(), expr.into(),
checker.semantic().current_statement().into(), checker.semantic().current_statement().into(),
checker.comment_ranges(), checker.tokens(),
checker.locator().contents(),
) )
.unwrap_or(expr.range()), .unwrap_or(expr.range()),
))); )));

View File

@ -768,8 +768,8 @@ fn check_fixture_decorator(checker: &Checker, func_name: &str, decorator: &Decor
keyword, keyword,
arguments, arguments,
edits::Parentheses::Preserve, edits::Parentheses::Preserve,
checker.locator().contents(), checker.source(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::unsafe_edit) .map(Fix::unsafe_edit)
}); });

View File

@ -2,10 +2,9 @@ use rustc_hash::{FxBuildHasher, FxHashMap};
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::{Tokens, parenthesized_range};
use ruff_python_ast::{self as ast, Expr, ExprCall, ExprContext, StringLiteralFlags}; use ruff_python_ast::{self as ast, Expr, ExprCall, ExprContext, StringLiteralFlags};
use ruff_python_codegen::Generator; use ruff_python_codegen::Generator;
use ruff_python_trivia::CommentRanges;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_text_size::{Ranged, TextRange, TextSize};
@ -322,18 +321,8 @@ fn elts_to_csv(elts: &[Expr], generator: Generator, flags: StringLiteralFlags) -
/// ``` /// ```
/// ///
/// This method assumes that the first argument is a string. /// This method assumes that the first argument is a string.
fn get_parametrize_name_range( fn get_parametrize_name_range(call: &ExprCall, expr: &Expr, tokens: &Tokens) -> Option<TextRange> {
call: &ExprCall, parenthesized_range(expr.into(), (&call.arguments).into(), tokens)
expr: &Expr,
comment_ranges: &CommentRanges,
source: &str,
) -> Option<TextRange> {
parenthesized_range(
expr.into(),
(&call.arguments).into(),
comment_ranges,
source,
)
} }
/// PT006 /// PT006
@ -349,12 +338,7 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr
if names.len() > 1 { if names.len() > 1 {
match names_type { match names_type {
types::ParametrizeNameType::Tuple => { types::ParametrizeNameType::Tuple => {
let name_range = get_parametrize_name_range( let name_range = get_parametrize_name_range(call, expr, checker.tokens())
call,
expr,
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(expr.range()); .unwrap_or(expr.range());
let mut diagnostic = checker.report_diagnostic( let mut diagnostic = checker.report_diagnostic(
PytestParametrizeNamesWrongType { PytestParametrizeNamesWrongType {
@ -386,12 +370,7 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr
))); )));
} }
types::ParametrizeNameType::List => { types::ParametrizeNameType::List => {
let name_range = get_parametrize_name_range( let name_range = get_parametrize_name_range(call, expr, checker.tokens())
call,
expr,
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(expr.range()); .unwrap_or(expr.range());
let mut diagnostic = checker.report_diagnostic( let mut diagnostic = checker.report_diagnostic(
PytestParametrizeNamesWrongType { PytestParametrizeNamesWrongType {

View File

@ -10,7 +10,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::helpers::{Truthiness, contains_effect}; use ruff_python_ast::helpers::{Truthiness, contains_effect};
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_codegen::Generator; use ruff_python_codegen::Generator;
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
@ -800,12 +800,7 @@ fn is_short_circuit(
edit = Some(get_short_circuit_edit( edit = Some(get_short_circuit_edit(
value, value,
TextRange::new( TextRange::new(
parenthesized_range( parenthesized_range(furthest.into(), expr.into(), checker.tokens())
furthest.into(),
expr.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(furthest.range()) .unwrap_or(furthest.range())
.start(), .start(),
expr.end(), expr.end(),
@ -828,12 +823,7 @@ fn is_short_circuit(
edit = Some(get_short_circuit_edit( edit = Some(get_short_circuit_edit(
next_value, next_value,
TextRange::new( TextRange::new(
parenthesized_range( parenthesized_range(furthest.into(), expr.into(), checker.tokens())
furthest.into(),
expr.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(furthest.range()) .unwrap_or(furthest.range())
.start(), .start(),
expr.end(), expr.end(),

View File

@ -4,7 +4,7 @@ use ruff_text_size::{Ranged, TextRange};
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::{is_const_false, is_const_true}; use ruff_python_ast::helpers::{is_const_false, is_const_true};
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation};
@ -171,12 +171,7 @@ pub(crate) fn if_expr_with_true_false(
checker checker
.locator() .locator()
.slice( .slice(
parenthesized_range( parenthesized_range(test.into(), expr.into(), checker.tokens())
test.into(),
expr.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(test.range()), .unwrap_or(test.range()),
) )
.to_string(), .to_string(),

View File

@ -4,10 +4,10 @@ use anyhow::Result;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::ComparableStmt; use ruff_python_ast::comparable::ComparableStmt;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::stmt_if::{IfElifBranch, if_elif_branches}; use ruff_python_ast::stmt_if::{IfElifBranch, if_elif_branches};
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr}; use ruff_python_ast::{self as ast, Expr};
use ruff_python_trivia::{CommentRanges, SimpleTokenKind, SimpleTokenizer}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::LineRanges; use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -99,7 +99,7 @@ pub(crate) fn if_with_same_arms(checker: &Checker, stmt_if: &ast::StmtIf) {
&current_branch, &current_branch,
following_branch, following_branch,
checker.locator(), checker.locator(),
checker.comment_ranges(), checker.tokens(),
) )
}); });
} }
@ -111,7 +111,7 @@ fn merge_branches(
current_branch: &IfElifBranch, current_branch: &IfElifBranch,
following_branch: &IfElifBranch, following_branch: &IfElifBranch,
locator: &Locator, locator: &Locator,
comment_ranges: &CommentRanges, tokens: &ruff_python_ast::token::Tokens,
) -> Result<Fix> { ) -> Result<Fix> {
// Identify the colon (`:`) at the end of the current branch's test. // Identify the colon (`:`) at the end of the current branch's test.
let Some(current_branch_colon) = let Some(current_branch_colon) =
@ -127,12 +127,9 @@ fn merge_branches(
); );
// If the following test isn't parenthesized, consider parenthesizing it. // If the following test isn't parenthesized, consider parenthesizing it.
let following_branch_test = if let Some(range) = parenthesized_range( let following_branch_test = if let Some(range) =
following_branch.test.into(), parenthesized_range(following_branch.test.into(), stmt_if.into(), tokens)
stmt_if.into(), {
comment_ranges,
locator.contents(),
) {
Cow::Borrowed(locator.slice(range)) Cow::Borrowed(locator.slice(range))
} else if matches!( } else if matches!(
following_branch.test, following_branch.test,
@ -153,16 +150,11 @@ fn merge_branches(
// //
// For example, if the current test is `x if x else y`, we should parenthesize it to // For example, if the current test is `x if x else y`, we should parenthesize it to
// `(x if x else y) or ...`. // `(x if x else y) or ...`.
let parenthesize_edit = if matches!( let parenthesize_edit =
if matches!(
current_branch.test, current_branch.test,
Expr::Lambda(_) | Expr::Named(_) | Expr::If(_) Expr::Lambda(_) | Expr::Named(_) | Expr::If(_)
) && parenthesized_range( ) && parenthesized_range(current_branch.test.into(), stmt_if.into(), tokens).is_none()
current_branch.test.into(),
stmt_if.into(),
comment_ranges,
locator.contents(),
)
.is_none()
{ {
Some(Edit::range_replacement( Some(Edit::range_replacement(
format!("({})", locator.slice(current_branch.test)), format!("({})", locator.slice(current_branch.test)),

View File

@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::AnyNodeRef; use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Arguments, CmpOp, Comprehension, Expr}; use ruff_python_ast::{self as ast, Arguments, CmpOp, Comprehension, Expr};
use ruff_python_semantic::analyze::typing; use ruff_python_semantic::analyze::typing;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
@ -90,20 +90,10 @@ fn key_in_dict(checker: &Checker, left: &Expr, right: &Expr, operator: CmpOp, pa
} }
// Extract the exact range of the left and right expressions. // Extract the exact range of the left and right expressions.
let left_range = parenthesized_range( let left_range =
left.into(), parenthesized_range(left.into(), parent, checker.tokens()).unwrap_or(left.range());
parent, let right_range =
checker.comment_ranges(), parenthesized_range(right.into(), parent, checker.tokens()).unwrap_or(right.range());
checker.locator().contents(),
)
.unwrap_or(left.range());
let right_range = parenthesized_range(
right.into(),
parent,
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(right.range());
let mut diagnostic = checker.report_diagnostic( let mut diagnostic = checker.report_diagnostic(
InDictKeys { InDictKeys {

View File

@ -11,7 +11,7 @@ use crate::registry::Rule;
use crate::rules::flake8_type_checking::helpers::quote_type_expression; use crate::rules::flake8_type_checking::helpers::quote_type_expression;
use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation};
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
/// ## What it does /// ## What it does
/// Checks if [PEP 613] explicit type aliases contain references to /// Checks if [PEP 613] explicit type aliases contain references to
@ -295,21 +295,20 @@ pub(crate) fn quoted_type_alias(
let range = annotation_expr.range(); let range = annotation_expr.range();
let mut diagnostic = checker.report_diagnostic(QuotedTypeAlias, range); let mut diagnostic = checker.report_diagnostic(QuotedTypeAlias, range);
let fix_string = annotation_expr.value.to_string(); let fix_string = annotation_expr.value.to_string();
let fix_string = if (fix_string.contains('\n') || fix_string.contains('\r')) let fix_string = if (fix_string.contains('\n') || fix_string.contains('\r'))
&& parenthesized_range( && parenthesized_range(
// Check for parenthesis outside string ("""...""") // Check for parentheses outside the string ("""...""")
annotation_expr.into(), annotation_expr.into(),
checker.semantic().current_statement().into(), checker.semantic().current_statement().into(),
checker.comment_ranges(), checker.source_tokens(),
checker.locator().contents(),
) )
.is_none() .is_none()
&& parenthesized_range( && parenthesized_range(
// Check for parenthesis inside string """(...)""" // Check for parentheses inside the string """(...)"""
expr.into(), expr.into(),
annotation_expr.into(), annotation_expr.into(),
checker.comment_ranges(), checker.tokens(),
checker.locator().contents(),
) )
.is_none() .is_none()
{ {

View File

@ -1,10 +1,9 @@
use std::ops::Range; use std::ops::Range;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{Expr, ExprBinOp, ExprCall, Operator}; use ruff_python_ast::{Expr, ExprBinOp, ExprCall, Operator};
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
use ruff_python_trivia::CommentRanges;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -89,11 +88,7 @@ pub(crate) fn path_constructor_current_directory(
let mut diagnostic = checker.report_diagnostic(PathConstructorCurrentDirectory, arg.range()); let mut diagnostic = checker.report_diagnostic(PathConstructorCurrentDirectory, arg.range());
match parent_and_next_path_fragment_range( match parent_and_next_path_fragment_range(checker.semantic(), checker.tokens()) {
checker.semantic(),
checker.comment_ranges(),
checker.source(),
) {
Some((parent_range, next_fragment_range)) => { Some((parent_range, next_fragment_range)) => {
let next_fragment_expr = checker.locator().slice(next_fragment_range); let next_fragment_expr = checker.locator().slice(next_fragment_range);
let call_expr = checker.locator().slice(call.range()); let call_expr = checker.locator().slice(call.range());
@ -116,7 +111,7 @@ pub(crate) fn path_constructor_current_directory(
arguments, arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.source(), checker.source(),
checker.comment_ranges(), checker.tokens(),
)?; )?;
Ok(Fix::applicable_edit(edit, applicability(call.range()))) Ok(Fix::applicable_edit(edit, applicability(call.range())))
}), }),
@ -125,8 +120,7 @@ pub(crate) fn path_constructor_current_directory(
fn parent_and_next_path_fragment_range( fn parent_and_next_path_fragment_range(
semantic: &SemanticModel, semantic: &SemanticModel,
comment_ranges: &CommentRanges, tokens: &ruff_python_ast::token::Tokens,
source: &str,
) -> Option<(TextRange, TextRange)> { ) -> Option<(TextRange, TextRange)> {
let parent = semantic.current_expression_parent()?; let parent = semantic.current_expression_parent()?;
@ -142,6 +136,6 @@ fn parent_and_next_path_fragment_range(
Some(( Some((
parent.range(), parent.range(),
parenthesized_range(right.into(), parent.into(), comment_ranges, source).unwrap_or(range), parenthesized_range(right.into(), parent.into(), tokens).unwrap_or(range),
)) ))
} }

View File

@ -1,8 +1,7 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_const_true; use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::{Tokens, parenthesized_range};
use ruff_python_ast::{self as ast, Keyword, Stmt}; use ruff_python_ast::{self as ast, Keyword, Stmt};
use ruff_python_trivia::CommentRanges;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::Locator; use crate::Locator;
@ -91,7 +90,7 @@ pub(crate) fn inplace_argument(checker: &Checker, call: &ast::ExprCall) {
call, call,
keyword, keyword,
statement, statement,
checker.comment_ranges(), checker.tokens(),
checker.locator(), checker.locator(),
) { ) {
diagnostic.set_fix(fix); diagnostic.set_fix(fix);
@ -111,19 +110,14 @@ fn convert_inplace_argument_to_assignment(
call: &ast::ExprCall, call: &ast::ExprCall,
keyword: &Keyword, keyword: &Keyword,
statement: &Stmt, statement: &Stmt,
comment_ranges: &CommentRanges, tokens: &Tokens,
locator: &Locator, locator: &Locator,
) -> Option<Fix> { ) -> Option<Fix> {
// Add the assignment. // Add the assignment.
let attr = call.func.as_attribute_expr()?; let attr = call.func.as_attribute_expr()?;
let insert_assignment = Edit::insertion( let insert_assignment = Edit::insertion(
format!("{name} = ", name = locator.slice(attr.value.range())), format!("{name} = ", name = locator.slice(attr.value.range())),
parenthesized_range( parenthesized_range(call.into(), statement.into(), tokens)
call.into(),
statement.into(),
comment_ranges,
locator.contents(),
)
.unwrap_or(call.range()) .unwrap_or(call.range())
.start(), .start(),
); );
@ -134,7 +128,7 @@ fn convert_inplace_argument_to_assignment(
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
locator.contents(), locator.contents(),
comment_ranges, tokens,
) )
.ok()?; .ok()?;

View File

@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{ use ruff_python_ast::{
self as ast, Expr, ExprEllipsisLiteral, ExprLambda, Identifier, Parameter, self as ast, Expr, ExprEllipsisLiteral, ExprLambda, Identifier, Parameter,
ParameterWithDefault, Parameters, Stmt, ParameterWithDefault, Parameters, Stmt,
@ -265,25 +265,15 @@ fn replace_trailing_ellipsis_with_original_expr(
stmt: &Stmt, stmt: &Stmt,
checker: &Checker, checker: &Checker,
) -> String { ) -> String {
let original_expr_range = parenthesized_range( let original_expr_range =
(&lambda.body).into(), parenthesized_range((&lambda.body).into(), lambda.into(), checker.tokens())
lambda.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(lambda.body.range()); .unwrap_or(lambda.body.range());
// This prevents the autofix of introducing a syntax error if the lambda's body is an // This prevents the autofix of introducing a syntax error if the lambda's body is an
// expression spanned across multiple lines. To avoid the syntax error we preserve // expression spanned across multiple lines. To avoid the syntax error we preserve
// the parenthesis around the body. // the parenthesis around the body.
let original_expr_in_source = if parenthesized_range( let original_expr_in_source =
lambda.into(), if parenthesized_range(lambda.into(), stmt.into(), checker.tokens()).is_some() {
stmt.into(),
checker.comment_ranges(),
checker.source(),
)
.is_some()
{
format!("({})", checker.locator().slice(original_expr_range)) format!("({})", checker.locator().slice(original_expr_range))
} else { } else {
checker.locator().slice(original_expr_range).to_string() checker.locator().slice(original_expr_range).to_string()

View File

@ -1,4 +1,4 @@
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::{Tokens, parenthesized_range};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
@ -179,14 +179,13 @@ fn is_redundant_boolean_comparison(op: CmpOp, comparator: &Expr) -> Option<bool>
fn generate_redundant_comparison( fn generate_redundant_comparison(
compare: &ast::ExprCompare, compare: &ast::ExprCompare,
comment_ranges: &ruff_python_trivia::CommentRanges, tokens: &Tokens,
source: &str, source: &str,
comparator: &Expr, comparator: &Expr,
kind: bool, kind: bool,
needs_wrap: bool, needs_wrap: bool,
) -> String { ) -> String {
let comparator_range = let comparator_range = parenthesized_range(comparator.into(), compare.into(), tokens)
parenthesized_range(comparator.into(), compare.into(), comment_ranges, source)
.unwrap_or(comparator.range()); .unwrap_or(comparator.range());
let comparator_str = &source[comparator_range]; let comparator_str = &source[comparator_range];
@ -379,7 +378,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
.copied() .copied()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let comment_ranges = checker.comment_ranges(); let tokens = checker.tokens();
let source = checker.source(); let source = checker.source();
let content = match (&*compare.ops, &*compare.comparators) { let content = match (&*compare.ops, &*compare.comparators) {
@ -387,18 +386,13 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
if let Some(kind) = is_redundant_boolean_comparison(*op, &compare.left) { if let Some(kind) = is_redundant_boolean_comparison(*op, &compare.left) {
let needs_wrap = compare.left.range().start() != compare.range().start(); let needs_wrap = compare.left.range().start() != compare.range().start();
generate_redundant_comparison( generate_redundant_comparison(
compare, compare, tokens, source, comparator, kind, needs_wrap,
comment_ranges,
source,
comparator,
kind,
needs_wrap,
) )
} else if let Some(kind) = is_redundant_boolean_comparison(*op, comparator) { } else if let Some(kind) = is_redundant_boolean_comparison(*op, comparator) {
let needs_wrap = comparator.range().end() != compare.range().end(); let needs_wrap = comparator.range().end() != compare.range().end();
generate_redundant_comparison( generate_redundant_comparison(
compare, compare,
comment_ranges, tokens,
source, source,
&compare.left, &compare.left,
kind, kind,
@ -410,7 +404,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
&ops, &ops,
&compare.comparators, &compare.comparators,
compare.into(), compare.into(),
comment_ranges, tokens,
source, source,
) )
} }
@ -420,7 +414,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
&ops, &ops,
&compare.comparators, &compare.comparators,
compare.into(), compare.into(),
comment_ranges, tokens,
source, source,
), ),
}; };

View File

@ -107,7 +107,7 @@ pub(crate) fn not_tests(checker: &Checker, unary_op: &ast::ExprUnaryOp) {
&[CmpOp::NotIn], &[CmpOp::NotIn],
comparators, comparators,
unary_op.into(), unary_op.into(),
checker.comment_ranges(), checker.tokens(),
checker.source(), checker.source(),
), ),
unary_op.range(), unary_op.range(),
@ -127,7 +127,7 @@ pub(crate) fn not_tests(checker: &Checker, unary_op: &ast::ExprUnaryOp) {
&[CmpOp::IsNot], &[CmpOp::IsNot],
comparators, comparators,
unary_op.into(), unary_op.into(),
checker.comment_ranges(), checker.tokens(),
checker.source(), checker.source(),
), ),
unary_op.range(), unary_op.range(),

View File

@ -4,7 +4,9 @@ use rustc_hash::FxHashSet;
use std::sync::LazyLock; use std::sync::LazyLock;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::Parameter;
use ruff_python_ast::docstrings::{clean_space, leading_space}; use ruff_python_ast::docstrings::{clean_space, leading_space};
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::identifier::Identifier; use ruff_python_ast::identifier::Identifier;
use ruff_python_semantic::analyze::visibility::is_staticmethod; use ruff_python_semantic::analyze::visibility::is_staticmethod;
use ruff_python_trivia::textwrap::dedent; use ruff_python_trivia::textwrap::dedent;
@ -1184,6 +1186,9 @@ impl AlwaysFixableViolation for MissingSectionNameColon {
/// This rule is enabled when using the `google` convention, and disabled when /// This rule is enabled when using the `google` convention, and disabled when
/// using the `pep257` and `numpy` conventions. /// using the `pep257` and `numpy` conventions.
/// ///
/// Parameters annotated with `typing.Unpack` are exempt from this rule.
/// This follows the Python typing specification for unpacking keyword arguments.
///
/// ## Example /// ## Example
/// ```python /// ```python
/// def calculate_speed(distance: float, time: float) -> float: /// def calculate_speed(distance: float, time: float) -> float:
@ -1233,6 +1238,7 @@ impl AlwaysFixableViolation for MissingSectionNameColon {
/// - [PEP 257 Docstring Conventions](https://peps.python.org/pep-0257/) /// - [PEP 257 Docstring Conventions](https://peps.python.org/pep-0257/)
/// - [PEP 287 reStructuredText Docstring Format](https://peps.python.org/pep-0287/) /// - [PEP 287 reStructuredText Docstring Format](https://peps.python.org/pep-0287/)
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
/// - [Python - Unpack for keyword arguments](https://typing.python.org/en/latest/spec/callables.html#unpack-kwargs)
#[derive(ViolationMetadata)] #[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.73")] #[violation_metadata(stable_since = "v0.0.73")]
pub(crate) struct UndocumentedParam { pub(crate) struct UndocumentedParam {
@ -1808,7 +1814,9 @@ fn missing_args(checker: &Checker, docstring: &Docstring, docstrings_args: &FxHa
missing_arg_names.insert(starred_arg_name); missing_arg_names.insert(starred_arg_name);
} }
} }
if let Some(arg) = function.parameters.kwarg.as_ref() { if let Some(arg) = function.parameters.kwarg.as_ref()
&& !has_unpack_annotation(checker, arg)
{
let arg_name = arg.name.as_str(); let arg_name = arg.name.as_str();
let starred_arg_name = format!("**{arg_name}"); let starred_arg_name = format!("**{arg_name}");
if !arg_name.starts_with('_') if !arg_name.starts_with('_')
@ -1834,6 +1842,15 @@ fn missing_args(checker: &Checker, docstring: &Docstring, docstrings_args: &FxHa
} }
} }
/// Returns `true` if the parameter is annotated with `typing.Unpack`
fn has_unpack_annotation(checker: &Checker, parameter: &Parameter) -> bool {
parameter.annotation.as_ref().is_some_and(|annotation| {
checker
.semantic()
.match_typing_expr(map_subscript(annotation), "Unpack")
})
}
// See: `GOOGLE_ARGS_REGEX` in `pydocstyle/checker.py`. // See: `GOOGLE_ARGS_REGEX` in `pydocstyle/checker.py`.
static GOOGLE_ARGS_REGEX: LazyLock<Regex> = static GOOGLE_ARGS_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:(\r\n|\n)?\s*.+").unwrap()); LazyLock::new(|| Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:(\r\n|\n)?\s*.+").unwrap());

View File

@ -101,3 +101,13 @@ D417 Missing argument description in the docstring for `should_fail`: `Args`
200 | """ 200 | """
201 | Send a message. 201 | Send a message.
| |
D417 Missing argument description in the docstring for `function_with_unpack_and_missing_arg_doc_should_fail`: `query`
--> D417.py:238:5
|
236 | """
237 |
238 | def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
239 | """Function with Unpack kwargs but missing query arg documentation.
|

View File

@ -83,3 +83,13 @@ D417 Missing argument description in the docstring for `should_fail`: `Args`
200 | """ 200 | """
201 | Send a message. 201 | Send a message.
| |
D417 Missing argument description in the docstring for `function_with_unpack_and_missing_arg_doc_should_fail`: `query`
--> D417.py:238:5
|
236 | """
237 |
238 | def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
239 | """Function with Unpack kwargs but missing query arg documentation.
|

View File

@ -101,3 +101,13 @@ D417 Missing argument description in the docstring for `should_fail`: `Args`
200 | """ 200 | """
201 | Send a message. 201 | Send a message.
| |
D417 Missing argument description in the docstring for `function_with_unpack_and_missing_arg_doc_should_fail`: `query`
--> D417.py:238:5
|
236 | """
237 |
238 | def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
239 | """Function with Unpack kwargs but missing query arg documentation.
|

View File

@ -101,3 +101,13 @@ D417 Missing argument description in the docstring for `should_fail`: `Args`
200 | """ 200 | """
201 | Send a message. 201 | Send a message.
| |
D417 Missing argument description in the docstring for `function_with_unpack_and_missing_arg_doc_should_fail`: `query`
--> D417.py:238:5
|
236 | """
237 |
238 | def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
239 | """Function with Unpack kwargs but missing query arg documentation.
|

View File

@ -28,6 +28,7 @@ mod tests {
use crate::settings::types::PreviewMode; use crate::settings::types::PreviewMode;
use crate::settings::{LinterSettings, flags}; use crate::settings::{LinterSettings, flags};
use crate::source_kind::SourceKind; use crate::source_kind::SourceKind;
use crate::suppression::Suppressions;
use crate::test::{test_contents, test_path, test_snippet}; use crate::test::{test_contents, test_path, test_snippet};
use crate::{Locator, assert_diagnostics, assert_diagnostics_diff, directives}; use crate::{Locator, assert_diagnostics, assert_diagnostics_diff, directives};
@ -955,6 +956,8 @@ mod tests {
&locator, &locator,
&indexer, &indexer,
); );
let suppressions =
Suppressions::from_tokens(&settings, locator.contents(), parsed.tokens());
let mut messages = check_path( let mut messages = check_path(
Path::new("<filename>"), Path::new("<filename>"),
None, None,
@ -968,6 +971,7 @@ mod tests {
source_type, source_type,
&parsed, &parsed,
target_version, target_version,
&suppressions,
); );
messages.sort_by(Diagnostic::ruff_start_ordering); messages.sort_by(Diagnostic::ruff_start_ordering);
let actual = messages let actual = messages

View File

@ -3,7 +3,7 @@ use std::collections::hash_map::Entry;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::{ComparableExpr, HashableExpr}; use ruff_python_ast::comparable::{ComparableExpr, HashableExpr};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr}; use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -193,16 +193,14 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) {
parenthesized_range( parenthesized_range(
dict.value(i - 1).into(), dict.value(i - 1).into(),
dict.into(), dict.into(),
checker.comment_ranges(), checker.tokens(),
checker.locator().contents(),
) )
.unwrap_or_else(|| dict.value(i - 1).range()) .unwrap_or_else(|| dict.value(i - 1).range())
.end(), .end(),
parenthesized_range( parenthesized_range(
dict.value(i).into(), dict.value(i).into(),
dict.into(), dict.into(),
checker.comment_ranges(), checker.tokens(),
checker.locator().contents(),
) )
.unwrap_or_else(|| dict.value(i).range()) .unwrap_or_else(|| dict.value(i).range())
.end(), .end(),
@ -224,16 +222,14 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) {
parenthesized_range( parenthesized_range(
dict.value(i - 1).into(), dict.value(i - 1).into(),
dict.into(), dict.into(),
checker.comment_ranges(), checker.tokens(),
checker.locator().contents(),
) )
.unwrap_or_else(|| dict.value(i - 1).range()) .unwrap_or_else(|| dict.value(i - 1).range())
.end(), .end(),
parenthesized_range( parenthesized_range(
dict.value(i).into(), dict.value(i).into(),
dict.into(), dict.into(),
checker.comment_ranges(), checker.tokens(),
checker.locator().contents(),
) )
.unwrap_or_else(|| dict.value(i).range()) .unwrap_or_else(|| dict.value(i).range())
.end(), .end(),

View File

@ -2,7 +2,7 @@ use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::token::{TokenKind, Tokens}; use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_ast::{self as ast, Stmt}; use ruff_python_ast::{self as ast, Stmt};
use ruff_python_semantic::Binding; use ruff_python_semantic::Binding;
@ -172,12 +172,8 @@ fn remove_unused_variable(binding: &Binding, checker: &Checker) -> Option<Fix> {
{ {
// If the expression is complex (`x = foo()`), remove the assignment, // If the expression is complex (`x = foo()`), remove the assignment,
// but preserve the right-hand side. // but preserve the right-hand side.
let start = parenthesized_range( let start =
target.into(), parenthesized_range(target.into(), statement.into(), checker.tokens())
statement.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(target.range()) .unwrap_or(target.range())
.start(); .start();
let end = match_token_after(checker.tokens(), target.end(), |token| { let end = match_token_after(checker.tokens(), target.end(), |token| {

View File

@ -16,10 +16,10 @@ mod tests {
use crate::registry::Rule; use crate::registry::Rule;
use crate::rules::{flake8_tidy_imports, pylint}; use crate::rules::{flake8_tidy_imports, pylint};
use crate::assert_diagnostics;
use crate::settings::LinterSettings; use crate::settings::LinterSettings;
use crate::settings::types::PreviewMode; use crate::settings::types::PreviewMode;
use crate::test::test_path; use crate::test::test_path;
use crate::{assert_diagnostics, assert_diagnostics_diff};
#[test_case(Rule::SingledispatchMethod, Path::new("singledispatch_method.py"))] #[test_case(Rule::SingledispatchMethod, Path::new("singledispatch_method.py"))]
#[test_case( #[test_case(
@ -253,6 +253,32 @@ mod tests {
Ok(()) Ok(())
} }
#[test_case(
Rule::UselessExceptionStatement,
Path::new("useless_exception_statement.py")
)]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
assert_diagnostics_diff!(
snapshot,
Path::new("pylint").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Disabled,
..LinterSettings::for_rule(rule_code)
},
&LinterSettings {
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
}
);
Ok(())
}
#[test] #[test]
fn continue_in_finally() -> Result<()> { fn continue_in_finally() -> Result<()> {
let diagnostics = test_path( let diagnostics = test_path(

View File

@ -2,7 +2,7 @@ use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{ use ruff_python_ast::{
BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare, BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare,
parenthesize::{parentheses_iterator, parenthesized_range}, token::{parentheses_iterator, parenthesized_range},
}; };
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -62,7 +62,7 @@ pub(crate) fn boolean_chained_comparison(checker: &Checker, expr_bool_op: &ExprB
} }
let locator = checker.locator(); let locator = checker.locator();
let comment_ranges = checker.comment_ranges(); let tokens = checker.tokens();
// retrieve all compare expressions from boolean expression // retrieve all compare expressions from boolean expression
let compare_expressions = expr_bool_op let compare_expressions = expr_bool_op
@ -89,39 +89,21 @@ pub(crate) fn boolean_chained_comparison(checker: &Checker, expr_bool_op: &ExprB
continue; continue;
} }
let left_paren_count = parentheses_iterator( let left_paren_count =
left_compare.into(), parentheses_iterator(left_compare.into(), Some(expr_bool_op.into()), tokens).count();
Some(expr_bool_op.into()),
comment_ranges,
locator.contents(),
)
.count();
let right_paren_count = parentheses_iterator( let right_paren_count =
right_compare.into(), parentheses_iterator(right_compare.into(), Some(expr_bool_op.into()), tokens).count();
Some(expr_bool_op.into()),
comment_ranges,
locator.contents(),
)
.count();
// Create the edit that removes the comparison operator // Create the edit that removes the comparison operator
// In `a<(b) and ((b))<c`, we need to handle the // In `a<(b) and ((b))<c`, we need to handle the
// parentheses when specifying the fix range. // parentheses when specifying the fix range.
let left_compare_right_range = parenthesized_range( let left_compare_right_range =
left_compare_right.into(), parenthesized_range(left_compare_right.into(), left_compare.into(), tokens)
left_compare.into(),
comment_ranges,
locator.contents(),
)
.unwrap_or(left_compare_right.range()); .unwrap_or(left_compare_right.range());
let right_compare_left_range = parenthesized_range( let right_compare_left_range =
right_compare_left.into(), parenthesized_range(right_compare_left.into(), right_compare.into(), tokens)
right_compare.into(),
comment_ranges,
locator.contents(),
)
.unwrap_or(right_compare_left.range()); .unwrap_or(right_compare_left.range());
let edit = Edit::range_replacement( let edit = Edit::range_replacement(
locator.slice(left_compare_right_range).to_string(), locator.slice(left_compare_right_range).to_string(),

View File

@ -99,7 +99,7 @@ pub(crate) fn duplicate_bases(checker: &Checker, name: &str, arguments: Option<&
arguments, arguments,
Parentheses::Remove, Parentheses::Remove,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
.map(|edit| { .map(|edit| {
Fix::applicable_edit( Fix::applicable_edit(

View File

@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, CmpOp, Stmt}; use ruff_python_ast::{self as ast, CmpOp, Stmt};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -166,12 +166,7 @@ pub(crate) fn if_stmt_min_max(checker: &Checker, stmt_if: &ast::StmtIf) {
let replacement = format!( let replacement = format!(
"{} = {min_max}({}, {})", "{} = {min_max}({}, {})",
checker.locator().slice( checker.locator().slice(
parenthesized_range( parenthesized_range(body_target.into(), body.into(), checker.tokens())
body_target.into(),
body.into(),
checker.comment_ranges(),
checker.locator().contents()
)
.unwrap_or(body_target.range()) .unwrap_or(body_target.range())
), ),
checker.locator().slice(arg1), checker.locator().slice(arg1),

View File

@ -174,12 +174,8 @@ pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr
SliceBoundary::Last => "rsplit", SliceBoundary::Last => "rsplit",
}; };
let maxsplit_argument_edit = fix::edits::add_argument( let maxsplit_argument_edit =
"maxsplit=1", fix::edits::add_argument("maxsplit=1", arguments, checker.tokens());
arguments,
checker.comment_ranges(),
checker.locator().contents(),
);
// Only change `actual_split_type` if it doesn't match `suggested_split_type` // Only change `actual_split_type` if it doesn't match `suggested_split_type`
let split_type_edit: Option<Edit> = if actual_split_type == suggested_split_type { let split_type_edit: Option<Edit> = if actual_split_type == suggested_split_type {

View File

@ -2,7 +2,7 @@ use ast::Expr;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{ExprBinOp, ExprRef, Operator}; use ruff_python_ast::{ExprBinOp, ExprRef, Operator};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -150,12 +150,10 @@ fn augmented_assignment(
let right_operand_ref = ExprRef::from(right_operand); let right_operand_ref = ExprRef::from(right_operand);
let parent = original_expr.into(); let parent = original_expr.into();
let comment_ranges = checker.comment_ranges(); let tokens = checker.tokens();
let source = checker.source();
let right_operand_range = let right_operand_range =
parenthesized_range(right_operand_ref, parent, comment_ranges, source) parenthesized_range(right_operand_ref, parent, tokens).unwrap_or(right_operand.range());
.unwrap_or(right_operand.range());
let right_operand_expr = locator.slice(right_operand_range); let right_operand_expr = locator.slice(right_operand_range);
let target_expr = locator.slice(target); let target_expr = locator.slice(target);

View File

@ -75,12 +75,7 @@ pub(crate) fn subprocess_run_without_check(checker: &Checker, call: &ast::ExprCa
let mut diagnostic = let mut diagnostic =
checker.report_diagnostic(SubprocessRunWithoutCheck, call.func.range()); checker.report_diagnostic(SubprocessRunWithoutCheck, call.func.range());
diagnostic.set_fix(Fix::applicable_edit( diagnostic.set_fix(Fix::applicable_edit(
add_argument( add_argument("check=False", &call.arguments, checker.tokens()),
"check=False",
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
),
// If the function call contains `**kwargs`, mark the fix as unsafe. // If the function call contains `**kwargs`, mark the fix as unsafe.
if call if call
.arguments .arguments

View File

@ -1,8 +1,7 @@
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{self as ast, Expr, name::QualifiedName};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::typing; use ruff_python_semantic::analyze::typing;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -193,8 +192,7 @@ fn generate_keyword_fix(checker: &Checker, call: &ast::ExprCall) -> Fix {
})) }))
), ),
&call.arguments, &call.arguments,
checker.comment_ranges(), checker.tokens(),
checker.locator().contents(),
)) ))
} }

View File

@ -1,10 +1,11 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Expr}; use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::{SemanticModel, analyze};
use ruff_python_stdlib::builtins; use ruff_python_stdlib::builtins;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::preview::is_custom_exception_checking_enabled;
use crate::{Edit, Fix, FixAvailability, Violation}; use crate::{Edit, Fix, FixAvailability, Violation};
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
@ -20,6 +21,9 @@ use ruff_python_ast::PythonVersion;
/// This rule only detects built-in exceptions, like `ValueError`, and does /// This rule only detects built-in exceptions, like `ValueError`, and does
/// not catch user-defined exceptions. /// not catch user-defined exceptions.
/// ///
/// In [preview], this rule will also detect user-defined exceptions, but only
/// the ones defined in the file being checked.
///
/// ## Example /// ## Example
/// ```python /// ```python
/// ValueError("...") /// ValueError("...")
@ -32,7 +36,8 @@ use ruff_python_ast::PythonVersion;
/// ///
/// ## Fix safety /// ## Fix safety
/// This rule's fix is marked as unsafe, as converting a useless exception /// This rule's fix is marked as unsafe, as converting a useless exception
/// statement to a `raise` statement will change the program's behavior. ///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[derive(ViolationMetadata)] #[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "0.5.0")] #[violation_metadata(stable_since = "0.5.0")]
pub(crate) struct UselessExceptionStatement; pub(crate) struct UselessExceptionStatement;
@ -56,7 +61,10 @@ pub(crate) fn useless_exception_statement(checker: &Checker, expr: &ast::StmtExp
return; return;
}; };
if is_builtin_exception(func, checker.semantic(), checker.target_version()) { if is_builtin_exception(func, checker.semantic(), checker.target_version())
|| (is_custom_exception_checking_enabled(checker.settings())
&& is_custom_exception(func, checker.semantic(), checker.target_version()))
{
let mut diagnostic = checker.report_diagnostic(UselessExceptionStatement, expr.range()); let mut diagnostic = checker.report_diagnostic(UselessExceptionStatement, expr.range());
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion( diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
"raise ".to_string(), "raise ".to_string(),
@ -78,3 +86,34 @@ fn is_builtin_exception(
if builtins::is_exception(name, target_version.minor)) if builtins::is_exception(name, target_version.minor))
}) })
} }
/// Returns `true` if the given expression is a custom exception.
fn is_custom_exception(
expr: &Expr,
semantic: &SemanticModel,
target_version: PythonVersion,
) -> bool {
let Some(qualified_name) = semantic.resolve_qualified_name(expr) else {
return false;
};
let Some(symbol) = qualified_name.segments().last() else {
return false;
};
let Some(binding_id) = semantic.lookup_symbol(symbol) else {
return false;
};
let binding = semantic.binding(binding_id);
let Some(source) = binding.source else {
return false;
};
let statement = semantic.statement(source);
if let ast::Stmt::ClassDef(class_def) = statement {
return analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
if let ["" | "builtins", name] = qualified_name.segments() {
return builtins::is_exception(name, target_version.minor);
}
false
});
}
false
}

View File

@ -2,250 +2,294 @@
source: crates/ruff_linter/src/rules/pylint/mod.rs source: crates/ruff_linter/src/rules/pylint/mod.rs
--- ---
PLW0133 [*] Missing `raise` statement on exception PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:7:5 --> useless_exception_statement.py:26:5
| |
5 | # Test case 1: Useless exception statement 24 | # Test case 1: Useless exception statement
6 | def func():
7 | AssertionError("This is an assertion error") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
4 |
5 | # Test case 1: Useless exception statement
6 | def func():
- AssertionError("This is an assertion error") # PLW0133
7 + raise AssertionError("This is an assertion error") # PLW0133
8 |
9 |
10 | # Test case 2: Useless exception statement in try-except block
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:13:9
|
11 | def func():
12 | try:
13 | Exception("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14 | except Exception as err:
15 | pass
|
help: Add `raise` keyword
10 | # Test case 2: Useless exception statement in try-except block
11 | def func():
12 | try:
- Exception("This is an exception") # PLW0133
13 + raise Exception("This is an exception") # PLW0133
14 | except Exception as err:
15 | pass
16 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:21:9
|
19 | def func():
20 | if True:
21 | RuntimeError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
18 | # Test case 3: Useless exception statement in if statement
19 | def func():
20 | if True:
- RuntimeError("This is an exception") # PLW0133
21 + raise RuntimeError("This is an exception") # PLW0133
22 |
23 |
24 | # Test case 4: Useless exception statement in class
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:28:13
|
26 | class Class:
27 | def __init__(self):
28 | TypeError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
25 | def func(): 25 | def func():
26 | class Class: 26 | AssertionError("This is an assertion error") # PLW0133
27 | def __init__(self): | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
27 | MyError("This is a custom error") # PLW0133
28 | MySubError("This is a custom error") # PLW0133
|
help: Add `raise` keyword
23 |
24 | # Test case 1: Useless exception statement
25 | def func():
- AssertionError("This is an assertion error") # PLW0133
26 + raise AssertionError("This is an assertion error") # PLW0133
27 | MyError("This is a custom error") # PLW0133
28 | MySubError("This is a custom error") # PLW0133
29 | MyValueError("This is a custom value error") # PLW0133
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:35:9
|
33 | def func():
34 | try:
35 | Exception("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
36 | MyError("This is an exception") # PLW0133
37 | MySubError("This is an exception") # PLW0133
|
help: Add `raise` keyword
32 | # Test case 2: Useless exception statement in try-except block
33 | def func():
34 | try:
- Exception("This is an exception") # PLW0133
35 + raise Exception("This is an exception") # PLW0133
36 | MyError("This is an exception") # PLW0133
37 | MySubError("This is an exception") # PLW0133
38 | MyValueError("This is an exception") # PLW0133
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:46:9
|
44 | def func():
45 | if True:
46 | RuntimeError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
47 | MyError("This is an exception") # PLW0133
48 | MySubError("This is an exception") # PLW0133
|
help: Add `raise` keyword
43 | # Test case 3: Useless exception statement in if statement
44 | def func():
45 | if True:
- RuntimeError("This is an exception") # PLW0133
46 + raise RuntimeError("This is an exception") # PLW0133
47 | MyError("This is an exception") # PLW0133
48 | MySubError("This is an exception") # PLW0133
49 | MyValueError("This is an exception") # PLW0133
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:56:13
|
54 | class Class:
55 | def __init__(self):
56 | TypeError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
57 | MyError("This is an exception") # PLW0133
58 | MySubError("This is an exception") # PLW0133
|
help: Add `raise` keyword
53 | def func():
54 | class Class:
55 | def __init__(self):
- TypeError("This is an exception") # PLW0133 - TypeError("This is an exception") # PLW0133
28 + raise TypeError("This is an exception") # PLW0133 56 + raise TypeError("This is an exception") # PLW0133
29 | 57 | MyError("This is an exception") # PLW0133
30 | 58 | MySubError("This is an exception") # PLW0133
31 | # Test case 5: Useless exception statement in function 59 | MyValueError("This is an exception") # PLW0133
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:34:9 --> useless_exception_statement.py:65:9
| |
32 | def func(): 63 | def func():
33 | def inner(): 64 | def inner():
34 | IndexError("This is an exception") # PLW0133 65 | IndexError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
35 | 66 | MyError("This is an exception") # PLW0133
36 | inner() 67 | MySubError("This is an exception") # PLW0133
| |
help: Add `raise` keyword help: Add `raise` keyword
31 | # Test case 5: Useless exception statement in function 62 | # Test case 5: Useless exception statement in function
32 | def func(): 63 | def func():
33 | def inner(): 64 | def inner():
- IndexError("This is an exception") # PLW0133 - IndexError("This is an exception") # PLW0133
34 + raise IndexError("This is an exception") # PLW0133 65 + raise IndexError("This is an exception") # PLW0133
35 | 66 | MyError("This is an exception") # PLW0133
36 | inner() 67 | MySubError("This is an exception") # PLW0133
37 | 68 | MyValueError("This is an exception") # PLW0133
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:42:9 --> useless_exception_statement.py:76:9
| |
40 | def func(): 74 | def func():
41 | while True: 75 | while True:
42 | KeyError("This is an exception") # PLW0133 76 | KeyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
77 | MyError("This is an exception") # PLW0133
78 | MySubError("This is an exception") # PLW0133
| |
help: Add `raise` keyword help: Add `raise` keyword
39 | # Test case 6: Useless exception statement in while loop 73 | # Test case 6: Useless exception statement in while loop
40 | def func(): 74 | def func():
41 | while True: 75 | while True:
- KeyError("This is an exception") # PLW0133 - KeyError("This is an exception") # PLW0133
42 + raise KeyError("This is an exception") # PLW0133 76 + raise KeyError("This is an exception") # PLW0133
43 | 77 | MyError("This is an exception") # PLW0133
44 | 78 | MySubError("This is an exception") # PLW0133
45 | # Test case 7: Useless exception statement in abstract class 79 | MyValueError("This is an exception") # PLW0133
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:50:13 --> useless_exception_statement.py:87:13
| |
48 | @abstractmethod 85 | @abstractmethod
49 | def method(self): 86 | def method(self):
50 | NotImplementedError("This is an exception") # PLW0133 87 | NotImplementedError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
88 | MyError("This is an exception") # PLW0133
89 | MySubError("This is an exception") # PLW0133
| |
help: Add `raise` keyword help: Add `raise` keyword
47 | class Class(ABC): 84 | class Class(ABC):
48 | @abstractmethod 85 | @abstractmethod
49 | def method(self): 86 | def method(self):
- NotImplementedError("This is an exception") # PLW0133 - NotImplementedError("This is an exception") # PLW0133
50 + raise NotImplementedError("This is an exception") # PLW0133 87 + raise NotImplementedError("This is an exception") # PLW0133
51 | 88 | MyError("This is an exception") # PLW0133
52 | 89 | MySubError("This is an exception") # PLW0133
53 | # Test case 8: Useless exception statement inside context manager 90 | MyValueError("This is an exception") # PLW0133
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:56:9 --> useless_exception_statement.py:96:9
| |
54 | def func(): 94 | def func():
55 | with suppress(AttributeError): 95 | with suppress(Exception):
56 | AttributeError("This is an exception") # PLW0133 96 | AttributeError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
97 | MyError("This is an exception") # PLW0133
98 | MySubError("This is an exception") # PLW0133
| |
help: Add `raise` keyword help: Add `raise` keyword
53 | # Test case 8: Useless exception statement inside context manager 93 | # Test case 8: Useless exception statement inside context manager
54 | def func(): 94 | def func():
55 | with suppress(AttributeError): 95 | with suppress(Exception):
- AttributeError("This is an exception") # PLW0133 - AttributeError("This is an exception") # PLW0133
56 + raise AttributeError("This is an exception") # PLW0133 96 + raise AttributeError("This is an exception") # PLW0133
57 | 97 | MyError("This is an exception") # PLW0133
58 | 98 | MySubError("This is an exception") # PLW0133
59 | # Test case 9: Useless exception statement in parentheses 99 | MyValueError("This is an exception") # PLW0133
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:61:5 --> useless_exception_statement.py:104:5
| |
59 | # Test case 9: Useless exception statement in parentheses 102 | # Test case 9: Useless exception statement in parentheses
60 | def func(): 103 | def func():
61 | (RuntimeError("This is an exception")) # PLW0133 104 | (RuntimeError("This is an exception")) # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
105 | (MyError("This is an exception")) # PLW0133
106 | (MySubError("This is an exception")) # PLW0133
| |
help: Add `raise` keyword help: Add `raise` keyword
58 | 101 |
59 | # Test case 9: Useless exception statement in parentheses 102 | # Test case 9: Useless exception statement in parentheses
60 | def func(): 103 | def func():
- (RuntimeError("This is an exception")) # PLW0133 - (RuntimeError("This is an exception")) # PLW0133
61 + raise (RuntimeError("This is an exception")) # PLW0133 104 + raise (RuntimeError("This is an exception")) # PLW0133
62 | 105 | (MyError("This is an exception")) # PLW0133
63 | 106 | (MySubError("This is an exception")) # PLW0133
64 | # Test case 10: Useless exception statement in continuation 107 | (MyValueError("This is an exception")) # PLW0133
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:66:12 --> useless_exception_statement.py:112:12
| |
64 | # Test case 10: Useless exception statement in continuation 110 | # Test case 10: Useless exception statement in continuation
65 | def func(): 111 | def func():
66 | x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133 112 | x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
113 | x = 1; (MyError("This is an exception")); y = 2 # PLW0133
114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
| |
help: Add `raise` keyword help: Add `raise` keyword
63 | 109 |
64 | # Test case 10: Useless exception statement in continuation 110 | # Test case 10: Useless exception statement in continuation
65 | def func(): 111 | def func():
- x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133 - x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
66 + x = 1; raise (RuntimeError("This is an exception")); y = 2 # PLW0133 112 + x = 1; raise (RuntimeError("This is an exception")); y = 2 # PLW0133
67 | 113 | x = 1; (MyError("This is an exception")); y = 2 # PLW0133
68 | 114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
69 | # Test case 11: Useless warning statement 115 | x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:71:5 --> useless_exception_statement.py:120:5
| |
69 | # Test case 11: Useless warning statement 118 | # Test case 11: Useless warning statement
70 | def func(): 119 | def func():
71 | UserWarning("This is an assertion error") # PLW0133 120 | UserWarning("This is a user warning") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| 121 | MyUserWarning("This is a custom user warning") # PLW0133
help: Add `raise` keyword
68 |
69 | # Test case 11: Useless warning statement
70 | def func():
- UserWarning("This is an assertion error") # PLW0133
71 + raise UserWarning("This is an assertion error") # PLW0133
72 |
73 |
74 | # Non-violation test cases: PLW0133
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:126:1
|
124 | import builtins
125 |
126 | builtins.TypeError("still an exception even though it's an Attribute")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
127 |
128 | PythonFinalizationError("Added in Python 3.13")
| |
help: Add `raise` keyword help: Add `raise` keyword
117 |
118 | # Test case 11: Useless warning statement
119 | def func():
- UserWarning("This is a user warning") # PLW0133
120 + raise UserWarning("This is a user warning") # PLW0133
121 | MyUserWarning("This is a custom user warning") # PLW0133
122 |
123 | 123 |
124 | import builtins
125 |
- builtins.TypeError("still an exception even though it's an Attribute")
126 + raise builtins.TypeError("still an exception even though it's an Attribute")
127 |
128 | PythonFinalizationError("Added in Python 3.13")
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:128:1 --> useless_exception_statement.py:127:1
| |
126 | builtins.TypeError("still an exception even though it's an Attribute") 125 | import builtins
127 | 126 |
128 | PythonFinalizationError("Added in Python 3.13") 127 | builtins.TypeError("still an exception even though it's an Attribute") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
128 |
129 | PythonFinalizationError("Added in Python 3.13") # PLW0133
| |
help: Add `raise` keyword help: Add `raise` keyword
125 | 124 | # Test case 12: Useless exception statement at module level
126 | builtins.TypeError("still an exception even though it's an Attribute") 125 | import builtins
127 | 126 |
- PythonFinalizationError("Added in Python 3.13") - builtins.TypeError("still an exception even though it's an Attribute") # PLW0133
128 + raise PythonFinalizationError("Added in Python 3.13") 127 + raise builtins.TypeError("still an exception even though it's an Attribute") # PLW0133
128 |
129 | PythonFinalizationError("Added in Python 3.13") # PLW0133
130 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:129:1
|
127 | builtins.TypeError("still an exception even though it's an Attribute") # PLW0133
128 |
129 | PythonFinalizationError("Added in Python 3.13") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
130 |
131 | MyError("This is an exception") # PLW0133
|
help: Add `raise` keyword
126 |
127 | builtins.TypeError("still an exception even though it's an Attribute") # PLW0133
128 |
- PythonFinalizationError("Added in Python 3.13") # PLW0133
129 + raise PythonFinalizationError("Added in Python 3.13") # PLW0133
130 |
131 | MyError("This is an exception") # PLW0133
132 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:137:1
|
135 | MyValueError("This is an exception") # PLW0133
136 |
137 | UserWarning("This is a user warning") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
138 |
139 | MyUserWarning("This is a custom user warning") # PLW0133
|
help: Add `raise` keyword
134 |
135 | MyValueError("This is an exception") # PLW0133
136 |
- UserWarning("This is a user warning") # PLW0133
137 + raise UserWarning("This is a user warning") # PLW0133
138 |
139 | MyUserWarning("This is a custom user warning") # PLW0133
140 |
note: This is an unsafe fix and may change runtime behavior note: This is an unsafe fix and may change runtime behavior

View File

@ -0,0 +1,751 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 35
--- Added ---
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:27:5
|
25 | def func():
26 | AssertionError("This is an assertion error") # PLW0133
27 | MyError("This is a custom error") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28 | MySubError("This is a custom error") # PLW0133
29 | MyValueError("This is a custom value error") # PLW0133
|
help: Add `raise` keyword
24 | # Test case 1: Useless exception statement
25 | def func():
26 | AssertionError("This is an assertion error") # PLW0133
- MyError("This is a custom error") # PLW0133
27 + raise MyError("This is a custom error") # PLW0133
28 | MySubError("This is a custom error") # PLW0133
29 | MyValueError("This is a custom value error") # PLW0133
30 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:28:5
|
26 | AssertionError("This is an assertion error") # PLW0133
27 | MyError("This is a custom error") # PLW0133
28 | MySubError("This is a custom error") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
29 | MyValueError("This is a custom value error") # PLW0133
|
help: Add `raise` keyword
25 | def func():
26 | AssertionError("This is an assertion error") # PLW0133
27 | MyError("This is a custom error") # PLW0133
- MySubError("This is a custom error") # PLW0133
28 + raise MySubError("This is a custom error") # PLW0133
29 | MyValueError("This is a custom value error") # PLW0133
30 |
31 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:29:5
|
27 | MyError("This is a custom error") # PLW0133
28 | MySubError("This is a custom error") # PLW0133
29 | MyValueError("This is a custom value error") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
26 | AssertionError("This is an assertion error") # PLW0133
27 | MyError("This is a custom error") # PLW0133
28 | MySubError("This is a custom error") # PLW0133
- MyValueError("This is a custom value error") # PLW0133
29 + raise MyValueError("This is a custom value error") # PLW0133
30 |
31 |
32 | # Test case 2: Useless exception statement in try-except block
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:36:9
|
34 | try:
35 | Exception("This is an exception") # PLW0133
36 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
37 | MySubError("This is an exception") # PLW0133
38 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
33 | def func():
34 | try:
35 | Exception("This is an exception") # PLW0133
- MyError("This is an exception") # PLW0133
36 + raise MyError("This is an exception") # PLW0133
37 | MySubError("This is an exception") # PLW0133
38 | MyValueError("This is an exception") # PLW0133
39 | except Exception as err:
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:37:9
|
35 | Exception("This is an exception") # PLW0133
36 | MyError("This is an exception") # PLW0133
37 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
38 | MyValueError("This is an exception") # PLW0133
39 | except Exception as err:
|
help: Add `raise` keyword
34 | try:
35 | Exception("This is an exception") # PLW0133
36 | MyError("This is an exception") # PLW0133
- MySubError("This is an exception") # PLW0133
37 + raise MySubError("This is an exception") # PLW0133
38 | MyValueError("This is an exception") # PLW0133
39 | except Exception as err:
40 | pass
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:38:9
|
36 | MyError("This is an exception") # PLW0133
37 | MySubError("This is an exception") # PLW0133
38 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
39 | except Exception as err:
40 | pass
|
help: Add `raise` keyword
35 | Exception("This is an exception") # PLW0133
36 | MyError("This is an exception") # PLW0133
37 | MySubError("This is an exception") # PLW0133
- MyValueError("This is an exception") # PLW0133
38 + raise MyValueError("This is an exception") # PLW0133
39 | except Exception as err:
40 | pass
41 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:47:9
|
45 | if True:
46 | RuntimeError("This is an exception") # PLW0133
47 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
48 | MySubError("This is an exception") # PLW0133
49 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
44 | def func():
45 | if True:
46 | RuntimeError("This is an exception") # PLW0133
- MyError("This is an exception") # PLW0133
47 + raise MyError("This is an exception") # PLW0133
48 | MySubError("This is an exception") # PLW0133
49 | MyValueError("This is an exception") # PLW0133
50 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:48:9
|
46 | RuntimeError("This is an exception") # PLW0133
47 | MyError("This is an exception") # PLW0133
48 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
45 | if True:
46 | RuntimeError("This is an exception") # PLW0133
47 | MyError("This is an exception") # PLW0133
- MySubError("This is an exception") # PLW0133
48 + raise MySubError("This is an exception") # PLW0133
49 | MyValueError("This is an exception") # PLW0133
50 |
51 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:49:9
|
47 | MyError("This is an exception") # PLW0133
48 | MySubError("This is an exception") # PLW0133
49 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
46 | RuntimeError("This is an exception") # PLW0133
47 | MyError("This is an exception") # PLW0133
48 | MySubError("This is an exception") # PLW0133
- MyValueError("This is an exception") # PLW0133
49 + raise MyValueError("This is an exception") # PLW0133
50 |
51 |
52 | # Test case 4: Useless exception statement in class
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:57:13
|
55 | def __init__(self):
56 | TypeError("This is an exception") # PLW0133
57 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58 | MySubError("This is an exception") # PLW0133
59 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
54 | class Class:
55 | def __init__(self):
56 | TypeError("This is an exception") # PLW0133
- MyError("This is an exception") # PLW0133
57 + raise MyError("This is an exception") # PLW0133
58 | MySubError("This is an exception") # PLW0133
59 | MyValueError("This is an exception") # PLW0133
60 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:58:13
|
56 | TypeError("This is an exception") # PLW0133
57 | MyError("This is an exception") # PLW0133
58 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
55 | def __init__(self):
56 | TypeError("This is an exception") # PLW0133
57 | MyError("This is an exception") # PLW0133
- MySubError("This is an exception") # PLW0133
58 + raise MySubError("This is an exception") # PLW0133
59 | MyValueError("This is an exception") # PLW0133
60 |
61 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:59:13
|
57 | MyError("This is an exception") # PLW0133
58 | MySubError("This is an exception") # PLW0133
59 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
56 | TypeError("This is an exception") # PLW0133
57 | MyError("This is an exception") # PLW0133
58 | MySubError("This is an exception") # PLW0133
- MyValueError("This is an exception") # PLW0133
59 + raise MyValueError("This is an exception") # PLW0133
60 |
61 |
62 | # Test case 5: Useless exception statement in function
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:66:9
|
64 | def inner():
65 | IndexError("This is an exception") # PLW0133
66 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
67 | MySubError("This is an exception") # PLW0133
68 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
63 | def func():
64 | def inner():
65 | IndexError("This is an exception") # PLW0133
- MyError("This is an exception") # PLW0133
66 + raise MyError("This is an exception") # PLW0133
67 | MySubError("This is an exception") # PLW0133
68 | MyValueError("This is an exception") # PLW0133
69 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:67:9
|
65 | IndexError("This is an exception") # PLW0133
66 | MyError("This is an exception") # PLW0133
67 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
68 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
64 | def inner():
65 | IndexError("This is an exception") # PLW0133
66 | MyError("This is an exception") # PLW0133
- MySubError("This is an exception") # PLW0133
67 + raise MySubError("This is an exception") # PLW0133
68 | MyValueError("This is an exception") # PLW0133
69 |
70 | inner()
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:68:9
|
66 | MyError("This is an exception") # PLW0133
67 | MySubError("This is an exception") # PLW0133
68 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
69 |
70 | inner()
|
help: Add `raise` keyword
65 | IndexError("This is an exception") # PLW0133
66 | MyError("This is an exception") # PLW0133
67 | MySubError("This is an exception") # PLW0133
- MyValueError("This is an exception") # PLW0133
68 + raise MyValueError("This is an exception") # PLW0133
69 |
70 | inner()
71 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:77:9
|
75 | while True:
76 | KeyError("This is an exception") # PLW0133
77 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
78 | MySubError("This is an exception") # PLW0133
79 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
74 | def func():
75 | while True:
76 | KeyError("This is an exception") # PLW0133
- MyError("This is an exception") # PLW0133
77 + raise MyError("This is an exception") # PLW0133
78 | MySubError("This is an exception") # PLW0133
79 | MyValueError("This is an exception") # PLW0133
80 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:78:9
|
76 | KeyError("This is an exception") # PLW0133
77 | MyError("This is an exception") # PLW0133
78 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
79 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
75 | while True:
76 | KeyError("This is an exception") # PLW0133
77 | MyError("This is an exception") # PLW0133
- MySubError("This is an exception") # PLW0133
78 + raise MySubError("This is an exception") # PLW0133
79 | MyValueError("This is an exception") # PLW0133
80 |
81 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:79:9
|
77 | MyError("This is an exception") # PLW0133
78 | MySubError("This is an exception") # PLW0133
79 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
76 | KeyError("This is an exception") # PLW0133
77 | MyError("This is an exception") # PLW0133
78 | MySubError("This is an exception") # PLW0133
- MyValueError("This is an exception") # PLW0133
79 + raise MyValueError("This is an exception") # PLW0133
80 |
81 |
82 | # Test case 7: Useless exception statement in abstract class
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:88:13
|
86 | def method(self):
87 | NotImplementedError("This is an exception") # PLW0133
88 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
89 | MySubError("This is an exception") # PLW0133
90 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
85 | @abstractmethod
86 | def method(self):
87 | NotImplementedError("This is an exception") # PLW0133
- MyError("This is an exception") # PLW0133
88 + raise MyError("This is an exception") # PLW0133
89 | MySubError("This is an exception") # PLW0133
90 | MyValueError("This is an exception") # PLW0133
91 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:89:13
|
87 | NotImplementedError("This is an exception") # PLW0133
88 | MyError("This is an exception") # PLW0133
89 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
90 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
86 | def method(self):
87 | NotImplementedError("This is an exception") # PLW0133
88 | MyError("This is an exception") # PLW0133
- MySubError("This is an exception") # PLW0133
89 + raise MySubError("This is an exception") # PLW0133
90 | MyValueError("This is an exception") # PLW0133
91 |
92 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:90:13
|
88 | MyError("This is an exception") # PLW0133
89 | MySubError("This is an exception") # PLW0133
90 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
87 | NotImplementedError("This is an exception") # PLW0133
88 | MyError("This is an exception") # PLW0133
89 | MySubError("This is an exception") # PLW0133
- MyValueError("This is an exception") # PLW0133
90 + raise MyValueError("This is an exception") # PLW0133
91 |
92 |
93 | # Test case 8: Useless exception statement inside context manager
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:97:9
|
95 | with suppress(Exception):
96 | AttributeError("This is an exception") # PLW0133
97 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
98 | MySubError("This is an exception") # PLW0133
99 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
94 | def func():
95 | with suppress(Exception):
96 | AttributeError("This is an exception") # PLW0133
- MyError("This is an exception") # PLW0133
97 + raise MyError("This is an exception") # PLW0133
98 | MySubError("This is an exception") # PLW0133
99 | MyValueError("This is an exception") # PLW0133
100 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:98:9
|
96 | AttributeError("This is an exception") # PLW0133
97 | MyError("This is an exception") # PLW0133
98 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
99 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
95 | with suppress(Exception):
96 | AttributeError("This is an exception") # PLW0133
97 | MyError("This is an exception") # PLW0133
- MySubError("This is an exception") # PLW0133
98 + raise MySubError("This is an exception") # PLW0133
99 | MyValueError("This is an exception") # PLW0133
100 |
101 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:99:9
|
97 | MyError("This is an exception") # PLW0133
98 | MySubError("This is an exception") # PLW0133
99 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
96 | AttributeError("This is an exception") # PLW0133
97 | MyError("This is an exception") # PLW0133
98 | MySubError("This is an exception") # PLW0133
- MyValueError("This is an exception") # PLW0133
99 + raise MyValueError("This is an exception") # PLW0133
100 |
101 |
102 | # Test case 9: Useless exception statement in parentheses
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:105:5
|
103 | def func():
104 | (RuntimeError("This is an exception")) # PLW0133
105 | (MyError("This is an exception")) # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
106 | (MySubError("This is an exception")) # PLW0133
107 | (MyValueError("This is an exception")) # PLW0133
|
help: Add `raise` keyword
102 | # Test case 9: Useless exception statement in parentheses
103 | def func():
104 | (RuntimeError("This is an exception")) # PLW0133
- (MyError("This is an exception")) # PLW0133
105 + raise (MyError("This is an exception")) # PLW0133
106 | (MySubError("This is an exception")) # PLW0133
107 | (MyValueError("This is an exception")) # PLW0133
108 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:106:5
|
104 | (RuntimeError("This is an exception")) # PLW0133
105 | (MyError("This is an exception")) # PLW0133
106 | (MySubError("This is an exception")) # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
107 | (MyValueError("This is an exception")) # PLW0133
|
help: Add `raise` keyword
103 | def func():
104 | (RuntimeError("This is an exception")) # PLW0133
105 | (MyError("This is an exception")) # PLW0133
- (MySubError("This is an exception")) # PLW0133
106 + raise (MySubError("This is an exception")) # PLW0133
107 | (MyValueError("This is an exception")) # PLW0133
108 |
109 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:107:5
|
105 | (MyError("This is an exception")) # PLW0133
106 | (MySubError("This is an exception")) # PLW0133
107 | (MyValueError("This is an exception")) # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
104 | (RuntimeError("This is an exception")) # PLW0133
105 | (MyError("This is an exception")) # PLW0133
106 | (MySubError("This is an exception")) # PLW0133
- (MyValueError("This is an exception")) # PLW0133
107 + raise (MyValueError("This is an exception")) # PLW0133
108 |
109 |
110 | # Test case 10: Useless exception statement in continuation
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:113:12
|
111 | def func():
112 | x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
113 | x = 1; (MyError("This is an exception")); y = 2 # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
115 | x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
|
help: Add `raise` keyword
110 | # Test case 10: Useless exception statement in continuation
111 | def func():
112 | x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
- x = 1; (MyError("This is an exception")); y = 2 # PLW0133
113 + x = 1; raise (MyError("This is an exception")); y = 2 # PLW0133
114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
115 | x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
116 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:114:12
|
112 | x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
113 | x = 1; (MyError("This is an exception")); y = 2 # PLW0133
114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
115 | x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
|
help: Add `raise` keyword
111 | def func():
112 | x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
113 | x = 1; (MyError("This is an exception")); y = 2 # PLW0133
- x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
114 + x = 1; raise (MySubError("This is an exception")); y = 2 # PLW0133
115 | x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
116 |
117 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:115:12
|
113 | x = 1; (MyError("This is an exception")); y = 2 # PLW0133
114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
115 | x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
112 | x = 1; (RuntimeError("This is an exception")); y = 2 # PLW0133
113 | x = 1; (MyError("This is an exception")); y = 2 # PLW0133
114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133
- x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133
115 + x = 1; raise (MyValueError("This is an exception")); y = 2 # PLW0133
116 |
117 |
118 | # Test case 11: Useless warning statement
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:121:5
|
119 | def func():
120 | UserWarning("This is a user warning") # PLW0133
121 | MyUserWarning("This is a custom user warning") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
118 | # Test case 11: Useless warning statement
119 | def func():
120 | UserWarning("This is a user warning") # PLW0133
- MyUserWarning("This is a custom user warning") # PLW0133
121 + raise MyUserWarning("This is a custom user warning") # PLW0133
122 |
123 |
124 | # Test case 12: Useless exception statement at module level
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:131:1
|
129 | PythonFinalizationError("Added in Python 3.13") # PLW0133
130 |
131 | MyError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
132 |
133 | MySubError("This is an exception") # PLW0133
|
help: Add `raise` keyword
128 |
129 | PythonFinalizationError("Added in Python 3.13") # PLW0133
130 |
- MyError("This is an exception") # PLW0133
131 + raise MyError("This is an exception") # PLW0133
132 |
133 | MySubError("This is an exception") # PLW0133
134 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:133:1
|
131 | MyError("This is an exception") # PLW0133
132 |
133 | MySubError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
134 |
135 | MyValueError("This is an exception") # PLW0133
|
help: Add `raise` keyword
130 |
131 | MyError("This is an exception") # PLW0133
132 |
- MySubError("This is an exception") # PLW0133
133 + raise MySubError("This is an exception") # PLW0133
134 |
135 | MyValueError("This is an exception") # PLW0133
136 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:135:1
|
133 | MySubError("This is an exception") # PLW0133
134 |
135 | MyValueError("This is an exception") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
136 |
137 | UserWarning("This is a user warning") # PLW0133
|
help: Add `raise` keyword
132 |
133 | MySubError("This is an exception") # PLW0133
134 |
- MyValueError("This is an exception") # PLW0133
135 + raise MyValueError("This is an exception") # PLW0133
136 |
137 | UserWarning("This is a user warning") # PLW0133
138 |
note: This is an unsafe fix and may change runtime behavior
PLW0133 [*] Missing `raise` statement on exception
--> useless_exception_statement.py:139:1
|
137 | UserWarning("This is a user warning") # PLW0133
138 |
139 | MyUserWarning("This is a custom user warning") # PLW0133
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `raise` keyword
136 |
137 | UserWarning("This is a user warning") # PLW0133
138 |
- MyUserWarning("This is a custom user warning") # PLW0133
139 + raise MyUserWarning("This is a custom user warning") # PLW0133
140 |
141 |
142 | # Non-violation test cases: PLW0133
note: This is an unsafe fix and may change runtime behavior

View File

@ -204,7 +204,7 @@ pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassD
arguments, arguments,
Parentheses::Remove, Parentheses::Remove,
checker.source(), checker.source(),
checker.comment_ranges(), checker.tokens(),
)?; )?;
Ok(Fix::unsafe_edits( Ok(Fix::unsafe_edits(
Edit::insertion(type_params.to_string(), name.end()), Edit::insertion(type_params.to_string(), name.end()),

View File

@ -2,7 +2,7 @@ use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::visitor::Visitor; use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{Expr, ExprCall, ExprName, Keyword, StmtAnnAssign, StmtAssign, StmtRef}; use ruff_python_ast::{Expr, ExprCall, ExprName, Keyword, StmtAnnAssign, StmtAssign, StmtRef};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -261,11 +261,11 @@ fn create_diagnostic(
type_alias_kind: TypeAliasKind, type_alias_kind: TypeAliasKind,
) { ) {
let source = checker.source(); let source = checker.source();
let tokens = checker.tokens();
let comment_ranges = checker.comment_ranges(); let comment_ranges = checker.comment_ranges();
let range_with_parentheses = let range_with_parentheses =
parenthesized_range(value.into(), stmt.into(), comment_ranges, source) parenthesized_range(value.into(), stmt.into(), tokens).unwrap_or(value.range());
.unwrap_or(value.range());
let content = format!( let content = format!(
"type {name}{type_params} = {value}", "type {name}{type_params} = {value}",

View File

@ -1,9 +1,8 @@
use anyhow::Result; use anyhow::Result;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Keyword}; use ruff_python_ast::{self as ast, Keyword, token::Tokens};
use ruff_python_semantic::Modules; use ruff_python_semantic::Modules;
use ruff_python_trivia::CommentRanges;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -104,7 +103,7 @@ pub(crate) fn replace_stdout_stderr(checker: &Checker, call: &ast::ExprCall) {
stderr, stderr,
call, call,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
}); });
} }
@ -117,7 +116,7 @@ fn generate_fix(
stderr: &Keyword, stderr: &Keyword,
call: &ast::ExprCall, call: &ast::ExprCall,
source: &str, source: &str,
comment_ranges: &CommentRanges, tokens: &Tokens,
) -> Result<Fix> { ) -> Result<Fix> {
let (first, second) = if stdout.start() < stderr.start() { let (first, second) = if stdout.start() < stderr.start() {
(stdout, stderr) (stdout, stderr)
@ -132,7 +131,7 @@ fn generate_fix(
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
source, source,
comment_ranges, tokens,
)?], )?],
)) ))
} }

View File

@ -78,7 +78,7 @@ pub(crate) fn replace_universal_newlines(checker: &Checker, call: &ast::ExprCall
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::safe_edit) .map(Fix::safe_edit)
}); });

View File

@ -188,7 +188,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) {
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::safe_edit) .map(Fix::safe_edit)
}); });
@ -206,7 +206,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) {
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::safe_edit) .map(Fix::safe_edit)
}); });
@ -231,7 +231,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) {
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::safe_edit) .map(Fix::safe_edit)
}); });
@ -249,7 +249,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) {
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
.map(Fix::safe_edit) .map(Fix::safe_edit)
}); });

View File

@ -70,7 +70,7 @@ pub(crate) fn useless_class_metaclass_type(checker: &Checker, class_def: &StmtCl
arguments, arguments,
Parentheses::Remove, Parentheses::Remove,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
)?; )?;
let range = edit.range(); let range = edit.range();

View File

@ -73,7 +73,7 @@ pub(crate) fn useless_object_inheritance(checker: &Checker, class_def: &ast::Stm
arguments, arguments,
Parentheses::Remove, Parentheses::Remove,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
)?; )?;
let range = edit.range(); let range = edit.range();

View File

@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -139,12 +139,7 @@ pub(crate) fn yield_in_for_loop(checker: &Checker, stmt_for: &ast::StmtFor) {
let mut diagnostic = checker.report_diagnostic(YieldInForLoop, stmt_for.range()); let mut diagnostic = checker.report_diagnostic(YieldInForLoop, stmt_for.range());
let contents = checker.locator().slice( let contents = checker.locator().slice(
parenthesized_range( parenthesized_range(iter.as_ref().into(), stmt_for.into(), checker.tokens())
iter.as_ref().into(),
stmt_for.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(iter.range()), .unwrap_or(iter.range()),
); );
let contents = if iter.as_tuple_expr().is_some_and(|it| !it.parenthesized) { let contents = if iter.as_tuple_expr().is_some_and(|it| !it.parenthesized) {

View File

@ -1,7 +1,7 @@
use std::borrow::Cow; use std::borrow::Cow;
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
use ruff_python_ast::{self as ast, Expr, name::Name, parenthesize::parenthesized_range}; use ruff_python_ast::{self as ast, Expr, name::Name, token::parenthesized_range};
use ruff_python_codegen::Generator; use ruff_python_codegen::Generator;
use ruff_python_semantic::{BindingId, ResolvedReference, SemanticModel}; use ruff_python_semantic::{BindingId, ResolvedReference, SemanticModel};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -330,12 +330,8 @@ pub(super) fn parenthesize_loop_iter_if_necessary<'a>(
let locator = checker.locator(); let locator = checker.locator();
let iter = for_stmt.iter.as_ref(); let iter = for_stmt.iter.as_ref();
let original_parenthesized_range = parenthesized_range( let original_parenthesized_range =
iter.into(), parenthesized_range(iter.into(), for_stmt.into(), checker.tokens());
for_stmt.into(),
checker.comment_ranges(),
checker.source(),
);
if let Some(range) = original_parenthesized_range { if let Some(range) = original_parenthesized_range {
return Cow::Borrowed(locator.slice(range)); return Cow::Borrowed(locator.slice(range));

View File

@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{ use ruff_python_ast::{
Expr, ExprAttribute, ExprBinOp, ExprCall, ExprStringLiteral, ExprSubscript, ExprUnaryOp, Expr, ExprAttribute, ExprBinOp, ExprCall, ExprStringLiteral, ExprSubscript, ExprUnaryOp,
Number, Operator, PythonVersion, UnaryOp, Number, Operator, PythonVersion, UnaryOp,
@ -112,8 +112,7 @@ pub(crate) fn fromisoformat_replace_z(checker: &Checker, call: &ExprCall) {
let value_full_range = parenthesized_range( let value_full_range = parenthesized_range(
replace_time_zone.date.into(), replace_time_zone.date.into(),
replace_time_zone.parent.into(), replace_time_zone.parent.into(),
checker.comment_ranges(), checker.tokens(),
checker.source(),
) )
.unwrap_or(replace_time_zone.date.range()); .unwrap_or(replace_time_zone.date.range());

View File

@ -5,8 +5,7 @@ use ruff_python_ast as ast;
use ruff_python_ast::Expr; use ruff_python_ast::Expr;
use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::{Tokens, parenthesized_range};
use ruff_python_trivia::CommentRanges;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::Locator; use crate::Locator;
@ -76,8 +75,8 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &Checker, if_expr: &ast::Ex
Edit::range_replacement( Edit::range_replacement(
format!( format!(
"{} or {}", "{} or {}",
parenthesize_test(test, if_expr, checker.comment_ranges(), checker.locator()), parenthesize_test(test, if_expr, checker.tokens(), checker.locator()),
parenthesize_test(orelse, if_expr, checker.comment_ranges(), checker.locator()), parenthesize_test(orelse, if_expr, checker.tokens(), checker.locator()),
), ),
if_expr.range(), if_expr.range(),
), ),
@ -99,15 +98,10 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &Checker, if_expr: &ast::Ex
fn parenthesize_test<'a>( fn parenthesize_test<'a>(
expr: &Expr, expr: &Expr,
if_expr: &ast::ExprIf, if_expr: &ast::ExprIf,
comment_ranges: &CommentRanges, tokens: &Tokens,
locator: &Locator<'a>, locator: &Locator<'a>,
) -> Cow<'a, str> { ) -> Cow<'a, str> {
if let Some(range) = parenthesized_range( if let Some(range) = parenthesized_range(expr.into(), if_expr.into(), tokens) {
expr.into(),
if_expr.into(),
comment_ranges,
locator.contents(),
) {
Cow::Borrowed(locator.slice(range)) Cow::Borrowed(locator.slice(range))
} else if matches!(expr, Expr::If(_) | Expr::Lambda(_) | Expr::Named(_)) { } else if matches!(expr, Expr::If(_) | Expr::Lambda(_) | Expr::Named(_)) {
Cow::Owned(format!("({})", locator.slice(expr.range()))) Cow::Owned(format!("({})", locator.slice(expr.range())))

View File

@ -1,6 +1,6 @@
use ruff_diagnostics::Applicability; use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{Comprehension, Expr, StmtFor}; use ruff_python_ast::{Comprehension, Expr, StmtFor};
use ruff_python_semantic::analyze::typing; use ruff_python_semantic::analyze::typing;
use ruff_python_semantic::analyze::typing::is_io_base_expr; use ruff_python_semantic::analyze::typing::is_io_base_expr;
@ -104,8 +104,7 @@ fn readlines_in_iter(checker: &Checker, iter_expr: &Expr) {
let deletion_range = if let Some(parenthesized_range) = parenthesized_range( let deletion_range = if let Some(parenthesized_range) = parenthesized_range(
expr_attr.value.as_ref().into(), expr_attr.value.as_ref().into(),
expr_attr.into(), expr_attr.into(),
checker.comment_ranges(), checker.tokens(),
checker.source(),
) { ) {
expr_call.range().add_start(parenthesized_range.len()) expr_call.range().add_start(parenthesized_range.len())
} else { } else {

View File

@ -1,7 +1,7 @@
use anyhow::Result; use anyhow::Result;
use ruff_diagnostics::Applicability; use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, Number}; use ruff_python_ast::{self as ast, Expr, Number};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -152,13 +152,8 @@ fn generate_fix(checker: &Checker, call: &ast::ExprCall, base: Base, arg: &Expr)
checker.semantic(), checker.semantic(),
)?; )?;
let arg_range = parenthesized_range( let arg_range =
arg.into(), parenthesized_range(arg.into(), call.into(), checker.tokens()).unwrap_or(arg.range());
call.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(arg.range());
let arg_str = checker.locator().slice(arg_range); let arg_str = checker.locator().slice(arg_range);
Ok(Fix::applicable_edits( Ok(Fix::applicable_edits(

View File

@ -95,7 +95,7 @@ pub(crate) fn single_item_membership_test(
&[membership_test.replacement_op()], &[membership_test.replacement_op()],
std::slice::from_ref(item), std::slice::from_ref(item),
expr.into(), expr.into(),
checker.comment_ranges(), checker.tokens(),
checker.source(), checker.source(),
), ),
expr.range(), expr.range(),

View File

@ -305,6 +305,25 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn range_suppressions() -> Result<()> {
assert_diagnostics_diff!(
Path::new("ruff/suppressions.py"),
&settings::LinterSettings::for_rules(vec![
Rule::UnusedVariable,
Rule::AmbiguousVariableName,
Rule::UnusedNOQA,
]),
&settings::LinterSettings::for_rules(vec![
Rule::UnusedVariable,
Rule::AmbiguousVariableName,
Rule::UnusedNOQA,
])
.with_preview_mode(),
);
Ok(())
}
#[test] #[test]
fn ruf100_0() -> Result<()> { fn ruf100_0() -> Result<()> {
let diagnostics = test_path( let diagnostics = test_path(

View File

@ -163,7 +163,7 @@ fn convert_type_vars(
class_arguments, class_arguments,
Parentheses::Remove, Parentheses::Remove,
source, source,
checker.comment_ranges(), checker.tokens(),
)?; )?;
let replace_type_params = let replace_type_params =
Edit::range_replacement(new_type_params.to_string(), type_params.range); Edit::range_replacement(new_type_params.to_string(), type_params.range);

View File

@ -3,8 +3,8 @@ use anyhow::Result;
use ast::Keyword; use ast::Keyword;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_constant; use ruff_python_ast::helpers::is_constant;
use ruff_python_ast::token::Tokens;
use ruff_python_ast::{self as ast, Expr}; use ruff_python_ast::{self as ast, Expr};
use ruff_python_trivia::CommentRanges;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::Locator; use crate::Locator;
@ -108,9 +108,8 @@ pub(crate) fn default_factory_kwarg(checker: &Checker, call: &ast::ExprCall) {
}, },
call.range(), call.range(),
); );
diagnostic.try_set_fix(|| { diagnostic
convert_to_positional(call, keyword, checker.locator(), checker.comment_ranges()) .try_set_fix(|| convert_to_positional(call, keyword, checker.locator(), checker.tokens()));
});
} }
/// Returns `true` if a value is definitively not callable (e.g., `1` or `[]`). /// Returns `true` if a value is definitively not callable (e.g., `1` or `[]`).
@ -136,7 +135,7 @@ fn convert_to_positional(
call: &ast::ExprCall, call: &ast::ExprCall,
default_factory: &Keyword, default_factory: &Keyword,
locator: &Locator, locator: &Locator,
comment_ranges: &CommentRanges, tokens: &Tokens,
) -> Result<Fix> { ) -> Result<Fix> {
if call.arguments.len() == 1 { if call.arguments.len() == 1 {
// Ex) `defaultdict(default_factory=list)` // Ex) `defaultdict(default_factory=list)`
@ -153,7 +152,7 @@ fn convert_to_positional(
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
locator.contents(), locator.contents(),
comment_ranges, tokens,
)?; )?;
// Second, insert the value as the first positional argument. // Second, insert the value as the first positional argument.

View File

@ -128,7 +128,7 @@ pub(crate) fn falsy_dict_get_fallback(checker: &Checker, expr: &Expr) {
&call.arguments, &call.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.locator().contents(), checker.locator().contents(),
checker.comment_ranges(), checker.tokens(),
) )
.map(|edit| Fix::applicable_edit(edit, applicability)) .map(|edit| Fix::applicable_edit(edit, applicability))
}); });

View File

@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -77,14 +77,7 @@ pub(crate) fn parenthesize_chained_logical_operators(checker: &Checker, expr: &a
) => { ) => {
let locator = checker.locator(); let locator = checker.locator();
let source_range = bool_op.range(); let source_range = bool_op.range();
if parenthesized_range( if parenthesized_range(bool_op.into(), expr.into(), checker.tokens()).is_none() {
bool_op.into(),
expr.into(),
checker.comment_ranges(),
locator.contents(),
)
.is_none()
{
let new_source = format!("({})", locator.slice(source_range)); let new_source = format!("({})", locator.slice(source_range));
let edit = Edit::range_replacement(new_source, source_range); let edit = Edit::range_replacement(new_source, source_range);
checker checker

View File

@ -2,7 +2,7 @@ use anyhow::Context;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_semantic::{Scope, ScopeKind}; use ruff_python_semantic::{Scope, ScopeKind};
use ruff_python_trivia::{indentation_at_offset, textwrap}; use ruff_python_trivia::{indentation_at_offset, textwrap};
use ruff_source_file::LineRanges; use ruff_source_file::LineRanges;
@ -159,8 +159,7 @@ fn use_initvar(
let default_loc = parenthesized_range( let default_loc = parenthesized_range(
default.into(), default.into(),
parameter_with_default.into(), parameter_with_default.into(),
checker.comment_ranges(), checker.tokens(),
checker.source(),
) )
.unwrap_or(default.range()); .unwrap_or(default.range());

View File

@ -2,7 +2,7 @@ use anyhow::Result;
use itertools::Itertools; use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Arguments, Expr}; use ruff_python_ast::{self as ast, Arguments, Expr};
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -116,12 +116,7 @@ fn convert_to_reduce(iterable: &Expr, call: &ast::ExprCall, checker: &Checker) -
)?; )?;
let iterable = checker.locator().slice( let iterable = checker.locator().slice(
parenthesized_range( parenthesized_range(iterable.into(), (&call.arguments).into(), checker.tokens())
iterable.into(),
(&call.arguments).into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(iterable.range()), .unwrap_or(iterable.range()),
); );

View File

@ -1,7 +1,7 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
use ruff_python_ast::token::TokenKind; use ruff_python_ast::token::TokenKind;
use ruff_python_ast::{Expr, ExprCall, parenthesize::parenthesized_range}; use ruff_python_ast::{Expr, ExprCall, token::parenthesized_range};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -124,13 +124,8 @@ fn replace_with_map(starmap: &ExprCall, zip: &ExprCall, checker: &Checker) -> Op
let mut remove_zip = vec![]; let mut remove_zip = vec![];
let full_zip_range = parenthesized_range( let full_zip_range =
zip.into(), parenthesized_range(zip.into(), starmap.into(), checker.tokens()).unwrap_or(zip.range());
starmap.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(zip.range());
// Delete any parentheses around the `zip` call to prevent that the argument turns into a tuple. // Delete any parentheses around the `zip` call to prevent that the argument turns into a tuple.
remove_zip.push(Edit::range_deletion(TextRange::new( remove_zip.push(Edit::range_deletion(TextRange::new(
@ -138,12 +133,7 @@ fn replace_with_map(starmap: &ExprCall, zip: &ExprCall, checker: &Checker) -> Op
zip.start(), zip.start(),
))); )));
let full_zip_func_range = parenthesized_range( let full_zip_func_range = parenthesized_range((&zip.func).into(), zip.into(), checker.tokens())
(&zip.func).into(),
zip.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(zip.func.range()); .unwrap_or(zip.func.range());
// Delete the `zip` callee // Delete the `zip` callee

View File

@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::{Tokens, parenthesized_range};
use ruff_python_ast::{Arguments, Expr, ExprCall}; use ruff_python_ast::{Arguments, Expr, ExprCall};
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
@ -86,6 +86,7 @@ pub(crate) fn unnecessary_cast_to_int(checker: &Checker, call: &ExprCall) {
applicability, applicability,
checker.semantic(), checker.semantic(),
checker.locator(), checker.locator(),
checker.tokens(),
checker.comment_ranges(), checker.comment_ranges(),
checker.source(), checker.source(),
); );
@ -95,27 +96,26 @@ pub(crate) fn unnecessary_cast_to_int(checker: &Checker, call: &ExprCall) {
} }
/// Creates a fix that replaces `int(expression)` with `expression`. /// Creates a fix that replaces `int(expression)` with `expression`.
#[allow(clippy::too_many_arguments)]
fn unwrap_int_expression( fn unwrap_int_expression(
call: &ExprCall, call: &ExprCall,
argument: &Expr, argument: &Expr,
applicability: Applicability, applicability: Applicability,
semantic: &SemanticModel, semantic: &SemanticModel,
locator: &Locator, locator: &Locator,
tokens: &Tokens,
comment_ranges: &CommentRanges, comment_ranges: &CommentRanges,
source: &str, source: &str,
) -> Fix { ) -> Fix {
let content = if let Some(range) = parenthesized_range( let content = if let Some(range) =
argument.into(), parenthesized_range(argument.into(), (&call.arguments).into(), tokens)
(&call.arguments).into(), {
comment_ranges,
source,
) {
locator.slice(range).to_string() locator.slice(range).to_string()
} else { } else {
let parenthesize = semantic.current_expression_parent().is_some() let parenthesize = semantic.current_expression_parent().is_some()
|| argument.is_named_expr() || argument.is_named_expr()
|| locator.count_lines(argument.range()) > 0; || locator.count_lines(argument.range()) > 0;
if parenthesize && !has_own_parentheses(argument, comment_ranges, source) { if parenthesize && !has_own_parentheses(argument, tokens, source) {
format!("({})", locator.slice(argument.range())) format!("({})", locator.slice(argument.range()))
} else { } else {
locator.slice(argument.range()).to_string() locator.slice(argument.range()).to_string()
@ -255,7 +255,7 @@ fn round_applicability(arguments: &Arguments, semantic: &SemanticModel) -> Optio
} }
/// Returns `true` if the given [`Expr`] has its own parentheses (e.g., `()`, `[]`, `{}`). /// Returns `true` if the given [`Expr`] has its own parentheses (e.g., `()`, `[]`, `{}`).
fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str) -> bool { fn has_own_parentheses(expr: &Expr, tokens: &Tokens, source: &str) -> bool {
match expr { match expr {
Expr::ListComp(_) Expr::ListComp(_)
| Expr::SetComp(_) | Expr::SetComp(_)
@ -276,12 +276,8 @@ fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str
// f // f
// (10) // (10)
// ``` // ```
let func_end = parenthesized_range( let func_end =
call_expr.func.as_ref().into(), parenthesized_range(call_expr.func.as_ref().into(), call_expr.into(), tokens)
call_expr.into(),
comment_ranges,
source,
)
.unwrap_or(call_expr.func.range()) .unwrap_or(call_expr.func.range())
.end(); .end();
lines_after_ignoring_trivia(func_end, source) == 0 lines_after_ignoring_trivia(func_end, source) == 0
@ -291,8 +287,7 @@ fn has_own_parentheses(expr: &Expr, comment_ranges: &CommentRanges, source: &str
let subscript_end = parenthesized_range( let subscript_end = parenthesized_range(
subscript_expr.value.as_ref().into(), subscript_expr.value.as_ref().into(),
subscript_expr.into(), subscript_expr.into(),
comment_ranges, tokens,
source,
) )
.unwrap_or(subscript_expr.value.range()) .unwrap_or(subscript_expr.value.range())
.end(); .end();

View File

@ -3,7 +3,7 @@ use ruff_python_ast::{self as ast, BoolOp, CmpOp, Expr};
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -108,21 +108,11 @@ pub(crate) fn unnecessary_key_check(checker: &Checker, expr: &Expr) {
format!( format!(
"{}.get({})", "{}.get({})",
checker.locator().slice( checker.locator().slice(
parenthesized_range( parenthesized_range(obj_right.into(), right.into(), checker.tokens(),)
obj_right.into(),
right.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(obj_right.range()) .unwrap_or(obj_right.range())
), ),
checker.locator().slice( checker.locator().slice(
parenthesized_range( parenthesized_range(key_right.into(), right.into(), checker.tokens(),)
key_right.into(),
right.into(),
checker.comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(key_right.range()) .unwrap_or(key_right.range())
), ),
), ),

View File

@ -2,7 +2,7 @@ use ruff_diagnostics::{Applicability, Edit};
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_empty_f_string; use ruff_python_ast::helpers::is_empty_f_string;
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr}; use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -140,31 +140,19 @@ fn fix_unnecessary_literal_in_deque(
// call. otherwise, we only delete the `iterable` argument and leave the others untouched. // call. otherwise, we only delete the `iterable` argument and leave the others untouched.
let edit = if let Some(maxlen) = maxlen { let edit = if let Some(maxlen) = maxlen {
let deque_name = checker.locator().slice( let deque_name = checker.locator().slice(
parenthesized_range( parenthesized_range(deque.func.as_ref().into(), deque.into(), checker.tokens())
deque.func.as_ref().into(),
deque.into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(deque.func.range()), .unwrap_or(deque.func.range()),
); );
let len_str = checker.locator().slice(maxlen); let len_str = checker.locator().slice(maxlen);
let deque_str = format!("{deque_name}(maxlen={len_str})"); let deque_str = format!("{deque_name}(maxlen={len_str})");
Edit::range_replacement(deque_str, deque.range) Edit::range_replacement(deque_str, deque.range)
} else { } else {
let range = parenthesized_range(
iterable.value().into(),
(&deque.arguments).into(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(iterable.range());
remove_argument( remove_argument(
&range, &iterable,
&deque.arguments, &deque.arguments,
Parentheses::Preserve, Parentheses::Preserve,
checker.source(), checker.source(),
checker.comment_ranges(), checker.tokens(),
)? )?
}; };
let has_comments = checker.comment_ranges().intersects(edit.range()); let has_comments = checker.comment_ranges().intersects(edit.range());

View File

@ -4,7 +4,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::AlwaysFixableViolation; use crate::AlwaysFixableViolation;
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq, Default)]
pub(crate) struct UnusedCodes { pub(crate) struct UnusedCodes {
pub disabled: Vec<String>, pub disabled: Vec<String>,
pub duplicated: Vec<String>, pub duplicated: Vec<String>,
@ -12,6 +12,21 @@ pub(crate) struct UnusedCodes {
pub unmatched: Vec<String>, pub unmatched: Vec<String>,
} }
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum UnusedNOQAKind {
Noqa,
Suppression,
}
impl UnusedNOQAKind {
fn as_str(&self) -> &str {
match self {
UnusedNOQAKind::Noqa => "`noqa` directive",
UnusedNOQAKind::Suppression => "suppression",
}
}
}
/// ## What it does /// ## What it does
/// Checks for `noqa` directives that are no longer applicable. /// Checks for `noqa` directives that are no longer applicable.
/// ///
@ -46,6 +61,7 @@ pub(crate) struct UnusedCodes {
#[violation_metadata(stable_since = "v0.0.155")] #[violation_metadata(stable_since = "v0.0.155")]
pub(crate) struct UnusedNOQA { pub(crate) struct UnusedNOQA {
pub codes: Option<UnusedCodes>, pub codes: Option<UnusedCodes>,
pub kind: UnusedNOQAKind,
} }
impl AlwaysFixableViolation for UnusedNOQA { impl AlwaysFixableViolation for UnusedNOQA {
@ -95,16 +111,20 @@ impl AlwaysFixableViolation for UnusedNOQA {
)); ));
} }
if codes_by_reason.is_empty() { if codes_by_reason.is_empty() {
"Unused `noqa` directive".to_string() format!("Unused {}", self.kind.as_str())
} else { } else {
format!("Unused `noqa` directive ({})", codes_by_reason.join("; ")) format!(
"Unused {} ({})",
self.kind.as_str(),
codes_by_reason.join("; ")
)
} }
} }
None => "Unused blanket `noqa` directive".to_string(), None => format!("Unused blanket {}", self.kind.as_str()),
} }
} }
fn fix_title(&self) -> String { fn fix_title(&self) -> String {
"Remove unused `noqa` directive".to_string() format!("Remove unused {}", self.kind.as_str())
} }
} }

View File

@ -0,0 +1,451 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 14
Added: 11
--- Removed ---
E741 Ambiguous variable name: `I`
--> suppressions.py:4:5
|
2 | # These should both be ignored by the range suppression.
3 | # ruff: disable[E741, F841]
4 | I = 1
| ^
5 | # ruff: enable[E741, F841]
|
F841 [*] Local variable `I` is assigned to but never used
--> suppressions.py:4:5
|
2 | # These should both be ignored by the range suppression.
3 | # ruff: disable[E741, F841]
4 | I = 1
| ^
5 | # ruff: enable[E741, F841]
|
help: Remove assignment to unused variable `I`
1 | def f():
2 | # These should both be ignored by the range suppression.
3 | # ruff: disable[E741, F841]
- I = 1
4 + pass
5 | # ruff: enable[E741, F841]
6 |
7 |
note: This is an unsafe fix and may change runtime behavior
E741 Ambiguous variable name: `I`
--> suppressions.py:12:5
|
10 | # Should also generate an "unmatched suppression" warning.
11 | # ruff:disable[E741,F841]
12 | I = 1
| ^
|
F841 [*] Local variable `I` is assigned to but never used
--> suppressions.py:12:5
|
10 | # Should also generate an "unmatched suppression" warning.
11 | # ruff:disable[E741,F841]
12 | I = 1
| ^
|
help: Remove assignment to unused variable `I`
9 | # These should both be ignored by the implicit range suppression.
10 | # Should also generate an "unmatched suppression" warning.
11 | # ruff:disable[E741,F841]
- I = 1
12 + pass
13 |
14 |
15 | def f():
note: This is an unsafe fix and may change runtime behavior
E741 Ambiguous variable name: `I`
--> suppressions.py:26:5
|
24 | # the other logged to the user.
25 | # ruff: disable[E741]
26 | I = 1
| ^
27 | # ruff: enable[E741]
|
E741 Ambiguous variable name: `l`
--> suppressions.py:35:5
|
33 | # middle line should be completely silenced.
34 | # ruff: disable[E741]
35 | l = 0
| ^
36 | # ruff: disable[F841]
37 | O = 1
|
E741 Ambiguous variable name: `O`
--> suppressions.py:37:5
|
35 | l = 0
36 | # ruff: disable[F841]
37 | O = 1
| ^
38 | # ruff: enable[E741]
39 | I = 2
|
F841 [*] Local variable `O` is assigned to but never used
--> suppressions.py:37:5
|
35 | l = 0
36 | # ruff: disable[F841]
37 | O = 1
| ^
38 | # ruff: enable[E741]
39 | I = 2
|
help: Remove assignment to unused variable `O`
34 | # ruff: disable[E741]
35 | l = 0
36 | # ruff: disable[F841]
- O = 1
37 | # ruff: enable[E741]
38 | I = 2
39 | # ruff: enable[F841]
note: This is an unsafe fix and may change runtime behavior
F841 [*] Local variable `I` is assigned to but never used
--> suppressions.py:39:5
|
37 | O = 1
38 | # ruff: enable[E741]
39 | I = 2
| ^
40 | # ruff: enable[F841]
|
help: Remove assignment to unused variable `I`
36 | # ruff: disable[F841]
37 | O = 1
38 | # ruff: enable[E741]
- I = 2
39 | # ruff: enable[F841]
40 |
41 |
note: This is an unsafe fix and may change runtime behavior
F841 [*] Local variable `foo` is assigned to but never used
--> suppressions.py:62:5
|
60 | # TODO: Duplicate codes should be counted as duplicate, not unused
61 | # ruff: disable[F841, F841]
62 | foo = 0
| ^^^
|
help: Remove assignment to unused variable `foo`
59 | def f():
60 | # TODO: Duplicate codes should be counted as duplicate, not unused
61 | # ruff: disable[F841, F841]
- foo = 0
62 + pass
63 |
64 |
65 | def f():
note: This is an unsafe fix and may change runtime behavior
F841 [*] Local variable `foo` is assigned to but never used
--> suppressions.py:70:5
|
68 | # ruff: disable[F841]
69 | # ruff: disable[F841]
70 | foo = 0
| ^^^
|
help: Remove assignment to unused variable `foo`
67 | # and the other should trigger an unused suppression diagnostic
68 | # ruff: disable[F841]
69 | # ruff: disable[F841]
- foo = 0
70 + pass
71 |
72 |
73 | def f():
note: This is an unsafe fix and may change runtime behavior
F841 [*] Local variable `foo` is assigned to but never used
--> suppressions.py:76:5
|
74 | # Multiple codes but only one is used
75 | # ruff: disable[E741, F401, F841]
76 | foo = 0
| ^^^
|
help: Remove assignment to unused variable `foo`
73 | def f():
74 | # Multiple codes but only one is used
75 | # ruff: disable[E741, F401, F841]
- foo = 0
76 + pass
77 |
78 |
79 | def f():
note: This is an unsafe fix and may change runtime behavior
E741 Ambiguous variable name: `I`
--> suppressions.py:82:5
|
80 | # Multiple codes but only two are used
81 | # ruff: disable[E741, F401, F841]
82 | I = 0
| ^
|
F841 [*] Local variable `I` is assigned to but never used
--> suppressions.py:82:5
|
80 | # Multiple codes but only two are used
81 | # ruff: disable[E741, F401, F841]
82 | I = 0
| ^
|
help: Remove assignment to unused variable `I`
79 | def f():
80 | # Multiple codes but only two are used
81 | # ruff: disable[E741, F401, F841]
- I = 0
82 + pass
83 |
84 |
85 | def f():
note: This is an unsafe fix and may change runtime behavior
--- Added ---
RUF100 [*] Unused suppression (non-enabled: `E501`)
--> suppressions.py:46:5
|
44 | # Neither of these are ignored and warnings are
45 | # logged to user
46 | # ruff: disable[E501]
| ^^^^^^^^^^^^^^^^^^^^^
47 | I = 1
48 | # ruff: enable[E501]
|
help: Remove unused suppression
43 | def f():
44 | # Neither of these are ignored and warnings are
45 | # logged to user
- # ruff: disable[E501]
46 | I = 1
47 | # ruff: enable[E501]
48 |
RUF100 [*] Unused suppression (non-enabled: `E501`)
--> suppressions.py:48:5
|
46 | # ruff: disable[E501]
47 | I = 1
48 | # ruff: enable[E501]
| ^^^^^^^^^^^^^^^^^^^^
|
help: Remove unused suppression
45 | # logged to user
46 | # ruff: disable[E501]
47 | I = 1
- # ruff: enable[E501]
48 |
49 |
50 | def f():
RUF100 [*] Unused `noqa` directive (unused: `E741`, `F841`)
--> suppressions.py:55:12
|
53 | # and an unusued noqa diagnostic should be logged.
54 | # ruff:disable[E741,F841]
55 | I = 1 # noqa: E741,F841
| ^^^^^^^^^^^^^^^^^
56 | # ruff:enable[E741,F841]
|
help: Remove unused `noqa` directive
52 | # These should both be ignored by the range suppression,
53 | # and an unusued noqa diagnostic should be logged.
54 | # ruff:disable[E741,F841]
- I = 1 # noqa: E741,F841
55 + I = 1
56 | # ruff:enable[E741,F841]
57 |
58 |
RUF100 [*] Unused suppression (unused: `F841`)
--> suppressions.py:61:21
|
59 | def f():
60 | # TODO: Duplicate codes should be counted as duplicate, not unused
61 | # ruff: disable[F841, F841]
| ^^^^
62 | foo = 0
|
help: Remove unused suppression
58 |
59 | def f():
60 | # TODO: Duplicate codes should be counted as duplicate, not unused
- # ruff: disable[F841, F841]
61 + # ruff: disable[F841]
62 | foo = 0
63 |
64 |
RUF100 [*] Unused suppression (unused: `F841`)
--> suppressions.py:69:5
|
67 | # and the other should trigger an unused suppression diagnostic
68 | # ruff: disable[F841]
69 | # ruff: disable[F841]
| ^^^^^^^^^^^^^^^^^^^^^
70 | foo = 0
|
help: Remove unused suppression
66 | # Overlapping range suppressions, one should be marked as used,
67 | # and the other should trigger an unused suppression diagnostic
68 | # ruff: disable[F841]
- # ruff: disable[F841]
69 | foo = 0
70 |
71 |
RUF100 [*] Unused suppression (unused: `E741`)
--> suppressions.py:75:21
|
73 | def f():
74 | # Multiple codes but only one is used
75 | # ruff: disable[E741, F401, F841]
| ^^^^
76 | foo = 0
|
help: Remove unused suppression
72 |
73 | def f():
74 | # Multiple codes but only one is used
- # ruff: disable[E741, F401, F841]
75 + # ruff: disable[F401, F841]
76 | foo = 0
77 |
78 |
RUF100 [*] Unused suppression (non-enabled: `F401`)
--> suppressions.py:75:27
|
73 | def f():
74 | # Multiple codes but only one is used
75 | # ruff: disable[E741, F401, F841]
| ^^^^
76 | foo = 0
|
help: Remove unused suppression
72 |
73 | def f():
74 | # Multiple codes but only one is used
- # ruff: disable[E741, F401, F841]
75 + # ruff: disable[E741, F841]
76 | foo = 0
77 |
78 |
RUF100 [*] Unused suppression (non-enabled: `F401`)
--> suppressions.py:81:27
|
79 | def f():
80 | # Multiple codes but only two are used
81 | # ruff: disable[E741, F401, F841]
| ^^^^
82 | I = 0
|
help: Remove unused suppression
78 |
79 | def f():
80 | # Multiple codes but only two are used
- # ruff: disable[E741, F401, F841]
81 + # ruff: disable[E741, F841]
82 | I = 0
83 |
84 |
RUF100 [*] Unused suppression (unused: `E741`)
--> suppressions.py:87:21
|
85 | def f():
86 | # Multiple codes but none are used
87 | # ruff: disable[E741, F401, F841]
| ^^^^
88 | print("hello")
|
help: Remove unused suppression
84 |
85 | def f():
86 | # Multiple codes but none are used
- # ruff: disable[E741, F401, F841]
87 + # ruff: disable[F401, F841]
88 | print("hello")
RUF100 [*] Unused suppression (non-enabled: `F401`)
--> suppressions.py:87:27
|
85 | def f():
86 | # Multiple codes but none are used
87 | # ruff: disable[E741, F401, F841]
| ^^^^
88 | print("hello")
|
help: Remove unused suppression
84 |
85 | def f():
86 | # Multiple codes but none are used
- # ruff: disable[E741, F401, F841]
87 + # ruff: disable[E741, F841]
88 | print("hello")
RUF100 [*] Unused suppression (unused: `F841`)
--> suppressions.py:87:33
|
85 | def f():
86 | # Multiple codes but none are used
87 | # ruff: disable[E741, F401, F841]
| ^^^^
88 | print("hello")
|
help: Remove unused suppression
84 |
85 | def f():
86 | # Multiple codes but none are used
- # ruff: disable[E741, F401, F841]
87 + # ruff: disable[E741, F401]
88 | print("hello")

View File

@ -465,6 +465,12 @@ impl LinterSettings {
self self
} }
#[must_use]
pub fn with_preview_mode(mut self) -> Self {
self.preview = PreviewMode::Enabled;
self
}
/// Resolve the [`TargetVersion`] to use for linting. /// Resolve the [`TargetVersion`] to use for linting.
/// ///
/// This method respects the per-file version overrides in /// This method respects the per-file version overrides in

View File

@ -1,7 +1,10 @@
use compact_str::CompactString; use compact_str::CompactString;
use core::fmt; use core::fmt;
use ruff_db::diagnostic::Diagnostic;
use ruff_diagnostics::{Edit, Fix};
use ruff_python_ast::token::{TokenKind, Tokens}; use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_ast::whitespace::indentation; use ruff_python_ast::whitespace::indentation;
use std::cell::Cell;
use std::{error::Error, fmt::Formatter}; use std::{error::Error, fmt::Formatter};
use thiserror::Error; use thiserror::Error;
@ -9,7 +12,14 @@ use ruff_python_trivia::Cursor;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize, TextSlice}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize, TextSlice};
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
#[allow(unused)] use crate::Locator;
use crate::checkers::ast::LintContext;
use crate::codes::Rule;
use crate::fix::edits::delete_comment;
use crate::preview::is_range_suppressions_enabled;
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA, UnusedNOQAKind};
use crate::settings::LinterSettings;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
enum SuppressionAction { enum SuppressionAction {
Disable, Disable,
@ -31,7 +41,6 @@ pub(crate) struct SuppressionComment {
reason: TextRange, reason: TextRange,
} }
#[allow(unused)]
impl SuppressionComment { impl SuppressionComment {
/// Return the suppressed codes as strings /// Return the suppressed codes as strings
fn codes_as_str<'src>(&self, source: &'src str) -> impl Iterator<Item = &'src str> { fn codes_as_str<'src>(&self, source: &'src str) -> impl Iterator<Item = &'src str> {
@ -48,7 +57,6 @@ pub(crate) struct PendingSuppressionComment<'a> {
comment: SuppressionComment, comment: SuppressionComment,
} }
#[allow(unused)]
impl PendingSuppressionComment<'_> { impl PendingSuppressionComment<'_> {
/// Whether the comment "matches" another comment, based on indentation and suppressed codes /// Whether the comment "matches" another comment, based on indentation and suppressed codes
/// Expects a "forward search" for matches, ie, will only match if the current comment is a /// Expects a "forward search" for matches, ie, will only match if the current comment is a
@ -64,8 +72,7 @@ impl PendingSuppressionComment<'_> {
} }
} }
#[allow(unused)] #[derive(Debug)]
#[derive(Clone, Debug)]
pub(crate) struct Suppression { pub(crate) struct Suppression {
/// The lint code being suppressed /// The lint code being suppressed
code: CompactString, code: CompactString,
@ -75,9 +82,11 @@ pub(crate) struct Suppression {
/// Any comments associated with the suppression /// Any comments associated with the suppression
comments: SmallVec<[SuppressionComment; 2]>, comments: SmallVec<[SuppressionComment; 2]>,
/// Whether this suppression actually suppressed a diagnostic
used: Cell<bool>,
} }
#[allow(unused)]
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub(crate) enum InvalidSuppressionKind { pub(crate) enum InvalidSuppressionKind {
/// Trailing suppression not supported /// Trailing suppression not supported
@ -98,8 +107,8 @@ pub(crate) struct InvalidSuppression {
} }
#[allow(unused)] #[allow(unused)]
#[derive(Debug)] #[derive(Debug, Default)]
pub(crate) struct Suppressions { pub struct Suppressions {
/// Valid suppression ranges with associated comments /// Valid suppression ranges with associated comments
valid: Vec<Suppression>, valid: Vec<Suppression>,
@ -110,11 +119,121 @@ pub(crate) struct Suppressions {
errors: Vec<ParseError>, errors: Vec<ParseError>,
} }
#[allow(unused)]
impl Suppressions { impl Suppressions {
pub(crate) fn from_tokens(source: &str, tokens: &Tokens) -> Suppressions { pub fn from_tokens(settings: &LinterSettings, source: &str, tokens: &Tokens) -> Suppressions {
if is_range_suppressions_enabled(settings) {
let builder = SuppressionsBuilder::new(source); let builder = SuppressionsBuilder::new(source);
builder.load_from_tokens(tokens) builder.load_from_tokens(tokens)
} else {
Suppressions::default()
}
}
pub(crate) fn is_empty(&self) -> bool {
self.valid.is_empty()
}
/// Check if a diagnostic is suppressed by any known range suppressions
pub(crate) fn check_diagnostic(&self, diagnostic: &Diagnostic) -> bool {
if self.valid.is_empty() {
return false;
}
let Some(code) = diagnostic.secondary_code() else {
return false;
};
let Some(span) = diagnostic.primary_span() else {
return false;
};
let Some(range) = span.range() else {
return false;
};
for suppression in &self.valid {
if *code == suppression.code.as_str() && suppression.range.contains_range(range) {
suppression.used.set(true);
return true;
}
}
false
}
pub(crate) fn check_suppressions(&self, context: &LintContext, locator: &Locator) {
if !context.any_rule_enabled(&[Rule::UnusedNOQA, Rule::InvalidRuleCode]) {
return;
}
let unused = self
.valid
.iter()
.filter(|suppression| !suppression.used.get());
for suppression in unused {
let Ok(rule) = Rule::from_code(&suppression.code) else {
continue; // TODO: invalid code
};
for comment in &suppression.comments {
let mut range = comment.range;
let edit = if comment.codes.len() == 1 {
delete_comment(comment.range, locator)
} else {
let code_index = comment
.codes
.iter()
.position(|range| locator.slice(range) == suppression.code)
.unwrap();
range = comment.codes[code_index];
let code_range = if code_index < (comment.codes.len() - 1) {
TextRange::new(
comment.codes[code_index].start(),
comment.codes[code_index + 1].start(),
)
} else {
TextRange::new(
comment.codes[code_index - 1].end(),
comment.codes[code_index].end(),
)
};
Edit::range_deletion(code_range)
};
let codes = if context.is_rule_enabled(rule) {
UnusedCodes {
unmatched: vec![suppression.code.to_string()],
..Default::default()
}
} else {
UnusedCodes {
disabled: vec![suppression.code.to_string()],
..Default::default()
}
};
let mut diagnostic = context.report_diagnostic(
UnusedNOQA {
codes: Some(codes),
kind: UnusedNOQAKind::Suppression,
},
range,
);
diagnostic.set_fix(Fix::safe_edit(edit));
}
}
for error in self
.errors
.iter()
.filter(|error| error.kind == ParseErrorKind::MissingCodes)
{
let mut diagnostic = context.report_diagnostic(
UnusedNOQA {
codes: Some(UnusedCodes::default()),
kind: UnusedNOQAKind::Suppression,
},
error.range,
);
diagnostic.set_fix(Fix::safe_edit(delete_comment(error.range, locator)));
}
} }
} }
@ -240,6 +359,7 @@ impl<'a> SuppressionsBuilder<'a> {
code: code.into(), code: code.into(),
range: combined_range, range: combined_range,
comments: smallvec![comment.comment.clone(), other.comment.clone()], comments: smallvec![comment.comment.clone(), other.comment.clone()],
used: false.into(),
}); });
} }
@ -256,6 +376,7 @@ impl<'a> SuppressionsBuilder<'a> {
code: code.into(), code: code.into(),
range: implicit_range, range: implicit_range,
comments: smallvec![comment.comment.clone()], comments: smallvec![comment.comment.clone()],
used: false.into(),
}); });
} }
self.pending.remove(comment_index); self.pending.remove(comment_index);
@ -369,8 +490,10 @@ impl<'src> SuppressionParser<'src> {
} else if self.cursor.as_str().starts_with("enable") { } else if self.cursor.as_str().starts_with("enable") {
self.cursor.skip_bytes("enable".len()); self.cursor.skip_bytes("enable".len());
Ok(SuppressionAction::Enable) Ok(SuppressionAction::Enable)
} else if self.cursor.as_str().starts_with("noqa") { } else if self.cursor.as_str().starts_with("noqa")
// file-level "noqa" variant, ignore for now || self.cursor.as_str().starts_with("isort")
{
// alternate suppression variants, ignore for now
self.error(ParseErrorKind::NotASuppression) self.error(ParseErrorKind::NotASuppression)
} else { } else {
self.error(ParseErrorKind::UnknownAction) self.error(ParseErrorKind::UnknownAction)
@ -457,9 +580,12 @@ mod tests {
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use similar::DiffableStr; use similar::DiffableStr;
use crate::suppression::{ use crate::{
settings::LinterSettings,
suppression::{
InvalidSuppression, ParseError, Suppression, SuppressionAction, SuppressionComment, InvalidSuppression, ParseError, Suppression, SuppressionAction, SuppressionComment,
SuppressionParser, Suppressions, SuppressionParser, Suppressions,
},
}; };
#[test] #[test]
@ -1376,7 +1502,11 @@ def bar():
/// Parse all suppressions and errors in a module for testing /// Parse all suppressions and errors in a module for testing
fn debug(source: &'_ str) -> DebugSuppressions<'_> { fn debug(source: &'_ str) -> DebugSuppressions<'_> {
let parsed = parse(source, ParseOptions::from(Mode::Module)).unwrap(); let parsed = parse(source, ParseOptions::from(Mode::Module)).unwrap();
let suppressions = Suppressions::from_tokens(source, parsed.tokens()); let suppressions = Suppressions::from_tokens(
&LinterSettings::default().with_preview_mode(),
source,
parsed.tokens(),
);
DebugSuppressions { DebugSuppressions {
source, source,
suppressions, suppressions,

View File

@ -32,6 +32,7 @@ use crate::packaging::detect_package_root;
use crate::settings::types::UnsafeFixes; use crate::settings::types::UnsafeFixes;
use crate::settings::{LinterSettings, flags}; use crate::settings::{LinterSettings, flags};
use crate::source_kind::SourceKind; use crate::source_kind::SourceKind;
use crate::suppression::Suppressions;
use crate::{Applicability, FixAvailability}; use crate::{Applicability, FixAvailability};
use crate::{Locator, directives}; use crate::{Locator, directives};
@ -234,6 +235,7 @@ pub(crate) fn test_contents<'a>(
&locator, &locator,
&indexer, &indexer,
); );
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
let messages = check_path( let messages = check_path(
path, path,
path.parent() path.parent()
@ -249,6 +251,7 @@ pub(crate) fn test_contents<'a>(
source_type, source_type,
&parsed, &parsed,
target_version, target_version,
&suppressions,
); );
let source_has_errors = parsed.has_invalid_syntax(); let source_has_errors = parsed.has_invalid_syntax();
@ -299,6 +302,8 @@ pub(crate) fn test_contents<'a>(
&indexer, &indexer,
); );
let suppressions =
Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
let fixed_messages = check_path( let fixed_messages = check_path(
path, path,
None, None,
@ -312,6 +317,7 @@ pub(crate) fn test_contents<'a>(
source_type, source_type,
&parsed, &parsed,
target_version, target_version,
&suppressions,
); );
if parsed.has_invalid_syntax() && !source_has_errors { if parsed.has_invalid_syntax() && !source_has_errors {

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