Implement `PLR2004` (`MagicValueComparison`) (#1828)

This PR adds [Pylint
`R2004`](https://pylint.pycqa.org/en/latest/user_guide/messages/refactor/magic-value-comparison.html#magic-value-comparison-r2004)

Feel free to suggest changes and additions, I have tried to maintain
parity with the Pylint implementation
[`magic_value.py`](https://github.com/PyCQA/pylint/blob/main/pylint/extensions/magic_value.py)

See #970
This commit is contained in:
max0x53 2023-01-13 00:44:18 +00:00 committed by GitHub
parent ef17c82998
commit b47e8e6770
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 206 additions and 0 deletions

View File

@ -1096,6 +1096,7 @@ For more, see [Pylint](https://pypi.org/project/pylint/2.15.7/) on PyPI.
| PLR0402 | ConsiderUsingFromImport | Use `from ... import ...` in lieu of alias | |
| PLR1701 | ConsiderMergingIsinstance | Merge these isinstance calls: `isinstance(..., (...))` | |
| PLR1722 | UseSysExit | Use `sys.exit()` instead of `exit` | 🛠 |
| PLR2004 | MagicValueComparison | Magic number used in comparison, consider replacing magic with a constant variable | |
| PLW0120 | UselessElseOnLoop | Else clause on loop without a break statement, remove the else and de-indent all the code inside it | |
| PLW0602 | GlobalVariableNotAssigned | Using global for `...` but no assignment is done | |

View File

@ -0,0 +1,71 @@
"""Check that magic values are not used in comparisons"""
import cmath
user_input = 10
if 10 > user_input: # [magic-value-comparison]
pass
if 10 == 100: # [comparison-of-constants] R0133
pass
if 1 == 3: # [comparison-of-constants] R0133
pass
x = 0
if 4 == 3 == x: # [comparison-of-constants] R0133
pass
time_delta = 7224
ONE_HOUR = 3600
if time_delta > ONE_HOUR: # correct
pass
argc = 1
if argc != -1: # correct
pass
if argc != 0: # correct
pass
if argc != 1: # correct
pass
if argc != 2: # [magic-value-comparison]
pass
if __name__ == "__main__": # correct
pass
ADMIN_PASSWORD = "SUPERSECRET"
input_password = "password"
if input_password == "": # correct
pass
if input_password == ADMIN_PASSWORD: # correct
pass
if input_password == "Hunter2": # [magic-value-comparison]
pass
PI = 3.141592653589793238
pi_estimation = 3.14
if pi_estimation == 3.141592653589793238: # [magic-value-comparison]
pass
if pi_estimation == PI: # correct
pass
HELLO_WORLD = b"Hello, World!"
user_input = b"Hello, There!"
if user_input == b"something": # [magic-value-comparison]
pass
if user_input == HELLO_WORLD: # correct
pass

View File

@ -1445,6 +1445,10 @@
"PLR1701",
"PLR172",
"PLR1722",
"PLR2",
"PLR20",
"PLR200",
"PLR2004",
"PLW",
"PLW0",
"PLW01",

View File

@ -2590,6 +2590,10 @@ where
);
}
if self.settings.enabled.contains(&RuleCode::PLR2004) {
pylint::rules::magic_value_comparison(self, left, comparators);
}
if self.settings.enabled.contains(&RuleCode::SIM118) {
flake8_simplify::rules::key_in_dict_compare(self, expr, left, ops, comparators);
}

View File

@ -27,6 +27,7 @@ mod tests {
#[test_case(RuleCode::PLR1722, Path::new("consider_using_sys_exit_4.py"); "PLR1722_4")]
#[test_case(RuleCode::PLR1722, Path::new("consider_using_sys_exit_5.py"); "PLR1722_5")]
#[test_case(RuleCode::PLR1722, Path::new("consider_using_sys_exit_6.py"); "PLR1722_6")]
#[test_case(RuleCode::PLR2004, Path::new("magic_value_comparison.py"); "PLR2004")]
#[test_case(RuleCode::PLW0120, Path::new("useless_else_on_loop.py"); "PLW0120")]
#[test_case(RuleCode::PLW0602, Path::new("global_variable_not_assigned.py"); "PLW0602")]
fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {

View File

@ -0,0 +1,51 @@
use itertools::Itertools;
use rustpython_ast::{Constant, Expr, ExprKind};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::violations;
fn is_magic_value(constant: &Constant) -> bool {
match constant {
Constant::None => false,
// E712 `if True == do_something():`
Constant::Bool(_) => false,
Constant::Str(value) => !matches!(value.as_str(), "" | "__main__"),
Constant::Bytes(_) => true,
Constant::Int(value) => !matches!(value.try_into(), Ok(-1 | 0 | 1)),
Constant::Tuple(_) => true,
Constant::Float(_) => true,
Constant::Complex { .. } => true,
Constant::Ellipsis => true,
}
}
/// PLR2004
pub fn magic_value_comparison(checker: &mut Checker, left: &Expr, comparators: &[Expr]) {
for (left, right) in std::iter::once(left)
.chain(comparators.iter())
.tuple_windows()
{
// If both of the comparators are constant, skip rule for the whole expression.
// R0133: comparison-of-constants
if matches!(left.node, ExprKind::Constant { .. })
&& matches!(right.node, ExprKind::Constant { .. })
{
return;
}
}
for comparison_expr in std::iter::once(left).chain(comparators.iter()) {
if let ExprKind::Constant { value, .. } = &comparison_expr.node {
if is_magic_value(value) {
let diagnostic = Diagnostic::new(
violations::MagicValueComparison(value.to_string()),
Range::from_located(comparison_expr),
);
checker.diagnostics.push(diagnostic);
}
}
}
}

View File

@ -1,4 +1,5 @@
pub use await_outside_async::await_outside_async;
pub use magic_value_comparison::magic_value_comparison;
pub use merge_isinstance::merge_isinstance;
pub use misplaced_comparison_constant::misplaced_comparison_constant;
pub use property_with_parameters::property_with_parameters;
@ -10,6 +11,7 @@ pub use useless_else_on_loop::useless_else_on_loop;
pub use useless_import_alias::useless_import_alias;
mod await_outside_async;
mod magic_value_comparison;
mod merge_isinstance;
mod misplaced_comparison_constant;
mod property_with_parameters;

View File

@ -0,0 +1,55 @@
---
source: src/pylint/mod.rs
expression: diagnostics
---
- kind:
MagicValueComparison: "10"
location:
row: 6
column: 3
end_location:
row: 6
column: 5
fix: ~
parent: ~
- kind:
MagicValueComparison: "2"
location:
row: 36
column: 11
end_location:
row: 36
column: 12
fix: ~
parent: ~
- kind:
MagicValueComparison: "'Hunter2'"
location:
row: 51
column: 21
end_location:
row: 51
column: 30
fix: ~
parent: ~
- kind:
MagicValueComparison: "3.141592653589793"
location:
row: 57
column: 20
end_location:
row: 57
column: 40
fix: ~
parent: ~
- kind:
MagicValueComparison: "b'something'"
location:
row: 66
column: 17
end_location:
row: 66
column: 29
fix: ~
parent: ~

View File

@ -189,6 +189,7 @@ define_rule_mapping!(
PLR0402 => violations::ConsiderUsingFromImport,
PLR1701 => violations::ConsiderMergingIsinstance,
PLR1722 => violations::UseSysExit,
PLR2004 => violations::MagicValueComparison,
PLW0120 => violations::UselessElseOnLoop,
PLW0602 => violations::GlobalVariableNotAssigned,
// flake8-builtins

View File

@ -1191,6 +1191,22 @@ impl AlwaysAutofixableViolation for UseSysExit {
}
}
define_violation!(
pub struct MagicValueComparison(pub String);
);
impl Violation for MagicValueComparison {
fn message(&self) -> String {
let MagicValueComparison(value) = self;
format!(
"Magic number used in comparison, consider replacing {value} with a constant variable"
)
}
fn placeholder() -> Self {
MagicValueComparison("magic".to_string())
}
}
define_violation!(
pub struct UselessElseOnLoop;
);