diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py index d795fd1941..db49315f54 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB157.py @@ -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 diff --git a/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs b/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs index 50c98026d5..28779b021a 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs @@ -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) }; diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap index 92e8057055..3f0a1c2cf6 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap @@ -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