Skip to content

Commit fd9c700

Browse files
committed
[refurb] - implement FURB161/use-bit-count
1 parent 20def33 commit fd9c700

File tree

8 files changed

+317
-0
lines changed

8 files changed

+317
-0
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
x = 10
2+
3+
def ten() -> int:
4+
return 10
5+
6+
count = bin(x).count("1") # FURB161
7+
count = bin(10).count("1") # FURB161
8+
count = bin(0b1010).count("1") # FURB161
9+
count = bin(0xA).count("1") # FURB161
10+
count = bin(0o12).count("1") # FURB161
11+
count = bin(0x10 + 0x1000).count("1") # FURB161
12+
count = bin(ten()).count("1") # FURB161
13+
14+
count = x.bit_count() # OK
15+
count = (10).bit_count() # OK
16+
count = 0b1010.bit_count() # OK
17+
count = 0xA.bit_count() # OK
18+
count = 0o12.bit_count() # OK
19+
count = (0x10 + 0x1000).bit_count() # OK
20+
count = ten().bit_count() # OK

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
438438
}
439439
}
440440
}
441+
if checker.enabled(Rule::BitCount) {
442+
refurb::rules::bit_count(checker, call);
443+
}
441444
if checker.enabled(Rule::TypeOfPrimitive) {
442445
pyupgrade::rules::type_of_primitive(checker, expr, func, args);
443446
}

crates/ruff_linter/src/codes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
964964
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
965965
(Refurb, "148") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryEnumerate),
966966
(Refurb, "152") => (RuleGroup::Preview, rules::refurb::rules::MathConstant),
967+
(Refurb, "161") => (RuleGroup::Preview, rules::refurb::rules::BitCount),
967968
(Refurb, "163") => (RuleGroup::Preview, rules::refurb::rules::RedundantLogBase),
968969
(Refurb, "168") => (RuleGroup::Preview, rules::refurb::rules::IsinstanceTypeNone),
969970
(Refurb, "169") => (RuleGroup::Preview, rules::refurb::rules::TypeNoneComparison),

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ mod tests {
2727
#[test_case(Rule::PrintEmptyString, Path::new("FURB105.py"))]
2828
#[test_case(Rule::ImplicitCwd, Path::new("FURB177.py"))]
2929
#[test_case(Rule::SingleItemMembershipTest, Path::new("FURB171.py"))]
30+
#[test_case(Rule::BitCount, Path::new("FURB161.py"))]
3031
#[test_case(Rule::IsinstanceTypeNone, Path::new("FURB168.py"))]
3132
#[test_case(Rule::TypeNoneComparison, Path::new("FURB169.py"))]
3233
#[test_case(Rule::RedundantLogBase, Path::new("FURB163.py"))]
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
2+
use ruff_macros::{derive_message_formats, violation};
3+
use ruff_python_ast::{self as ast, Expr, ExprAttribute, ExprCall};
4+
use ruff_text_size::{Ranged, TextRange, TextSize};
5+
6+
use crate::{checkers::ast::Checker, settings::types::PythonVersion};
7+
8+
/// ## What it does
9+
/// Checks for the use of `bin(x).count("1")` as a population count.
10+
///
11+
/// ## Why is this bad?
12+
/// Python 3.10 added the `int.bit_count()` method, which is more efficient.
13+
///
14+
/// ## Example
15+
/// ```python
16+
/// x = bin(123).count("1")
17+
/// y = bin(0b1111011).count("1")
18+
/// ```
19+
///
20+
/// Use instead:
21+
/// ```python
22+
/// x = (123).bit_count()
23+
/// y = 0b1111011.bit_count()
24+
/// ```
25+
#[violation]
26+
pub struct BitCount {
27+
original_argument: String,
28+
replacement: String,
29+
}
30+
31+
impl AlwaysFixableViolation for BitCount {
32+
#[derive_message_formats]
33+
fn message(&self) -> String {
34+
let BitCount {
35+
original_argument, ..
36+
} = self;
37+
format!("Use of bin({original_argument}).count('1')")
38+
}
39+
40+
fn fix_title(&self) -> String {
41+
let BitCount { replacement, .. } = self;
42+
format!("Replace with `{replacement}`")
43+
}
44+
}
45+
46+
/// FURB161
47+
pub(crate) fn bit_count(checker: &mut Checker, call: &ExprCall) {
48+
if checker.settings.target_version < PythonVersion::Py310 {
49+
// `int.bit_count()` was added in Python 3.10
50+
return;
51+
}
52+
53+
let Expr::Attribute(ExprAttribute { attr, value, .. }) = call.func.as_ref() else {
54+
return;
55+
};
56+
57+
// make sure we're doing count
58+
if attr.as_str() != "count" {
59+
return;
60+
}
61+
62+
let Some(arg) = call.arguments.args.first() else {
63+
return;
64+
};
65+
66+
let Expr::StringLiteral(ast::ExprStringLiteral {
67+
value: count_value, ..
68+
}) = arg
69+
else {
70+
return;
71+
};
72+
73+
// make sure we're doing count("1")
74+
if count_value != "1" {
75+
return;
76+
}
77+
78+
let Expr::Call(ExprCall {
79+
func, arguments, ..
80+
}) = value.as_ref()
81+
else {
82+
return;
83+
};
84+
85+
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
86+
return;
87+
};
88+
89+
// make sure we're doing bin()
90+
if id.as_str() != "bin" {
91+
return;
92+
}
93+
94+
if arguments.len() != 1 {
95+
return;
96+
}
97+
98+
let Some(arg) = arguments.args.first() else {
99+
return;
100+
};
101+
102+
let literal_text = checker.locator().slice(arg.range());
103+
104+
let replacement = match arg {
105+
Expr::Name(ast::ExprName { id, .. }) => {
106+
format!("{id}.bit_count()")
107+
}
108+
Expr::NumberLiteral(ast::ExprNumberLiteral { .. }) => {
109+
let first_two_chars = checker
110+
.locator()
111+
.slice(TextRange::new(arg.start(), arg.start() + TextSize::from(2)));
112+
113+
match first_two_chars {
114+
"0b" | "0B" | "0x" | "0X" | "0o" | "0O" => format!("{literal_text}.bit_count()"),
115+
_ => format!("({literal_text}).bit_count()"),
116+
}
117+
}
118+
Expr::Call(ast::ExprCall { .. }) => {
119+
format!("{literal_text}.bit_count()")
120+
}
121+
_ => {
122+
format!("({literal_text}).bit_count()")
123+
}
124+
};
125+
126+
let mut diagnostic = Diagnostic::new(
127+
BitCount {
128+
original_argument: literal_text.to_string(),
129+
replacement: replacement.clone(),
130+
},
131+
call.range(),
132+
);
133+
134+
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
135+
replacement,
136+
call.range(),
137+
)));
138+
139+
checker.diagnostics.push(diagnostic);
140+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub(crate) use bit_count::*;
12
pub(crate) use check_and_remove_from_set::*;
23
pub(crate) use delete_full_slice::*;
34
pub(crate) use hashlib_digest_hex::*;
@@ -16,6 +17,7 @@ pub(crate) use slice_copy::*;
1617
pub(crate) use type_none_comparison::*;
1718
pub(crate) use unnecessary_enumerate::*;
1819

