Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/wps_light/WPS303.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
.1
1.
1E+1
1E-1
1.E+1
1.0E+1
1.1E+1

x = 123456789
x = 123456
x = .1
x = 1.
x = 1E+1
x = 1E-1
x = 1.000_000_01
x = 123456789.123456789
x = 123456789.123456789E123456789
x = 123456789E123456789
x = 123456789J
x = 123456789.123456789J
x = 0XB1ACC
x = 0B1011
x = 0O777
x = 0.000000006
x = 10000
x = 133333

# Attribute access
x = 1. .imag
x = 1E+1.imag
x = 1E-1.real
x = 123456789.123456789.hex()
x = 123456789.123456789E123456789 .real
x = 123456789E123456789 .conjugate()
x = 123456789J.real
x = 123456789.123456789J.__add__(0b1011.bit_length())
x = 0XB1ACC.conjugate()
x = 0B1011 .conjugate()
x = 0O777 .real
x = 0.000000006 .hex()
x = -100.0000J

if 10 .real:
...

# This is a type error, not a syntax error
y = 100[no]
y = 100(no)

bin = 0b1001_1010_0001_0100
hex = 0x1b_a0_44_fe
dec = 33_554_432
real = 1_000.111_1e-1_000

valid = 0_0_0
also_ok = 000
4_2
1_0000_0000
0b1001_0100
0xffff_ffff
0o5_7_7
1_00_00.5
1e1_0
.1_4
0x_f
0o_5
5 changes: 4 additions & 1 deletion crates/ruff_linter/src/checkers/ast/analyze/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::rules::{
flake8_future_annotations, flake8_gettext, flake8_implicit_str_concat, flake8_logging,
flake8_logging_format, flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_self,
flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_use_pathlib, flynt, numpy,
pandas_vet, pep8_naming, pycodestyle, pyflakes, pylint, pyupgrade, refurb, ruff,
pandas_vet, pep8_naming, pycodestyle, pyflakes, pylint, pyupgrade, refurb, ruff, wps_light,
};
use crate::settings::types::PythonVersion;

Expand Down Expand Up @@ -1377,6 +1377,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::MathConstant) {
refurb::rules::math_constant(checker, number_literal);
}
if checker.enabled(Rule::UnderscoresInNumber) {
wps_light::rules::underscores_in_number(checker, number_literal);
}
}
Expr::StringLiteral(string_like @ ast::ExprStringLiteral { value, range: _ }) => {
if checker.enabled(Rule::UnicodeKindPrefix) {
Expand Down
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pydoclint, "501") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingException),
(Pydoclint, "502") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousException),

// wps-light
(WpsLight, "303") => (RuleGroup::Preview, rules::wps_light::rules::UnderscoresInNumber),

// ruff
(Ruff, "001") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterString),
(Ruff, "002") => (RuleGroup::Stable, rules::ruff::rules::AmbiguousUnicodeCharacterDocstring),
Expand Down
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ pub enum Linter {
/// [pydoclint](https://pypi.org/project/pydoclint/)
#[prefix = "DOC"]
Pydoclint,
/// [wps-light](https://pypi.org/project/wps-light/)
#[prefix = "WPS"]
WpsLight,
/// Ruff-specific rules
#[prefix = "RUF"]
Ruff,
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ pub mod pyupgrade;
pub mod refurb;
pub mod ruff;
pub mod tryceratops;
pub mod wps_light;
26 changes: 26 additions & 0 deletions crates/ruff_linter/src/rules/wps_light/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! Rules from [wps-light](https://pypi.org/project/wps-light/).
pub(crate) mod rules;

#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;

use anyhow::Result;
use test_case::test_case;

use crate::registry::Rule;
use crate::test::test_path;
use crate::{assert_messages, settings};

#[test_case(Rule::UnderscoresInNumber, Path::new("WPS303.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("wps_light").join(path).as_path(),
&settings::LinterSettings::for_rule(rule_code),
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/rules/wps_light/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub(crate) use underscores_in_number::*;

mod underscores_in_number;
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::ExprNumberLiteral;
use ruff_text_size::Ranged;

use crate::checkers::ast::Checker;

/// ## What it does
/// Forbids underscores (`_`) in number literals.
///
/// ## Why is this bad?
/// Numbers like `1000` can be written in multiple ways using underscores:
/// `1_000`, `10_00`, and `100_0`. While all of these are valid and represent
/// the same number, they rely on the author's cultural habits, leading to
/// inconsistencies and potential confusion. Enforcing a single, clear way to
/// write numbers improves readability and maintainability.
///
/// ## Example
/// ```python
/// phone = 8_83_134_43
/// million = 100_00_00
/// ```
///
/// Use instead:
/// ```python
/// phone = 88313443
/// million = 1000000
/// ```
#[violation]
pub struct UnderscoresInNumber {
number: String,
}

impl AlwaysFixableViolation for UnderscoresInNumber {
#[derive_message_formats]
fn message(&self) -> String {
let Self { number, .. } = self;
format!("Found underscores in number literal `{number}`")
}

fn fix_title(&self) -> String {
"Remove underscores from number literal".to_string()
}
}

/// WPS303
pub(crate) fn underscores_in_number(checker: &mut Checker, number: &ExprNumberLiteral) {
let num_str = &checker.locator().contents()[number.range()];
if !num_str.contains('_') {
return;
}
checker.diagnostics.push(
Diagnostic::new(
UnderscoresInNumber {
number: num_str.to_string(),
},
number.range(),
)
.with_fix(Fix::safe_edit(Edit::range_replacement(
num_str.replace('_', ""),
number.range(),
))),
);
}
Loading