diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B028.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B028.py index a2915f29adf08..45911df3942f4 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B028.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B028.py @@ -9,3 +9,9 @@ warnings.warn(DeprecationWarning("test"), source=None) warnings.warn(DeprecationWarning("test"), source=None, stacklevel=2) warnings.warn(DeprecationWarning("test"), stacklevel=1) + +warnings.warn( + DeprecationWarning("test"), + # some comments here + source = None # no trailing comma + ) diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs index 65e88e1d35465..d0f181770e3f3 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::{self as ast}; use ruff_text_size::Ranged; -use crate::checkers::ast::Checker; +use crate::{checkers::ast::Checker, fix::edits::add_argument}; /// ## What it does /// Checks for `warnings.warn` calls without an explicit `stacklevel` keyword @@ -28,16 +28,26 @@ use crate::checkers::ast::Checker; /// warnings.warn("This is a warning", stacklevel=2) /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe because it changes +/// the behavior of the code. Moreover, the fix will assign +/// a stacklevel of 2, while the user may wish to assign a +/// higher stacklevel to address the diagnostic. +/// /// ## References /// - [Python documentation: `warnings.warn`](https://docs.python.org/3/library/warnings.html#warnings.warn) #[derive(ViolationMetadata)] pub(crate) struct NoExplicitStacklevel; -impl Violation for NoExplicitStacklevel { +impl AlwaysFixableViolation for NoExplicitStacklevel { #[derive_message_formats] fn message(&self) -> String { "No explicit `stacklevel` keyword argument found".to_string() } + + fn fix_title(&self) -> String { + "Set `stacklevel=2`".to_string() + } } /// B028 @@ -53,8 +63,16 @@ pub(crate) fn no_explicit_stacklevel(checker: &mut Checker, call: &ast::ExprCall if call.arguments.find_keyword("stacklevel").is_some() { return; } + let mut diagnostic = Diagnostic::new(NoExplicitStacklevel, call.func.range()); + + let edit = add_argument( + "stacklevel=2", + &call.arguments, + checker.comment_ranges(), + checker.locator().contents(), + ); + + diagnostic.set_fix(Fix::unsafe_edit(edit)); - checker - .diagnostics - .push(Diagnostic::new(NoExplicitStacklevel, call.func.range())); + checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B028_B028.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B028_B028.py.snap index 081910662ff8d..e9752d9da32ad 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B028_B028.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B028_B028.py.snap @@ -1,8 +1,7 @@ --- source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs -snapshot_kind: text --- -B028.py:8:1: B028 No explicit `stacklevel` keyword argument found +B028.py:8:1: B028 [*] No explicit `stacklevel` keyword argument found | 6 | """ 7 | @@ -11,8 +10,19 @@ B028.py:8:1: B028 No explicit `stacklevel` keyword argument found 9 | warnings.warn(DeprecationWarning("test"), source=None) 10 | warnings.warn(DeprecationWarning("test"), source=None, stacklevel=2) | + = help: Set `stacklevel=2` -B028.py:9:1: B028 No explicit `stacklevel` keyword argument found +ℹ Unsafe fix +5 5 | B028 - on lines 8 and 9 +6 6 | """ +7 7 | +8 |-warnings.warn(DeprecationWarning("test")) + 8 |+warnings.warn(DeprecationWarning("test"), stacklevel=2) +9 9 | warnings.warn(DeprecationWarning("test"), source=None) +10 10 | warnings.warn(DeprecationWarning("test"), source=None, stacklevel=2) +11 11 | warnings.warn(DeprecationWarning("test"), stacklevel=1) + +B028.py:9:1: B028 [*] No explicit `stacklevel` keyword argument found | 8 | warnings.warn(DeprecationWarning("test")) 9 | warnings.warn(DeprecationWarning("test"), source=None) @@ -20,3 +30,34 @@ B028.py:9:1: B028 No explicit `stacklevel` keyword argument found 10 | warnings.warn(DeprecationWarning("test"), source=None, stacklevel=2) 11 | warnings.warn(DeprecationWarning("test"), stacklevel=1) | + = help: Set `stacklevel=2` + +ℹ Unsafe fix +6 6 | """ +7 7 | +8 8 | warnings.warn(DeprecationWarning("test")) +9 |-warnings.warn(DeprecationWarning("test"), source=None) +10 9 | warnings.warn(DeprecationWarning("test"), source=None, stacklevel=2) + 10 |+warnings.warn(DeprecationWarning("test"), source=None, stacklevel=2) +11 11 | warnings.warn(DeprecationWarning("test"), stacklevel=1) +12 12 | +13 13 | warnings.warn( + +B028.py:13:1: B028 [*] No explicit `stacklevel` keyword argument found + | +11 | warnings.warn(DeprecationWarning("test"), stacklevel=1) +12 | +13 | warnings.warn( + | ^^^^^^^^^^^^^ B028 +14 | DeprecationWarning("test"), +15 | # some comments here + | + = help: Set `stacklevel=2` + +ℹ Unsafe fix +13 13 | warnings.warn( +14 14 | DeprecationWarning("test"), +15 15 | # some comments here +16 |- source = None # no trailing comma + 16 |+ source = None, stacklevel=2 # no trailing comma +17 17 | )