mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-19 20:24:27 +00:00
[refurb] Fix false negative for underscores before sign in Decimal constructor (FURB157) (#21190)
## Summary
Fixes FURB157 false negative where `Decimal("_-1")` was not flagged as
verbose when underscores precede the sign character. This fixes #21186.
## Problem Analysis
The `verbose-decimal-constructor` (FURB157) rule failed to detect
verbose `Decimal` constructors when the sign character (`+` or `-`) was
preceded by underscores. For example, `Decimal("_-1")` was not flagged,
even though it can be simplified to `Decimal(-1)`.
The bug occurred because the rule checked for the sign character at the
start of the string before stripping leading underscores. According to
Python's `Decimal` parser behavior (as documented in CPython's
`_pydecimal.py`), underscores are removed before parsing the sign. The
rule's logic didn't match this behavior, causing a false negative for
cases like `"_-1"` where the underscore came before the sign.
This was a regression introduced in version 0.14.3, as these cases were
correctly flagged in version 0.14.2.
## Approach
The fix updates the sign extraction logic to:
1. Strip leading underscores first (matching Python's Decimal parser
behavior)
2. Extract the sign from the underscore-stripped string
3. Preserve the string after the sign for normalization purposes
This ensures that cases like `Decimal("_-1")`, `Decimal("_+1")`, and
`Decimal("_-1_000")` are correctly detected and flagged. The
normalization logic was also updated to use the string after the sign
(without underscores) to avoid double signs in the replacement output.
This commit is contained in:
parent
2e7ab00d51
commit
47e41ac6b6
3 changed files with 76 additions and 6 deletions
|
|
@ -85,3 +85,9 @@ Decimal("1234_5678") # Safe fix: preserves non-thousands separators
|
|||
Decimal("0001_2345")
|
||||
Decimal("000_1_2345")
|
||||
Decimal("000_000")
|
||||
|
||||
# Test cases for underscores before sign
|
||||
# https://github.com/astral-sh/ruff/issues/21186
|
||||
Decimal("_-1") # Should flag as verbose
|
||||
Decimal("_+1") # Should flag as verbose
|
||||
Decimal("_-1_000") # Should flag as verbose
|
||||
|
|
|
|||
|
|
@ -93,16 +93,21 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal
|
|||
// https://github.com/python/cpython/blob/ac556a2ad1213b8bb81372fe6fb762f5fcb076de/Lib/_pydecimal.py#L6060-L6077
|
||||
// _after_ trimming whitespace from the string and removing all occurrences of "_".
|
||||
let original_str = str_literal.to_str().trim_whitespace();
|
||||
// Strip leading underscores before extracting the sign, as Python's Decimal parser
|
||||
// removes underscores before parsing the sign.
|
||||
let sign_check_str = original_str.trim_start_matches('_');
|
||||
// Extract the unary sign, if any.
|
||||
let (unary, original_str) = if let Some(trimmed) = original_str.strip_prefix('+') {
|
||||
let (unary, sign_check_str) = if let Some(trimmed) = sign_check_str.strip_prefix('+') {
|
||||
("+", trimmed)
|
||||
} else if let Some(trimmed) = original_str.strip_prefix('-') {
|
||||
} else if let Some(trimmed) = sign_check_str.strip_prefix('-') {
|
||||
("-", trimmed)
|
||||
} else {
|
||||
("", original_str)
|
||||
("", sign_check_str)
|
||||
};
|
||||
let mut rest = Cow::from(original_str);
|
||||
let has_digit_separators = memchr::memchr(b'_', rest.as_bytes()).is_some();
|
||||
// Save the string after the sign for normalization (before removing underscores)
|
||||
let str_after_sign_for_normalization = sign_check_str;
|
||||
let mut rest = Cow::from(sign_check_str);
|
||||
let has_digit_separators = memchr::memchr(b'_', original_str.as_bytes()).is_some();
|
||||
if has_digit_separators {
|
||||
rest = Cow::from(rest.replace('_', ""));
|
||||
}
|
||||
|
|
@ -123,7 +128,7 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal
|
|||
|
||||
// If the original string had digit separators, normalize them
|
||||
let rest = if has_digit_separators {
|
||||
Cow::from(normalize_digit_separators(original_str))
|
||||
Cow::from(normalize_digit_separators(str_after_sign_for_normalization))
|
||||
} else {
|
||||
Cow::from(rest)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -669,6 +669,7 @@ help: Replace with `1_2345`
|
|||
85 + Decimal(1_2345)
|
||||
86 | Decimal("000_1_2345")
|
||||
87 | Decimal("000_000")
|
||||
88 |
|
||||
|
||||
FURB157 [*] Verbose expression in `Decimal` constructor
|
||||
--> FURB157.py:86:9
|
||||
|
|
@ -686,6 +687,8 @@ help: Replace with `1_2345`
|
|||
- Decimal("000_1_2345")
|
||||
86 + Decimal(1_2345)
|
||||
87 | Decimal("000_000")
|
||||
88 |
|
||||
89 | # Test cases for underscores before sign
|
||||
|
||||
FURB157 [*] Verbose expression in `Decimal` constructor
|
||||
--> FURB157.py:87:9
|
||||
|
|
@ -694,6 +697,8 @@ FURB157 [*] Verbose expression in `Decimal` constructor
|
|||
86 | Decimal("000_1_2345")
|
||||
87 | Decimal("000_000")
|
||||
| ^^^^^^^^^
|
||||
88 |
|
||||
89 | # Test cases for underscores before sign
|
||||
|
|
||||
help: Replace with `0`
|
||||
84 | # Separators _and_ leading zeros
|
||||
|
|
@ -701,3 +706,57 @@ help: Replace with `0`
|
|||
86 | Decimal("000_1_2345")
|
||||
- Decimal("000_000")
|
||||
87 + Decimal(0)
|
||||
88 |
|
||||
89 | # Test cases for underscores before sign
|
||||
90 | # https://github.com/astral-sh/ruff/issues/21186
|
||||
|
||||
FURB157 [*] Verbose expression in `Decimal` constructor
|
||||
--> FURB157.py:91:9
|
||||
|
|
||||
89 | # Test cases for underscores before sign
|
||||
90 | # https://github.com/astral-sh/ruff/issues/21186
|
||||
91 | Decimal("_-1") # Should flag as verbose
|
||||
| ^^^^^
|
||||
92 | Decimal("_+1") # Should flag as verbose
|
||||
93 | Decimal("_-1_000") # Should flag as verbose
|
||||
|
|
||||
help: Replace with `-1`
|
||||
88 |
|
||||
89 | # Test cases for underscores before sign
|
||||
90 | # https://github.com/astral-sh/ruff/issues/21186
|
||||
- Decimal("_-1") # Should flag as verbose
|
||||
91 + Decimal(-1) # Should flag as verbose
|
||||
92 | Decimal("_+1") # Should flag as verbose
|
||||
93 | Decimal("_-1_000") # Should flag as verbose
|
||||
|
||||
FURB157 [*] Verbose expression in `Decimal` constructor
|
||||
--> FURB157.py:92:9
|
||||
|
|
||||
90 | # https://github.com/astral-sh/ruff/issues/21186
|
||||
91 | Decimal("_-1") # Should flag as verbose
|
||||
92 | Decimal("_+1") # Should flag as verbose
|
||||
| ^^^^^
|
||||
93 | Decimal("_-1_000") # Should flag as verbose
|
||||
|
|
||||
help: Replace with `+1`
|
||||
89 | # Test cases for underscores before sign
|
||||
90 | # https://github.com/astral-sh/ruff/issues/21186
|
||||
91 | Decimal("_-1") # Should flag as verbose
|
||||
- Decimal("_+1") # Should flag as verbose
|
||||
92 + Decimal(+1) # Should flag as verbose
|
||||
93 | Decimal("_-1_000") # Should flag as verbose
|
||||
|
||||
FURB157 [*] Verbose expression in `Decimal` constructor
|
||||
--> FURB157.py:93:9
|
||||
|
|
||||
91 | Decimal("_-1") # Should flag as verbose
|
||||
92 | Decimal("_+1") # Should flag as verbose
|
||||
93 | Decimal("_-1_000") # Should flag as verbose
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
help: Replace with `-1_000`
|
||||
90 | # https://github.com/astral-sh/ruff/issues/21186
|
||||
91 | Decimal("_-1") # Should flag as verbose
|
||||
92 | Decimal("_+1") # Should flag as verbose
|
||||
- Decimal("_-1_000") # Should flag as verbose
|
||||
93 + Decimal(-1_000) # Should flag as verbose
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue