diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B004.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B004.py index 83590228a6..3a81f8c28b 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B004.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B004.py @@ -29,3 +29,13 @@ def this_is_fine(): o = object() if callable(o): print("Ooh, this is actually callable.") + +# https://github.com/astral-sh/ruff/issues/18741 +# The autofix for this is unsafe due to the comments. +hasattr( + # comment 1 + obj, # comment 2 + # comment 3 + "__call__", # comment 4 + # comment 5 +) diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs index 5bfaefe079..fbeea00d97 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs @@ -1,3 +1,4 @@ +use ruff_diagnostics::Applicability; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; @@ -26,6 +27,21 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// callable(obj) /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe if there's comments in the `hasattr` call +/// expression, as comments may be removed. +/// +/// For example, the fix would be marked as unsafe in the following case: +/// ```python +/// hasattr( +/// # comment 1 +/// obj, # comment 2 +/// # comment 3 +/// "__call__", # comment 4 +/// # comment 5 +/// ) +/// ``` +/// /// ## References /// - [Python documentation: `callable`](https://docs.python.org/3/library/functions.html#callable) /// - [Python documentation: `hasattr`](https://docs.python.org/3/library/functions.html#hasattr) @@ -84,7 +100,15 @@ pub(crate) fn unreliable_callable_check( format!("{binding}({})", checker.locator().slice(obj)), expr.range(), ); - Ok(Fix::safe_edits(binding_edit, import_edit)) + Ok(Fix::applicable_edits( + binding_edit, + import_edit, + if checker.comment_ranges().intersects(expr.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }, + )) }); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B004_B004.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B004_B004.py.snap index 2d67212e68..1ef53a053b 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B004_B004.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B004_B004.py.snap @@ -85,4 +85,32 @@ B004.py:24:8: B004 [*] Using `hasattr(x, "__call__")` to test if x is callable i 25 |+ if builtins.callable(o): 25 26 | print("STILL a bug!") 26 27 | -27 28 | +27 28 | + +B004.py:35:1: B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results. + | +33 | # https://github.com/astral-sh/ruff/issues/18741 +34 | # The autofix for this is unsafe due to the comments. +35 | / hasattr( +36 | | # comment 1 +37 | | obj, # comment 2 +38 | | # comment 3 +39 | | "__call__", # comment 4 +40 | | # comment 5 +41 | | ) + | |_^ B004 + | + = help: Replace with `callable()` + +ℹ Unsafe fix +32 32 | +33 33 | # https://github.com/astral-sh/ruff/issues/18741 +34 34 | # The autofix for this is unsafe due to the comments. +35 |-hasattr( +36 |- # comment 1 +37 |- obj, # comment 2 +38 |- # comment 3 +39 |- "__call__", # comment 4 +40 |- # comment 5 +41 |-) + 35 |+callable(obj)