Skip to content

Commit

Permalink
[refurb] - implement FURB161/use-bit-count
Browse files Browse the repository at this point in the history
  • Loading branch information
diceroll123 committed Dec 24, 2023
1 parent 20def33 commit fd9c700
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 0 deletions.
20 changes: 20 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/refurb/FURB161.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
x = 10

def ten() -> int:
return 10

count = bin(x).count("1") # FURB161
count = bin(10).count("1") # FURB161
count = bin(0b1010).count("1") # FURB161
count = bin(0xA).count("1") # FURB161
count = bin(0o12).count("1") # FURB161
count = bin(0x10 + 0x1000).count("1") # FURB161
count = bin(ten()).count("1") # FURB161

count = x.bit_count() # OK
count = (10).bit_count() # OK
count = 0b1010.bit_count() # OK
count = 0xA.bit_count() # OK
count = 0o12.bit_count() # OK
count = (0x10 + 0x1000).bit_count() # OK
count = ten().bit_count() # OK
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/checkers/ast/analyze/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
}
if checker.enabled(Rule::BitCount) {
refurb::rules::bit_count(checker, call);
}
if checker.enabled(Rule::TypeOfPrimitive) {
pyupgrade::rules::type_of_primitive(checker, expr, func, args);
}
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
(Refurb, "148") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryEnumerate),
(Refurb, "152") => (RuleGroup::Preview, rules::refurb::rules::MathConstant),
(Refurb, "161") => (RuleGroup::Preview, rules::refurb::rules::BitCount),
(Refurb, "163") => (RuleGroup::Preview, rules::refurb::rules::RedundantLogBase),
(Refurb, "168") => (RuleGroup::Preview, rules::refurb::rules::IsinstanceTypeNone),
(Refurb, "169") => (RuleGroup::Preview, rules::refurb::rules::TypeNoneComparison),
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/refurb/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ mod tests {
#[test_case(Rule::PrintEmptyString, Path::new("FURB105.py"))]
#[test_case(Rule::ImplicitCwd, Path::new("FURB177.py"))]
#[test_case(Rule::SingleItemMembershipTest, Path::new("FURB171.py"))]
#[test_case(Rule::BitCount, Path::new("FURB161.py"))]
#[test_case(Rule::IsinstanceTypeNone, Path::new("FURB168.py"))]
#[test_case(Rule::TypeNoneComparison, Path::new("FURB169.py"))]
#[test_case(Rule::RedundantLogBase, Path::new("FURB163.py"))]
Expand Down
140 changes: 140 additions & 0 deletions crates/ruff_linter/src/rules/refurb/rules/bit_count.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, ExprAttribute, ExprCall};
use ruff_text_size::{Ranged, TextRange, TextSize};

use crate::{checkers::ast::Checker, settings::types::PythonVersion};

/// ## What it does
/// Checks for the use of `bin(x).count("1")` as a population count.
///
/// ## Why is this bad?
/// Python 3.10 added the `int.bit_count()` method, which is more efficient.
///
/// ## Example
/// ```python
/// x = bin(123).count("1")
/// y = bin(0b1111011).count("1")
/// ```
///
/// Use instead:
/// ```python
/// x = (123).bit_count()
/// y = 0b1111011.bit_count()
/// ```
#[violation]
pub struct BitCount {
original_argument: String,
replacement: String,
}

impl AlwaysFixableViolation for BitCount {
#[derive_message_formats]
fn message(&self) -> String {
let BitCount {
original_argument, ..
} = self;
format!("Use of bin({original_argument}).count('1')")
}

fn fix_title(&self) -> String {
let BitCount { replacement, .. } = self;
format!("Replace with `{replacement}`")
}
}

/// FURB161
pub(crate) fn bit_count(checker: &mut Checker, call: &ExprCall) {
if checker.settings.target_version < PythonVersion::Py310 {
// `int.bit_count()` was added in Python 3.10
return;
}

let Expr::Attribute(ExprAttribute { attr, value, .. }) = call.func.as_ref() else {
return;
};

// make sure we're doing count
if attr.as_str() != "count" {
return;
}

let Some(arg) = call.arguments.args.first() else {
return;
};

let Expr::StringLiteral(ast::ExprStringLiteral {
value: count_value, ..
}) = arg
else {
return;
};

// make sure we're doing count("1")
if count_value != "1" {
return;
}

let Expr::Call(ExprCall {
func, arguments, ..
}) = value.as_ref()
else {
return;
};

let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
return;
};

// make sure we're doing bin()
if id.as_str() != "bin" {
return;
}

if arguments.len() != 1 {
return;
}

let Some(arg) = arguments.args.first() else {
return;
};

let literal_text = checker.locator().slice(arg.range());

let replacement = match arg {
Expr::Name(ast::ExprName { id, .. }) => {
format!("{id}.bit_count()")
}
Expr::NumberLiteral(ast::ExprNumberLiteral { .. }) => {
let first_two_chars = checker
.locator()
.slice(TextRange::new(arg.start(), arg.start() + TextSize::from(2)));

match first_two_chars {
"0b" | "0B" | "0x" | "0X" | "0o" | "0O" => format!("{literal_text}.bit_count()"),
_ => format!("({literal_text}).bit_count()"),
}
}
Expr::Call(ast::ExprCall { .. }) => {
format!("{literal_text}.bit_count()")
}
_ => {
format!("({literal_text}).bit_count()")
}
};

let mut diagnostic = Diagnostic::new(
BitCount {
original_argument: literal_text.to_string(),
replacement: replacement.clone(),
},
call.range(),
);

diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
replacement,
call.range(),
)));

checker.diagnostics.push(diagnostic);
}
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/refurb/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub(crate) use bit_count::*;
pub(crate) use check_and_remove_from_set::*;
pub(crate) use delete_full_slice::*;
pub(crate) use hashlib_digest_hex::*;
Expand All @@ -16,6 +17,7 @@ pub(crate) use slice_copy::*;
pub(crate) use type_none_comparison::*;
pub(crate) use unnecessary_enumerate::*;

mod bit_count;
mod check_and_remove_from_set;
mod delete_full_slice;
mod hashlib_digest_hex;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB161.py:6:9: FURB161 [*] Use of bin(x).count('1')
|
4 | return 10
5 |
6 | count = bin(x).count("1") # FURB161
| ^^^^^^^^^^^^^^^^^ FURB161
7 | count = bin(10).count("1") # FURB161
8 | count = bin(0b1010).count("1") # FURB161
|
= help: Replace with `x.bit_count()`

Safe fix
3 3 | def ten() -> int:
4 4 | return 10
5 5 |
6 |-count = bin(x).count("1") # FURB161
6 |+count = x.bit_count() # FURB161
7 7 | count = bin(10).count("1") # FURB161
8 8 | count = bin(0b1010).count("1") # FURB161
9 9 | count = bin(0xA).count("1") # FURB161

FURB161.py:7:9: FURB161 [*] Use of bin(10).count('1')
|
6 | count = bin(x).count("1") # FURB161
7 | count = bin(10).count("1") # FURB161
| ^^^^^^^^^^^^^^^^^^ FURB161
8 | count = bin(0b1010).count("1") # FURB161
9 | count = bin(0xA).count("1") # FURB161
|
= help: Replace with `(10).bit_count()`

Safe fix
4 4 | return 10
5 5 |
6 6 | count = bin(x).count("1") # FURB161
7 |-count = bin(10).count("1") # FURB161
7 |+count = (10).bit_count() # FURB161
8 8 | count = bin(0b1010).count("1") # FURB161
9 9 | count = bin(0xA).count("1") # FURB161
10 10 | count = bin(0o12).count("1") # FURB161

FURB161.py:8:9: FURB161 [*] Use of bin(0b1010).count('1')
|
6 | count = bin(x).count("1") # FURB161
7 | count = bin(10).count("1") # FURB161
8 | count = bin(0b1010).count("1") # FURB161
| ^^^^^^^^^^^^^^^^^^^^^^ FURB161
9 | count = bin(0xA).count("1") # FURB161
10 | count = bin(0o12).count("1") # FURB161
|
= help: Replace with `0b1010.bit_count()`

