[flake8-bugbear] Make fix unsafe if it deletes comments (B014) (#22659)

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

<!-- How was it tested? -->
This commit is contained in:
chiri
2026-01-19 21:09:39 +03:00
committed by GitHub
parent 230e455e93
commit a49d8af14c
3 changed files with 73 additions and 18 deletions

View File

@@ -88,3 +88,14 @@ try:
pas
except(re.error, re.error):
p
try:
pass
except (
ValueError,
ValueError,
# text
TypeError,
):
pass

View File

@@ -1,10 +1,10 @@
use itertools::Itertools;
use rustc_hash::{FxHashMap, FxHashSet};
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::name::UnqualifiedName;
use ruff_python_ast::{self as ast, ExceptHandler, Expr, ExprContext};
use ruff_text_size::{Ranged, TextRange};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::checkers::ast::Checker;
use crate::fix::edits::pad;
@@ -37,6 +37,9 @@ use crate::{Edit, Fix};
/// ...
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as safe, unless the exception handler contains comments.
///
/// ## References
/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause)
#[derive(ViolationMetadata)]
@@ -154,22 +157,32 @@ fn duplicate_handler_exceptions<'a>(
},
expr.range(),
);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
// Single exceptions don't require parentheses, but since we're _removing_
// parentheses, insert whitespace as needed.
if let [elt] = unique_elts.as_slice() {
pad(
checker.generator().expr(elt),
expr.range(),
checker.locator(),
)
} else {
// Multiple exceptions must always be parenthesized. This is done
// manually as the generator never parenthesizes lone tuples.
format!("({})", checker.generator().expr(&type_pattern(unique_elts)))
},
expr.range(),
)));
let applicability = if checker.comment_ranges().intersects(expr.range()) {
Applicability::Unsafe
} else {
Applicability::Safe
};
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_replacement(
// Single exceptions don't require parentheses, but since we're _removing_
// parentheses, insert whitespace as needed.
if let [elt] = unique_elts.as_slice() {
pad(
checker.generator().expr(elt),
expr.range(),
checker.locator(),
)
} else {
// Multiple exceptions must always be parenthesized. This is done
// manually as the generator never parenthesizes lone tuples.
format!("({})", checker.generator().expr(&type_pattern(unique_elts)))
},
expr.range(),
),
applicability,
));
}
}

View File

@@ -96,3 +96,34 @@ help: De-duplicate exceptions
- except(re.error, re.error):
89 + except re.error:
90 | p
91 |
92 |
B014 [*] Exception handler with duplicate exception: `ValueError`
--> B014.py:95:8
|
93 | try:
94 | pass
95 | except (
| ________^
96 | | ValueError,
97 | | ValueError,
98 | | # text
99 | | TypeError,
100 | | ):
| |_^
101 | pass
|
help: De-duplicate exceptions
92 |
93 | try:
94 | pass
- except (
- ValueError,
- ValueError,
- # text
- TypeError,
- ):
95 + except (ValueError, TypeError):
96 | pass
note: This is an unsafe fix and may change runtime behavior