Skip to content

Commit 2faac1e

Browse files
authored
[refurb] Implement math-constant (FURB152) (#8727)
## Summary Implements [FURB152](https://github.com/dosisod/refurb/blob/master/docs/checks.md#furb152-use-math-constant) that checks for literals that are similar to constants in `math` module, for example: ```python A = 3.141592 * r ** 2 ``` Use instead: ```python A = math.pi * r ** 2 ``` Related to #1348.
1 parent b7dbb90 commit 2faac1e

File tree

9 files changed

+175
-1
lines changed

9 files changed

+175
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
r = 3.1 # OK
2+
3+
A = 3.14 * r ** 2 # FURB152
4+
5+
C = 6.28 * r # FURB152
6+
7+
e = 2.71 # FURB152

crates/ruff_linter/src/checkers/ast/analyze/expression.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1245,10 +1245,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
12451245
refurb::rules::single_item_membership_test(checker, expr, left, ops, comparators);
12461246
}
12471247
}
1248-
Expr::NumberLiteral(_) => {
1248+
Expr::NumberLiteral(number_literal @ ast::ExprNumberLiteral { .. }) => {
12491249
if checker.source_type.is_stub() && checker.enabled(Rule::NumericLiteralTooLong) {
12501250
flake8_pyi::rules::numeric_literal_too_long(checker, expr);
12511251
}
1252+
if checker.enabled(Rule::MathConstant) {
1253+
refurb::rules::math_constant(checker, number_literal);
1254+
}
12521255
}
12531256
Expr::BytesLiteral(_) => {
12541257
if checker.source_type.is_stub() && checker.enabled(Rule::StringOrBytesTooLong) {

crates/ruff_linter/src/codes.rs

+1
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
951951
(Refurb, "140") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedStarmap),
952952
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
953953
(Refurb, "148") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryEnumerate),
954+
(Refurb, "152") => (RuleGroup::Preview, rules::refurb::rules::MathConstant),
954955
(Refurb, "168") => (RuleGroup::Preview, rules::refurb::rules::IsinstanceTypeNone),
955956
(Refurb, "169") => (RuleGroup::Preview, rules::refurb::rules::TypeNoneComparison),
956957
(Refurb, "171") => (RuleGroup::Preview, rules::refurb::rules::SingleItemMembershipTest),

crates/ruff_linter/src/rules/refurb/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ mod tests {
2222
#[test_case(Rule::ReimplementedStarmap, Path::new("FURB140.py"))]
2323
#[test_case(Rule::SliceCopy, Path::new("FURB145.py"))]
2424
#[test_case(Rule::UnnecessaryEnumerate, Path::new("FURB148.py"))]
25+
#[test_case(Rule::MathConstant, Path::new("FURB152.py"))]
2526
#[test_case(Rule::PrintEmptyString, Path::new("FURB105.py"))]
2627
#[test_case(Rule::ImplicitCwd, Path::new("FURB177.py"))]
2728
#[test_case(Rule::SingleItemMembershipTest, Path::new("FURB171.py"))]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use anyhow::Result;
2+
3+
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
4+
use ruff_macros::{derive_message_formats, violation};
5+
use ruff_python_ast::{self as ast, Number};
6+
use ruff_text_size::Ranged;
7+
8+
use crate::checkers::ast::Checker;
9+
use crate::importer::ImportRequest;
10+
11+
/// ## What it does
12+
/// Checks for literals that are similar to constants in `math` module.
13+
///
14+
/// ## Why is this bad?
15+
/// Hard-coding mathematical constants like π increases code duplication,
16+
/// reduces readability, and may lead to a lack of precision.
17+
///
18+
/// ## Example
19+
/// ```python
20+
/// A = 3.141592 * r**2
21+
/// ```
22+
///
23+
/// Use instead:
24+
/// ```python
25+
/// A = math.pi * r**2
26+
/// ```
27+
///
28+
/// ## References
29+
/// - [Python documentation: `math` constants](https://docs.python.org/3/library/math.html#constants)
30+
#[violation]
31+
pub struct MathConstant {
32+
literal: String,
33+
constant: &'static str,
34+
}
35+
36+
impl Violation for MathConstant {
37+
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
38+
39+
#[derive_message_formats]
40+
fn message(&self) -> String {
41+
let MathConstant { literal, constant } = self;
42+
format!("Replace `{literal}` with `math.{constant}`")
43+
}
44+
45+
fn fix_title(&self) -> Option<String> {
46+
let MathConstant { constant, .. } = self;
47+
Some(format!("Use `math.{constant}`"))
48+
}
49+
}
50+
51+
/// FURB152
52+
pub(crate) fn math_constant(checker: &mut Checker, literal: &ast::ExprNumberLiteral) {
53+
let Number::Float(value) = literal.value else {
54+
return;
55+
};
56+
for (real_value, constant) in [
57+
(std::f64::consts::PI, "pi"),
58+
(std::f64::consts::E, "e"),
59+
(std::f64::consts::TAU, "tau"),
60+
] {
61+
if (value - real_value).abs() < 1e-2 {
62+
let mut diagnostic = Diagnostic::new(
63+
MathConstant {
64+
literal: checker.locator().slice(literal).into(),
65+
constant,
66+
},
67+
literal.range(),
68+
);
69+
diagnostic.try_set_fix(|| convert_to_constant(literal, constant, checker));
70+
checker.diagnostics.push(diagnostic);
71+
return;
72+
}
73+
}
74+
}
75+
76+
fn convert_to_constant(
77+
literal: &ast::ExprNumberLiteral,
78+
constant: &'static str,
79+
checker: &Checker,
80+
) -> Result<Fix> {
81+
let (edit, binding) = checker.importer().get_or_import_symbol(
82+
&ImportRequest::import("math", constant),
83+
literal.start(),
84+
checker.semantic(),
85+
)?;
86+
Ok(Fix::safe_edits(
87+
Edit::range_replacement(binding, literal.range()),
88+
[edit],
89+
))
90+
}

crates/ruff_linter/src/rules/refurb/rules/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub(crate) use delete_full_slice::*;
33
pub(crate) use if_expr_min_max::*;
44
pub(crate) use implicit_cwd::*;
55
pub(crate) use isinstance_type_none::*;
6+
pub(crate) use math_constant::*;
67
pub(crate) use print_empty_string::*;
78
pub(crate) use read_whole_file::*;
89
pub(crate) use reimplemented_starmap::*;
@@ -17,6 +18,7 @@ mod delete_full_slice;
1718
mod if_expr_min_max;
1819
mod implicit_cwd;
1920
mod isinstance_type_none;
21+
mod math_constant;
2022
mod print_empty_string;
2123
mod read_whole_file;
2224
mod reimplemented_starmap;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
source: crates/ruff_linter/src/rules/refurb/mod.rs
3+
---
4+
FURB152.py:3:5: FURB152 [*] Replace `3.14` with `math.pi`
5+
|
6+
1 | r = 3.1 # OK
7+
2 |
8+
3 | A = 3.14 * r ** 2 # FURB152
9+
| ^^^^ FURB152
10+
4 |
11+
5 | C = 6.28 * r # FURB152
12+
|
13+
= help: Use `math.pi`
14+
15+
Safe fix
16+
1 |+import math
17+
1 2 | r = 3.1 # OK
18+
2 3 |
19+
3 |-A = 3.14 * r ** 2 # FURB152
20+
4 |+A = math.pi * r ** 2 # FURB152
21+
4 5 |
22+
5 6 | C = 6.28 * r # FURB152
23+
6 7 |
24+
25+
FURB152.py:5:5: FURB152 [*] Replace `6.28` with `math.tau`
26+
|
27+
3 | A = 3.14 * r ** 2 # FURB152
28+
4 |
29+
5 | C = 6.28 * r # FURB152
30+
| ^^^^ FURB152
31+
6 |
32+
7 | e = 2.71 # FURB152
33+
|
34+
= help: Use `math.tau`
35+
36+
Safe fix
37+
1 |+import math
38+
1 2 | r = 3.1 # OK
39+
2 3 |
40+
3 4 | A = 3.14 * r ** 2 # FURB152
41+
4 5 |
42+
5 |-C = 6.28 * r # FURB152
43+
6 |+C = math.tau * r # FURB152
44+
6 7 |
45+
7 8 | e = 2.71 # FURB152
46+
47+
FURB152.py:7:5: FURB152 [*] Replace `2.71` with `math.e`
48+
|
49+
5 | C = 6.28 * r # FURB152
50+
6 |
51+
7 | e = 2.71 # FURB152
52+
| ^^^^ FURB152
53+
|
54+
= help: Use `math.e`
55+
56+
Safe fix
57+
1 |+import math
58+
1 2 | r = 3.1 # OK
59+
2 3 |
60+
3 4 | A = 3.14 * r ** 2 # FURB152
61+
4 5 |
62+
5 6 | C = 6.28 * r # FURB152
63+
6 7 |
64+
7 |-e = 2.71 # FURB152
65+
8 |+e = math.e # FURB152
66+
67+

crates/ruff_workspace/src/configuration.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1159,6 +1159,7 @@ mod tests {
11591159
Rule::TooManyPublicMethods,
11601160
Rule::UndocumentedWarn,
11611161
Rule::UnnecessaryEnumerate,
1162+
Rule::MathConstant,
11621163
];
11631164

11641165
#[allow(clippy::needless_pass_by_value)]

ruff.schema.json

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)