Safe fix
5 5 |
6 6 | count = bin(x).count("1") # FURB161
7 7 | count = bin(10).count("1") # FURB161
8 |-count = bin(0b1010).count("1") # FURB161
8 |+count = 0b1010.bit_count() # FURB161
9 9 | count = bin(0xA).count("1") # FURB161
10 10 | count = bin(0o12).count("1") # FURB161
11 11 | count = bin(0x10 + 0x1000).count("1") # FURB161

FURB161.py:9:9: FURB161 [*] Use of bin(0xA).count('1')
|
7 | count = bin(10).count("1") # FURB161
8 | count = bin(0b1010).count("1") # FURB161
9 | count = bin(0xA).count("1") # FURB161
| ^^^^^^^^^^^^^^^^^^^ FURB161
10 | count = bin(0o12).count("1") # FURB161
11 | count = bin(0x10 + 0x1000).count("1") # FURB161
|
= help: Replace with `0xA.bit_count()`

Safe fix
6 6 | count = bin(x).count("1") # FURB161
7 7 | count = bin(10).count("1") # FURB161
8 8 | count = bin(0b1010).count("1") # FURB161
9 |-count = bin(0xA).count("1") # FURB161
9 |+count = 0xA.bit_count() # FURB161
10 10 | count = bin(0o12).count("1") # FURB161
11 11 | count = bin(0x10 + 0x1000).count("1") # FURB161
12 12 | count = bin(ten()).count("1") # FURB161

FURB161.py:10:9: FURB161 [*] Use of bin(0o12).count('1')
|
8 | count = bin(0b1010).count("1") # FURB161
9 | count = bin(0xA).count("1") # FURB161
10 | count = bin(0o12).count("1") # FURB161
| ^^^^^^^^^^^^^^^^^^^^ FURB161
11 | count = bin(0x10 + 0x1000).count("1") # FURB161
12 | count = bin(ten()).count("1") # FURB161
|
= help: Replace with `0o12.bit_count()`

Safe fix
7 7 | count = bin(10).count("1") # FURB161
8 8 | count = bin(0b1010).count("1") # FURB161
9 9 | count = bin(0xA).count("1") # FURB161
10 |-count = bin(0o12).count("1") # FURB161
10 |+count = 0o12.bit_count() # FURB161
11 11 | count = bin(0x10 + 0x1000).count("1") # FURB161
12 12 | count = bin(ten()).count("1") # FURB161
13 13 |

FURB161.py:11:9: FURB161 [*] Use of bin(0x10 + 0x1000).count('1')
|
9 | count = bin(0xA).count("1") # FURB161
10 | count = bin(0o12).count("1") # FURB161
11 | count = bin(0x10 + 0x1000).count("1") # FURB161
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB161
12 | count = bin(ten()).count("1") # FURB161
|
= help: Replace with `(0x10 + 0x1000).bit_count()`

Safe fix
8 8 | count = bin(0b1010).count("1") # FURB161
9 9 | count = bin(0xA).count("1") # FURB161
10 10 | count = bin(0o12).count("1") # FURB161
11 |-count = bin(0x10 + 0x1000).count("1") # FURB161
11 |+count = (0x10 + 0x1000).bit_count() # FURB161
12 12 | count = bin(ten()).count("1") # FURB161
13 13 |
14 14 | count = x.bit_count() # OK

FURB161.py:12:9: FURB161 [*] Use of bin(ten()).count('1')
|
10 | count = bin(0o12).count("1") # FURB161
11 | count = bin(0x10 + 0x1000).count("1") # FURB161
12 | count = bin(ten()).count("1") # FURB161
| ^^^^^^^^^^^^^^^^^^^^^ FURB161
13 |
14 | count = x.bit_count() # OK
|
= help: Replace with `ten().bit_count()`

Safe fix
9 9 | count = bin(0xA).count("1") # FURB161
10 10 | count = bin(0o12).count("1") # FURB161
11 11 | count = bin(0x10 + 0x1000).count("1") # FURB161
12 |-count = bin(ten()).count("1") # FURB161
12 |+count = ten().bit_count() # FURB161
13 13 |
14 14 | count = x.bit_count() # OK
15 15 | count = (10).bit_count() # OK


1 change: 1 addition & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit fd9c700

Please sign in to comment.