20+
mod bit_count;
1921
mod check_and_remove_from_set;
2022
mod delete_full_slice;
2123
mod hashlib_digest_hex;
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
---
2+
source: crates/ruff_linter/src/rules/refurb/mod.rs
3+
---
4+
FURB161.py:6:9: FURB161 [*] Use of bin(x).count('1')
5+
|
6+
4 | return 10
7+
5 |
8+
6 | count = bin(x).count("1") # FURB161
9+
| ^^^^^^^^^^^^^^^^^ FURB161
10+
7 | count = bin(10).count("1") # FURB161
11+
8 | count = bin(0b1010).count("1") # FURB161
12+
|
13+
= help: Replace with `x.bit_count()`
14+
15+
Safe fix
16+
3 3 | def ten() -> int:
17+
4 4 | return 10
18+
5 5 |
19+
6 |-count = bin(x).count("1") # FURB161
20+
6 |+count = x.bit_count() # FURB161
21+
7 7 | count = bin(10).count("1") # FURB161
22+
8 8 | count = bin(0b1010).count("1") # FURB161
23+
9 9 | count = bin(0xA).count("1") # FURB161
24+
25+
FURB161.py:7:9: FURB161 [*] Use of bin(10).count('1')
26+
|
27+
6 | count = bin(x).count("1") # FURB161
28+
7 | count = bin(10).count("1") # FURB161
29+
| ^^^^^^^^^^^^^^^^^^ FURB161
30+
8 | count = bin(0b1010).count("1") # FURB161
31+
9 | count = bin(0xA).count("1") # FURB161
32+
|
33+
= help: Replace with `(10).bit_count()`
34+
35+
Safe fix
36+
4 4 | return 10
37+
5 5 |
38+
6 6 | count = bin(x).count("1") # FURB161
39+
7 |-count = bin(10).count("1") # FURB161
40+
7 |+count = (10).bit_count() # FURB161
41+
8 8 | count = bin(0b1010).count("1") # FURB161
42+
9 9 | count = bin(0xA).count("1") # FURB161
43+
10 10 | count = bin(0o12).count("1") # FURB161
44+
45+
FURB161.py:8:9: FURB161 [*] Use of bin(0b1010).count('1')
46+
|
47+
6 | count = bin(x).count("1") # FURB161
48+
7 | count = bin(10).count("1") # FURB161
49+
8 | count = bin(0b1010).count("1") # FURB161
50+
| ^^^^^^^^^^^^^^^^^^^^^^ FURB161
51+
9 | count = bin(0xA).count("1") # FURB161
52+
10 | count = bin(0o12).count("1") # FURB161
53+
|
54+
= help: Replace with `0b1010.bit_count()`
55+
56+
Safe fix
57+
5 5 |
58+
6 6 | count = bin(x).count("1") # FURB161
59+
7 7 | count = bin(10).count("1") # FURB161
60+
8 |-count = bin(0b1010).count("1") # FURB161
61+
8 |+count = 0b1010.bit_count() # FURB161
62+
9 9 | count = bin(0xA).count("1") # FURB161
63+
10 10 | count = bin(0o12).count("1") # FURB161
64+
11 11 | count = bin(0x10 + 0x1000).count("1") # FURB161
65+
66+
FURB161.py:9:9: FURB161 [*] Use of bin(0xA).count('1')
67+
|
68+
7 | count = bin(10).count("1") # FURB161
69+
8 | count = bin(0b1010).count("1") # FURB161
70+
9 | count = bin(0xA).count("1") # FURB161
71+
| ^^^^^^^^^^^^^^^^^^^ FURB161
72+
10 | count = bin(0o12).count("1") # FURB161
73+
11 | count = bin(0x10 + 0x1000).count("1") # FURB161
74+
|
75+
= help: Replace with `0xA.bit_count()`
76+
77+
Safe fix
78+
6 6 | count = bin(x).count("1") # FURB161
79+
7 7 | count = bin(10).count("1") # FURB161
80+
8 8 | count = bin(0b1010).count("1") # FURB161
81+
9 |-count = bin(0xA).count("1") # FURB161
82+
9 |+count = 0xA.bit_count() # FURB161
83+
10 10 | count = bin(0o12).count("1") # FURB161
84+
11 11 | count = bin(0x10 + 0x1000).count("1") # FURB161
85+
12 12 | count = bin(ten()).count("1") # FURB161
86+
87+
FURB161.py:10:9: FURB161 [*] Use of bin(0o12).count('1')
88+
|
89+
8 | count = bin(0b1010).count("1") # FURB161
90+
9 | count = bin(0xA).count("1") # FURB161
91+
10 | count = bin(0o12).count("1") # FURB161
92+
| ^^^^^^^^^^^^^^^^^^^^ FURB161
93+
11 | count = bin(0x10 + 0x1000).count("1") # FURB161
94+
12 | count = bin(ten()).count("1") # FURB161
95+
|
96+
= help: Replace with `0o12.bit_count()`
97+
98+
Safe fix
99+
7 7 | count = bin(10).count("1") # FURB161
100+
8 8 | count = bin(0b1010).count("1") # FURB161
101+
9 9 | count = bin(0xA).count("1") # FURB161
102+
10 |-count = bin(0o12).count("1") # FURB161
103+
10 |+count = 0o12.bit_count() # FURB161
104+
11 11 | count = bin(0x10 + 0x1000).count("1") # FURB161
105+
12 12 | count = bin(ten()).count("1") # FURB161
106+
13 13 |
107+
108+
FURB161.py:11:9: FURB161 [*] Use of bin(0x10 + 0x1000).count('1')
109+
|
110+
9 | count = bin(0xA).count("1") # FURB161
111+
10 | count = bin(0o12).count("1") # FURB161
112+
11 | count = bin(0x10 + 0x1000).count("1") # FURB161
113+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB161
114+
12 | count = bin(ten()).count("1") # FURB161
115+
|
116+
= help: Replace with `(0x10 + 0x1000).bit_count()`
117+
118+
Safe fix
119+
8 8 | count = bin(0b1010).count("1") # FURB161
120+
9 9 | count = bin(0xA).count("1") # FURB161
121+
10 10 | count = bin(0o12).count("1") # FURB161
122+
11 |-count = bin(0x10 + 0x1000).count("1") # FURB161
123+
11 |+count = (0x10 + 0x1000).bit_count() # FURB161
124+
12 12 | count = bin(ten()).count("1") # FURB161
125+
13 13 |
126+
14 14 | count = x.bit_count() # OK
127+
128+
FURB161.py:12:9: FURB161 [*] Use of bin(ten()).count('1')
129+
|
130+
10 | count = bin(0o12).count("1") # FURB161
131+
11 | count = bin(0x10 + 0x1000).count("1") # FURB161
132+
12 | count = bin(ten()).count("1") # FURB161
133+
| ^^^^^^^^^^^^^^^^^^^^^ FURB161
134+
13 |
135+
14 | count = x.bit_count() # OK
136+
|
137+
= help: Replace with `ten().bit_count()`
138+
139+
Safe fix
140+
9 9 | count = bin(0xA).count("1") # FURB161
141+
10 10 | count = bin(0o12).count("1") # FURB161
142+
11 11 | count = bin(0x10 + 0x1000).count("1") # FURB161
143+
12 |-count = bin(ten()).count("1") # FURB161
144+
12 |+count = ten().bit_count() # FURB161
145+
13 13 |
146+
14 14 | count = x.bit_count() # OK
147+
15 15 | count = (10).bit_count() # OK
148+
149+

ruff.schema.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)