From c30fbe2b9138af0918237395e6d5a3efec7541d0 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Thu, 3 Jul 2025 09:00:41 +0200 Subject: [PATCH] add rule RUF062 for PEP 515 number formatting with digit grouping --- .../resources/test/fixtures/ruff/RUF062.py | 53 ++ .../src/checkers/ast/analyze/expression.rs | 3 + crates/ruff_linter/src/codes.rs | 1 + crates/ruff_linter/src/rules/ruff/mod.rs | 3 + ...ge_number_without_underscore_separators.rs | 205 +++++++ .../ruff_linter/src/rules/ruff/rules/mod.rs | 2 + crates/ruff_linter/src/rules/ruff/settings.rs | 34 +- ..._rules__ruff__tests__RUF062_RUF062.py.snap | 511 ++++++++++++++++++ crates/ruff_workspace/src/options.rs | 72 +++ ruff.schema.json | 1 + 10 files changed, 884 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/RUF062.py create mode 100644 crates/ruff_linter/src/rules/ruff/rules/large_number_without_underscore_separators.rs create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF062_RUF062.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF062.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF062.py new file mode 100644 index 0000000000000..dfe7c3b65f0bb --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF062.py @@ -0,0 +1,53 @@ +"""Tests for the RUF062 rule (large numeric literals without underscore separators).""" + +# These should trigger the rule (large numbers without underscore separators) +i = 1000000 +f = 123456789.123456789 +x = 0x1234ABCD +b = 0b10101010101010101010101 +o = 0o12345671234 + +# Scientific notation +sci = 1000000e10 +sci_uppercase = 1000000E10 + +# These should not trigger the rule (small numbers or already have separators) +dec_small_int = 1234 +dec_small_float = 123.45 +dec_with_separators = 1_000_000 +hex_with_separators = 0x1234_ABCD +bin_with_separators = 0b10101_01010101_01010101 +oct_with_separators = 0o123_4567_1234 +sci_with_separators = 1_000_000e10 + +# These should trigger the rule because their separators are misplaced +dec_misplaced_separators = 123_4567_89 +oct_misplaced_separators = 0o12_34_56 +hex_misplaced_separators = 0xABCD_EF +flt_misplaced_separators = 123.12_3456_789 + +# uppercase base prefix +hex_uppercase = 0XABCDEF +oct_uppercase = 0O123456 +bin_uppercase = 0B01010101010101 + +# Negative numbers should also be checked +neg_large = -1000000 +neg_with_separators = -1_000_000 # should not trigger +neg_with_spaces = - 100000 +neg_oct = -0o1234567 +neg_hex = -0xABCDEF +neg_bin -0b0101010100101 +neg_hex_with_spaces = - 0xABCDEF + +# Testing for minimun size thresholds +dec_4_digits = 1234 # Should not trigger, just below the threshold of 5 digits +dec_5_digits = 12345 # Should trigger, 5 digits +oct_4_digits = 0o1234 # Should not trigger, just below the threshold of 4 digits +oct_5_digits = 0o12345 # Should trigger, 5 digits +bin_8_digits = 0b01010101 # Should not trigger, just below the threshold of 9 digits +bin_9_digits = 0b101010101 # Should trigger, 9 digits +hex_4_digits = 0xABCD # Should not trigger, just below the threshold of 5 digits +hex_5_digits = 0xABCDE # Should trigger, 5 digits +flt_4_digits = .1234 # Should not trigger, just below the threshold of 5 digits +flt_5_digits = .12345 # Should trigger, 5 digits \ No newline at end of file diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index a1f854e4d5a6d..603083be17866 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1573,6 +1573,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if checker.is_rule_enabled(Rule::MathConstant) { refurb::rules::math_constant(checker, number_literal); } + if checker.is_rule_enabled(Rule::LargeNumberWithoutUnderscoreSeparators) { + ruff::rules::large_number_without_underscore_separators(checker, expr); + } } Expr::StringLiteral( string_like @ ast::ExprStringLiteral { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index dd3b86d25cc14..dc28d0d68b6fe 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -1034,6 +1034,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable), (Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection), (Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises), + (Ruff, "062") => (RuleGroup::Preview, rules::ruff::rules::LargeNumberWithoutUnderscoreSeparators), (Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::AccessAnnotationsFromClassDict), (Ruff, "064") => (RuleGroup::Preview, rules::ruff::rules::NonOctalPermissions), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index f712219503213..53899fff14284 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -110,6 +110,7 @@ mod tests { #[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_raises.py"))] #[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_warns.py"))] #[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_deprecated_call.py"))] + #[test_case(Rule::LargeNumberWithoutUnderscoreSeparators, Path::new("RUF062.py"))] #[test_case(Rule::NonOctalPermissions, Path::new("RUF064.py"))] #[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))] #[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))] @@ -131,6 +132,7 @@ mod tests { &LinterSettings { ruff: super::settings::Settings { parenthesize_tuple_in_subscript: true, + ..Default::default() }, ..LinterSettings::for_rule(Rule::IncorrectlyParenthesizedTupleInSubscript) }, @@ -146,6 +148,7 @@ mod tests { &LinterSettings { ruff: super::settings::Settings { parenthesize_tuple_in_subscript: false, + ..Default::default() }, unresolved_target_version: PythonVersion::PY310.into(), ..LinterSettings::for_rule(Rule::IncorrectlyParenthesizedTupleInSubscript) diff --git a/crates/ruff_linter/src/rules/ruff/rules/large_number_without_underscore_separators.rs b/crates/ruff_linter/src/rules/ruff/rules/large_number_without_underscore_separators.rs new file mode 100644 index 0000000000000..293d8de9cda2e --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/large_number_without_underscore_separators.rs @@ -0,0 +1,205 @@ +use crate::AlwaysFixableViolation; +use crate::checkers::ast::Checker; +use crate::rules::ruff::settings::Settings; +use ruff_diagnostics::{Edit, Fix}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast as ast; +use ruff_text_size::Ranged; + +/// ## What it does +/// Checks for numeric literals that could be more readable with underscore separators +/// between groups of digits. +/// +/// ## Why is this bad? +/// Large numeric literals can be difficult to read. Using underscore separators +/// improves readability by visually separating groups of digits. +/// +/// ## Example +/// +/// ```python +/// # Before +/// x = 1000000 +/// y = 1234567.89 +/// ``` +/// +/// Use instead: +/// ```python +/// # After +/// x = 1_000_000 +/// y = 1_234_567.89 +/// ``` +/// +/// ## References +/// - [PEP 515 - Underscores in Numeric Literals](https://peps.python.org/pep-0515/) +/// - [Number Localization Formatting Guide](https://randombits.dev/articles/number-localization/formatting) +#[derive(ViolationMetadata)] +pub(crate) struct LargeNumberWithoutUnderscoreSeparators; + +impl AlwaysFixableViolation for LargeNumberWithoutUnderscoreSeparators { + #[derive_message_formats] + fn message(&self) -> String { + "Large numeric literal without underscore separators".to_string() + } + + fn fix_title(&self) -> String { + "Add underscore separators to numeric literal".to_string() + } +} + +/// RUF062: Large numeric literal without underscore separators +pub(crate) fn large_number_without_underscore_separators(checker: &Checker, expr: &ast::Expr) { + let value_text = checker.locator().slice(expr.range()); + + // format number to compare with the source + let formatted_value: String = + format_number_with_underscores(value_text, &checker.settings().ruff); + + if formatted_value != value_text { + checker + .report_diagnostic(LargeNumberWithoutUnderscoreSeparators, expr.range()) + .set_fix(Fix::safe_edit(Edit::range_replacement( + formatted_value, + expr.range(), + ))); + } +} + +/// Format a numeric literal with properly placed underscore separators +fn format_number_with_underscores(value: &str, settings: &Settings) -> String { + // Remove existing underscores + let value = value.replace("_", ""); + if value.starts_with("0x") || value.starts_with("0X") { + // Hexadecimal + let prefix = &value[..2]; + let hex_part = &value[2..]; + + let formatted = format_digits( + hex_part, + settings.hex_digit_group_size, + settings.hex_digit_group_size, + settings.hex_digit_grouping_threshold, + ); + format!("{}{}", prefix, formatted) + } else if value.starts_with("0b") || value.starts_with("0B") { + // Binary + let prefix = &value[..2]; + let bin_part = &value[2..]; + + let formatted = format_digits( + bin_part, + settings.bin_digit_group_size, + settings.bin_digit_group_size, + settings.bin_digit_grouping_threshold, + ); + format!("{}{}", prefix, formatted) + } else if value.starts_with("0o") || value.starts_with("0O") { + // Octal + let prefix = &value[..2]; + let oct_part = &value[2..]; + + let formatted = format_digits( + oct_part, + settings.oct_digit_group_size, + settings.oct_digit_group_size, + settings.oct_digit_grouping_threshold, + ); + format!("{}{}", prefix, formatted) + } else { + if value.contains(['e', 'E']) { + // Handle scientific notation + let parts: Vec<&str> = value.split(['e', 'E']).collect(); + let base = format_number_with_underscores(parts[0], settings); + let exponent = parts[1]; + + // Determine which separator was used (e or E) + let separator = if value.contains('e') { 'e' } else { 'E' }; + + return format!("{}{}{}", base, separator, exponent); + } + + // Decimal (integer or float) + let parts: Vec<&str> = value.split('.').collect(); + let group_size = if settings.use_indian_decimal_format { + 2 + } else { + 3 + }; + let integer_part = format_digits( + &parts[0], + group_size, + 3, + settings.dec_digit_grouping_threshold, + ); + + if parts.len() > 1 { + // It's a float, handle the fractional part + let float_part = format_float( + parts[1], + group_size, + 3, + settings.dec_digit_grouping_threshold, + ); + format!("{}.{}", integer_part, float_part) + } else { + // It's an integer + format!("{}", integer_part) + } + } +} + +/// Helper function to format digits with underscores at specified intervals +fn format_digits( + digits: &str, + group_size: usize, + first_group_size: usize, + threshold: usize, +) -> String { + if digits.len() < threshold || group_size == 0 || first_group_size == 0 { + return digits.to_string(); + } + + let mut result = String::with_capacity(digits.len() * 2); + let mut count = 0; + + // Process digits from right to left + for c in digits.chars().rev() { + if count == first_group_size + || (count > first_group_size + 1 && (count - first_group_size) % group_size == 0) + { + result.push('_'); + } + result.push(c); + count += 1; + } + + // Reverse the result to get the correct order + result.chars().rev().collect() +} + +// Helper function to format float parts with underscores at specified intervals +fn format_float( + digits: &str, + group_size: usize, + first_group_size: usize, + threshold: usize, +) -> String { + if digits.len() < threshold || group_size == 0 || first_group_size == 0 { + return digits.to_string(); + } + + let mut result = String::with_capacity(digits.len() * 2); + let mut count = 0; + + // Process digits from right to left + for c in digits.chars() { + if count == first_group_size + || (count > first_group_size && (count - first_group_size) % group_size == 0) + { + result.push('_'); + } + result.push(c); + count += 1; + } + + result +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index 420b8c310a585..9d440048ce41a 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -32,6 +32,7 @@ pub(crate) use needless_else::*; pub(crate) use never_union::*; pub(crate) use non_octal_permissions::*; pub(crate) use none_not_at_end_of_union::*; +pub(crate) use large_number_without_underscore_separators::*; pub(crate) use parenthesize_chained_operators::*; pub(crate) use post_init_default::*; pub(crate) use pytest_raises_ambiguous_pattern::*; @@ -62,6 +63,7 @@ pub(crate) use zip_instead_of_pairwise::*; mod access_annotations_from_class_dict; mod ambiguous_unicode_character; +mod large_number_without_underscore_separators; mod assert_with_print_message; mod assignment_in_assert; mod asyncio_dangling_task; diff --git a/crates/ruff_linter/src/rules/ruff/settings.rs b/crates/ruff_linter/src/rules/ruff/settings.rs index c6768121f054f..0efb692dbb029 100644 --- a/crates/ruff_linter/src/rules/ruff/settings.rs +++ b/crates/ruff_linter/src/rules/ruff/settings.rs @@ -4,9 +4,33 @@ use crate::display_settings; use ruff_macros::CacheKey; use std::fmt; -#[derive(Debug, Clone, CacheKey, Default)] +#[derive(Debug, Clone, CacheKey)] pub struct Settings { pub parenthesize_tuple_in_subscript: bool, + pub use_indian_decimal_format: bool, + pub hex_digit_group_size: usize, + pub oct_digit_group_size: usize, + pub bin_digit_group_size: usize, + pub hex_digit_grouping_threshold: usize, + pub dec_digit_grouping_threshold: usize, + pub oct_digit_grouping_threshold: usize, + pub bin_digit_grouping_threshold: usize, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + parenthesize_tuple_in_subscript: false, + use_indian_decimal_format: false, + hex_digit_group_size: 4, + oct_digit_group_size: 4, + bin_digit_group_size: 8, + hex_digit_grouping_threshold: 5, + dec_digit_grouping_threshold: 5, + oct_digit_grouping_threshold: 5, + bin_digit_grouping_threshold: 8, + } + } } impl fmt::Display for Settings { @@ -16,6 +40,14 @@ impl fmt::Display for Settings { namespace = "linter.ruff", fields = [ self.parenthesize_tuple_in_subscript, + self.use_indian_decimal_format, + self.bin_digit_grouping_threshold, + self.oct_digit_grouping_threshold, + self.hex_digit_grouping_threshold, + self.dec_digit_grouping_threshold, + self.bin_digit_group_size, + self.oct_digit_group_size, + self.hex_digit_group_size, ] } Ok(()) diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF062_RUF062.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF062_RUF062.py.snap new file mode 100644 index 0000000000000..b6e4df3bd64f0 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF062_RUF062.py.snap @@ -0,0 +1,511 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF062.py:4:5: RUF062 [*] Large numeric literal without underscore separators + | +3 | # These should trigger the rule (large numbers without underscore separators) +4 | i = 1000000 + | ^^^^^^^ RUF062 +5 | f = 123456789.123456789 +6 | x = 0x1234ABCD + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +1 1 | """Tests for the RUF062 rule (large numeric literals without underscore separators).""" +2 2 | +3 3 | # These should trigger the rule (large numbers without underscore separators) +4 |-i = 1000000 + 4 |+i = 1_000_000 +5 5 | f = 123456789.123456789 +6 6 | x = 0x1234ABCD +7 7 | b = 0b10101010101010101010101 + +RUF062.py:5:5: RUF062 [*] Large numeric literal without underscore separators + | +3 | # These should trigger the rule (large numbers without underscore separators) +4 | i = 1000000 +5 | f = 123456789.123456789 + | ^^^^^^^^^^^^^^^^^^^ RUF062 +6 | x = 0x1234ABCD +7 | b = 0b10101010101010101010101 + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +2 2 | +3 3 | # These should trigger the rule (large numbers without underscore separators) +4 4 | i = 1000000 +5 |-f = 123456789.123456789 + 5 |+f = 123_456_789.123_456_789 +6 6 | x = 0x1234ABCD +7 7 | b = 0b10101010101010101010101 +8 8 | o = 0o12345671234 + +RUF062.py:6:5: RUF062 [*] Large numeric literal without underscore separators + | +4 | i = 1000000 +5 | f = 123456789.123456789 +6 | x = 0x1234ABCD + | ^^^^^^^^^^ RUF062 +7 | b = 0b10101010101010101010101 +8 | o = 0o12345671234 + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +3 3 | # These should trigger the rule (large numbers without underscore separators) +4 4 | i = 1000000 +5 5 | f = 123456789.123456789 +6 |-x = 0x1234ABCD + 6 |+x = 0x1234_ABCD +7 7 | b = 0b10101010101010101010101 +8 8 | o = 0o12345671234 +9 9 | + +RUF062.py:7:5: RUF062 [*] Large numeric literal without underscore separators + | +5 | f = 123456789.123456789 +6 | x = 0x1234ABCD +7 | b = 0b10101010101010101010101 + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF062 +8 | o = 0o12345671234 + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +4 4 | i = 1000000 +5 5 | f = 123456789.123456789 +6 6 | x = 0x1234ABCD +7 |-b = 0b10101010101010101010101 + 7 |+b = 0b1010101_01010101_01010101 +8 8 | o = 0o12345671234 +9 9 | +10 10 | # Scientific notation + +RUF062.py:8:5: RUF062 [*] Large numeric literal without underscore separators + | + 6 | x = 0x1234ABCD + 7 | b = 0b10101010101010101010101 + 8 | o = 0o12345671234 + | ^^^^^^^^^^^^^ RUF062 + 9 | +10 | # Scientific notation + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +5 5 | f = 123456789.123456789 +6 6 | x = 0x1234ABCD +7 7 | b = 0b10101010101010101010101 +8 |-o = 0o12345671234 + 8 |+o = 0o123_4567_1234 +9 9 | +10 10 | # Scientific notation +11 11 | sci = 1000000e10 + +RUF062.py:11:7: RUF062 [*] Large numeric literal without underscore separators + | +10 | # Scientific notation +11 | sci = 1000000e10 + | ^^^^^^^^^^ RUF062 +12 | sci_uppercase = 1000000E10 + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +8 8 | o = 0o12345671234 +9 9 | +10 10 | # Scientific notation +11 |-sci = 1000000e10 + 11 |+sci = 1_000_000e10 +12 12 | sci_uppercase = 1000000E10 +13 13 | +14 14 | # These should not trigger the rule (small numbers or already have separators) + +RUF062.py:12:17: RUF062 [*] Large numeric literal without underscore separators + | +10 | # Scientific notation +11 | sci = 1000000e10 +12 | sci_uppercase = 1000000E10 + | ^^^^^^^^^^ RUF062 +13 | +14 | # These should not trigger the rule (small numbers or already have separators) + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +9 9 | +10 10 | # Scientific notation +11 11 | sci = 1000000e10 +12 |-sci_uppercase = 1000000E10 + 12 |+sci_uppercase = 1_000_000E10 +13 13 | +14 14 | # These should not trigger the rule (small numbers or already have separators) +15 15 | dec_small_int = 1234 + +RUF062.py:24:28: RUF062 [*] Large numeric literal without underscore separators + | +23 | # These should trigger the rule because their separators are misplaced +24 | dec_misplaced_separators = 123_4567_89 + | ^^^^^^^^^^^ RUF062 +25 | oct_misplaced_separators = 0o12_34_56 +26 | hex_misplaced_separators = 0xABCD_EF + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +21 21 | sci_with_separators = 1_000_000e10 +22 22 | +23 23 | # These should trigger the rule because their separators are misplaced +24 |-dec_misplaced_separators = 123_4567_89 + 24 |+dec_misplaced_separators = 123_456_789 +25 25 | oct_misplaced_separators = 0o12_34_56 +26 26 | hex_misplaced_separators = 0xABCD_EF +27 27 | flt_misplaced_separators = 123.12_3456_789 + +RUF062.py:25:28: RUF062 [*] Large numeric literal without underscore separators + | +23 | # These should trigger the rule because their separators are misplaced +24 | dec_misplaced_separators = 123_4567_89 +25 | oct_misplaced_separators = 0o12_34_56 + | ^^^^^^^^^^ RUF062 +26 | hex_misplaced_separators = 0xABCD_EF +27 | flt_misplaced_separators = 123.12_3456_789 + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +22 22 | +23 23 | # These should trigger the rule because their separators are misplaced +24 24 | dec_misplaced_separators = 123_4567_89 +25 |-oct_misplaced_separators = 0o12_34_56 + 25 |+oct_misplaced_separators = 0o12_3456 +26 26 | hex_misplaced_separators = 0xABCD_EF +27 27 | flt_misplaced_separators = 123.12_3456_789 +28 28 | + +RUF062.py:26:28: RUF062 [*] Large numeric literal without underscore separators + | +24 | dec_misplaced_separators = 123_4567_89 +25 | oct_misplaced_separators = 0o12_34_56 +26 | hex_misplaced_separators = 0xABCD_EF + | ^^^^^^^^^ RUF062 +27 | flt_misplaced_separators = 123.12_3456_789 + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +23 23 | # These should trigger the rule because their separators are misplaced +24 24 | dec_misplaced_separators = 123_4567_89 +25 25 | oct_misplaced_separators = 0o12_34_56 +26 |-hex_misplaced_separators = 0xABCD_EF + 26 |+hex_misplaced_separators = 0xAB_CDEF +27 27 | flt_misplaced_separators = 123.12_3456_789 +28 28 | +29 29 | # uppercase base prefix + +RUF062.py:27:28: RUF062 [*] Large numeric literal without underscore separators + | +25 | oct_misplaced_separators = 0o12_34_56 +26 | hex_misplaced_separators = 0xABCD_EF +27 | flt_misplaced_separators = 123.12_3456_789 + | ^^^^^^^^^^^^^^^ RUF062 +28 | +29 | # uppercase base prefix + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +24 24 | dec_misplaced_separators = 123_4567_89 +25 25 | oct_misplaced_separators = 0o12_34_56 +26 26 | hex_misplaced_separators = 0xABCD_EF +27 |-flt_misplaced_separators = 123.12_3456_789 + 27 |+flt_misplaced_separators = 123.123_456_789 +28 28 | +29 29 | # uppercase base prefix +30 30 | hex_uppercase = 0XABCDEF + +RUF062.py:30:17: RUF062 [*] Large numeric literal without underscore separators + | +29 | # uppercase base prefix +30 | hex_uppercase = 0XABCDEF + | ^^^^^^^^ RUF062 +31 | oct_uppercase = 0O123456 +32 | bin_uppercase = 0B01010101010101 + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +27 27 | flt_misplaced_separators = 123.12_3456_789 +28 28 | +29 29 | # uppercase base prefix +30 |-hex_uppercase = 0XABCDEF + 30 |+hex_uppercase = 0XAB_CDEF +31 31 | oct_uppercase = 0O123456 +32 32 | bin_uppercase = 0B01010101010101 +33 33 | + +RUF062.py:31:17: RUF062 [*] Large numeric literal without underscore separators + | +29 | # uppercase base prefix +30 | hex_uppercase = 0XABCDEF +31 | oct_uppercase = 0O123456 + | ^^^^^^^^ RUF062 +32 | bin_uppercase = 0B01010101010101 + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +28 28 | +29 29 | # uppercase base prefix +30 30 | hex_uppercase = 0XABCDEF +31 |-oct_uppercase = 0O123456 + 31 |+oct_uppercase = 0O12_3456 +32 32 | bin_uppercase = 0B01010101010101 +33 33 | +34 34 | # Negative numbers should also be checked + +RUF062.py:32:17: RUF062 [*] Large numeric literal without underscore separators + | +30 | hex_uppercase = 0XABCDEF +31 | oct_uppercase = 0O123456 +32 | bin_uppercase = 0B01010101010101 + | ^^^^^^^^^^^^^^^^ RUF062 +33 | +34 | # Negative numbers should also be checked + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +29 29 | # uppercase base prefix +30 30 | hex_uppercase = 0XABCDEF +31 31 | oct_uppercase = 0O123456 +32 |-bin_uppercase = 0B01010101010101 + 32 |+bin_uppercase = 0B010101_01010101 +33 33 | +34 34 | # Negative numbers should also be checked +35 35 | neg_large = -1000000 + +RUF062.py:35:14: RUF062 [*] Large numeric literal without underscore separators + | +34 | # Negative numbers should also be checked +35 | neg_large = -1000000 + | ^^^^^^^ RUF062 +36 | neg_with_separators = -1_000_000 # should not trigger +37 | neg_with_spaces = - 100000 + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +32 32 | bin_uppercase = 0B01010101010101 +33 33 | +34 34 | # Negative numbers should also be checked +35 |-neg_large = -1000000 + 35 |+neg_large = -1_000_000 +36 36 | neg_with_separators = -1_000_000 # should not trigger +37 37 | neg_with_spaces = - 100000 +38 38 | neg_oct = -0o1234567 + +RUF062.py:37:23: RUF062 [*] Large numeric literal without underscore separators + | +35 | neg_large = -1000000 +36 | neg_with_separators = -1_000_000 # should not trigger +37 | neg_with_spaces = - 100000 + | ^^^^^^ RUF062 +38 | neg_oct = -0o1234567 +39 | neg_hex = -0xABCDEF + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +34 34 | # Negative numbers should also be checked +35 35 | neg_large = -1000000 +36 36 | neg_with_separators = -1_000_000 # should not trigger +37 |-neg_with_spaces = - 100000 + 37 |+neg_with_spaces = - 100_000 +38 38 | neg_oct = -0o1234567 +39 39 | neg_hex = -0xABCDEF +40 40 | neg_bin -0b0101010100101 + +RUF062.py:38:12: RUF062 [*] Large numeric literal without underscore separators + | +36 | neg_with_separators = -1_000_000 # should not trigger +37 | neg_with_spaces = - 100000 +38 | neg_oct = -0o1234567 + | ^^^^^^^^^ RUF062 +39 | neg_hex = -0xABCDEF +40 | neg_bin -0b0101010100101 + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +35 35 | neg_large = -1000000 +36 36 | neg_with_separators = -1_000_000 # should not trigger +37 37 | neg_with_spaces = - 100000 +38 |-neg_oct = -0o1234567 + 38 |+neg_oct = -0o123_4567 +39 39 | neg_hex = -0xABCDEF +40 40 | neg_bin -0b0101010100101 +41 41 | neg_hex_with_spaces = - 0xABCDEF + +RUF062.py:39:12: RUF062 [*] Large numeric literal without underscore separators + | +37 | neg_with_spaces = - 100000 +38 | neg_oct = -0o1234567 +39 | neg_hex = -0xABCDEF + | ^^^^^^^^ RUF062 +40 | neg_bin -0b0101010100101 +41 | neg_hex_with_spaces = - 0xABCDEF + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +36 36 | neg_with_separators = -1_000_000 # should not trigger +37 37 | neg_with_spaces = - 100000 +38 38 | neg_oct = -0o1234567 +39 |-neg_hex = -0xABCDEF + 39 |+neg_hex = -0xAB_CDEF +40 40 | neg_bin -0b0101010100101 +41 41 | neg_hex_with_spaces = - 0xABCDEF +42 42 | + +RUF062.py:40:10: RUF062 [*] Large numeric literal without underscore separators + | +38 | neg_oct = -0o1234567 +39 | neg_hex = -0xABCDEF +40 | neg_bin -0b0101010100101 + | ^^^^^^^^^^^^^^^ RUF062 +41 | neg_hex_with_spaces = - 0xABCDEF + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +37 37 | neg_with_spaces = - 100000 +38 38 | neg_oct = -0o1234567 +39 39 | neg_hex = -0xABCDEF +40 |-neg_bin -0b0101010100101 + 40 |+neg_bin -0b01010_10100101 +41 41 | neg_hex_with_spaces = - 0xABCDEF +42 42 | +43 43 | # Testing for minimun size thresholds + +RUF062.py:41:27: RUF062 [*] Large numeric literal without underscore separators + | +39 | neg_hex = -0xABCDEF +40 | neg_bin -0b0101010100101 +41 | neg_hex_with_spaces = - 0xABCDEF + | ^^^^^^^^ RUF062 +42 | +43 | # Testing for minimun size thresholds + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +38 38 | neg_oct = -0o1234567 +39 39 | neg_hex = -0xABCDEF +40 40 | neg_bin -0b0101010100101 +41 |-neg_hex_with_spaces = - 0xABCDEF + 41 |+neg_hex_with_spaces = - 0xAB_CDEF +42 42 | +43 43 | # Testing for minimun size thresholds +44 44 | dec_4_digits = 1234 # Should not trigger, just below the threshold of 5 digits + +RUF062.py:45:16: RUF062 [*] Large numeric literal without underscore separators + | +43 | # Testing for minimun size thresholds +44 | dec_4_digits = 1234 # Should not trigger, just below the threshold of 5 digits +45 | dec_5_digits = 12345 # Should trigger, 5 digits + | ^^^^^ RUF062 +46 | oct_4_digits = 0o1234 # Should not trigger, just below the threshold of 4 digits +47 | oct_5_digits = 0o12345 # Should trigger, 5 digits + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +42 42 | +43 43 | # Testing for minimun size thresholds +44 44 | dec_4_digits = 1234 # Should not trigger, just below the threshold of 5 digits +45 |-dec_5_digits = 12345 # Should trigger, 5 digits + 45 |+dec_5_digits = 12_345 # Should trigger, 5 digits +46 46 | oct_4_digits = 0o1234 # Should not trigger, just below the threshold of 4 digits +47 47 | oct_5_digits = 0o12345 # Should trigger, 5 digits +48 48 | bin_8_digits = 0b01010101 # Should not trigger, just below the threshold of 9 digits + +RUF062.py:47:16: RUF062 [*] Large numeric literal without underscore separators + | +45 | dec_5_digits = 12345 # Should trigger, 5 digits +46 | oct_4_digits = 0o1234 # Should not trigger, just below the threshold of 4 digits +47 | oct_5_digits = 0o12345 # Should trigger, 5 digits + | ^^^^^^^ RUF062 +48 | bin_8_digits = 0b01010101 # Should not trigger, just below the threshold of 9 digits +49 | bin_9_digits = 0b101010101 # Should trigger, 9 digits + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +44 44 | dec_4_digits = 1234 # Should not trigger, just below the threshold of 5 digits +45 45 | dec_5_digits = 12345 # Should trigger, 5 digits +46 46 | oct_4_digits = 0o1234 # Should not trigger, just below the threshold of 4 digits +47 |-oct_5_digits = 0o12345 # Should trigger, 5 digits + 47 |+oct_5_digits = 0o1_2345 # Should trigger, 5 digits +48 48 | bin_8_digits = 0b01010101 # Should not trigger, just below the threshold of 9 digits +49 49 | bin_9_digits = 0b101010101 # Should trigger, 9 digits +50 50 | hex_4_digits = 0xABCD # Should not trigger, just below the threshold of 5 digits + +RUF062.py:49:16: RUF062 [*] Large numeric literal without underscore separators + | +47 | oct_5_digits = 0o12345 # Should trigger, 5 digits +48 | bin_8_digits = 0b01010101 # Should not trigger, just below the threshold of 9 digits +49 | bin_9_digits = 0b101010101 # Should trigger, 9 digits + | ^^^^^^^^^^^ RUF062 +50 | hex_4_digits = 0xABCD # Should not trigger, just below the threshold of 5 digits +51 | hex_5_digits = 0xABCDE # Should trigger, 5 digits + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +46 46 | oct_4_digits = 0o1234 # Should not trigger, just below the threshold of 4 digits +47 47 | oct_5_digits = 0o12345 # Should trigger, 5 digits +48 48 | bin_8_digits = 0b01010101 # Should not trigger, just below the threshold of 9 digits +49 |-bin_9_digits = 0b101010101 # Should trigger, 9 digits + 49 |+bin_9_digits = 0b1_01010101 # Should trigger, 9 digits +50 50 | hex_4_digits = 0xABCD # Should not trigger, just below the threshold of 5 digits +51 51 | hex_5_digits = 0xABCDE # Should trigger, 5 digits +52 52 | flt_4_digits = .1234 # Should not trigger, just below the threshold of 5 digits + +RUF062.py:51:16: RUF062 [*] Large numeric literal without underscore separators + | +49 | bin_9_digits = 0b101010101 # Should trigger, 9 digits +50 | hex_4_digits = 0xABCD # Should not trigger, just below the threshold of 5 digits +51 | hex_5_digits = 0xABCDE # Should trigger, 5 digits + | ^^^^^^^ RUF062 +52 | flt_4_digits = .1234 # Should not trigger, just below the threshold of 5 digits +53 | flt_5_digits = .12345 # Should trigger, 5 digits + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +48 48 | bin_8_digits = 0b01010101 # Should not trigger, just below the threshold of 9 digits +49 49 | bin_9_digits = 0b101010101 # Should trigger, 9 digits +50 50 | hex_4_digits = 0xABCD # Should not trigger, just below the threshold of 5 digits +51 |-hex_5_digits = 0xABCDE # Should trigger, 5 digits + 51 |+hex_5_digits = 0xA_BCDE # Should trigger, 5 digits +52 52 | flt_4_digits = .1234 # Should not trigger, just below the threshold of 5 digits +53 53 | flt_5_digits = .12345 # Should trigger, 5 digits + +RUF062.py:53:16: RUF062 [*] Large numeric literal without underscore separators + | +51 | hex_5_digits = 0xABCDE # Should trigger, 5 digits +52 | flt_4_digits = .1234 # Should not trigger, just below the threshold of 5 digits +53 | flt_5_digits = .12345 # Should trigger, 5 digits + | ^^^^^^ RUF062 + | + = help: Add underscore separators to numeric literal + +ℹ Safe fix +50 50 | hex_4_digits = 0xABCD # Should not trigger, just below the threshold of 5 digits +51 51 | hex_5_digits = 0xABCDE # Should trigger, 5 digits +52 52 | flt_4_digits = .1234 # Should not trigger, just below the threshold of 5 digits +53 |-flt_5_digits = .12345 # Should trigger, 5 digits + 53 |+flt_5_digits = .123_45 # Should trigger, 5 digits diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 8c6b630f28c8e..0db2f9f4103f4 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -3454,6 +3454,70 @@ pub struct RuffOptions { note = "The `allowed-markup-names` option has been moved to the `flake8-bandit` section of the configuration." )] pub allowed_markup_calls: Option>, + + /// Size of digits groups for large hexadecimal numbers. + #[option( + default = "4", + value_type = "integer", + example = "hex-digit-group-size = 4" + )] + pub hex_digit_group_size: Option, + + /// Size of digits groups for large octal numbers. + #[option( + default = "4", + value_type = "integer", + example = "oct-digit-group-size = 4" + )] + pub oct_digit_group_size: Option, + + /// Size of digits groups for large binary numbers. + #[option( + default = "8", + value_type = "integer", + example = "bin-digit-group-size = 8" + )] + pub bin_digit_group_size: Option, + + /// Whether to use Indian-style decimal formatting + #[option( + default = r#"false"#, + value_type = "bool", + example = "use-indian-decimal-formatting = false" + )] + pub use_indian_decimal_formatting: Option, + + /// Minimum number of decimal digits to trigger grouping + #[option( + default = "5", + value_type = "integer", + example = "dec-digit-grouping-threshold = 5" + )] + pub dec_digit_grouping_threshold: Option, + + /// Minimum number of hexadecimal digits to trigger grouping + #[option( + default = "5", + value_type = "integer", + example = "hex-digit-grouping-threshold = 5" + )] + pub hex_digit_grouping_threshold: Option, + + /// Minimum number of octal digits to trigger grouping + #[option( + default = "5", + value_type = "integer", + example = "oct-digit-grouping-threshold = 5" + )] + pub oct_digit_grouping_threshold: Option, + + /// Minimum number of binary digits to trigger grouping + #[option( + default = "9", + value_type = "integer", + example = "bin-digit-grouping-threshold = 5" + )] + pub bin_digit_grouping_threshold: Option, } impl RuffOptions { @@ -3462,6 +3526,14 @@ impl RuffOptions { parenthesize_tuple_in_subscript: self .parenthesize_tuple_in_subscript .unwrap_or_default(), + use_indian_decimal_format: self.use_indian_decimal_formatting.unwrap(), + hex_digit_group_size: self.hex_digit_group_size.unwrap_or_default(), + oct_digit_group_size: self.oct_digit_group_size.unwrap_or_default(), + bin_digit_group_size: self.bin_digit_group_size.unwrap_or_default(), + dec_digit_grouping_threshold: self.dec_digit_grouping_threshold.unwrap_or_default(), + hex_digit_grouping_threshold: self.hex_digit_grouping_threshold.unwrap_or_default(), + oct_digit_grouping_threshold: self.oct_digit_grouping_threshold.unwrap_or_default(), + bin_digit_grouping_threshold: self.bin_digit_grouping_threshold.unwrap_or_default(), } } } diff --git a/ruff.schema.json b/ruff.schema.json index 6e64471f9366a..2b0a608bdafc9 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -4040,6 +4040,7 @@ "RUF06", "RUF060", "RUF061", + "RUF062", "RUF063", "RUF064", "RUF1",