Skip to content

Commit

Permalink
[pylint] C1901: compare-to-empty-string (#3405)
Browse files Browse the repository at this point in the history
  • Loading branch information
AreamanM authored Mar 9, 2023
1 parent 024caca commit 952307d
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
x = "a string"
y = "another string"
z = ""


def errors():
if x is "" or x == "":
print("x is an empty string")

if y is not "" or y != "":
print("y is not an empty string")

if "" != z:
print("z is an empty string")


def ok():
if x and not y:
print("x is not an empty string, but y is an empty string")
4 changes: 4 additions & 0 deletions crates/ruff/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3299,6 +3299,10 @@ where
pylint::rules::comparison_of_constant(self, left, ops, comparators);
}

if self.settings.rules.enabled(&Rule::CompareToEmptyString) {
pylint::rules::compare_to_empty_string(self, left, ops, comparators);
}

if self.settings.rules.enabled(&Rule::MagicValueComparison) {
pylint::rules::magic_value_comparison(self, left, comparators);
}
Expand Down
25 changes: 13 additions & 12 deletions crates/ruff/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,34 +161,35 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Pyflakes, "901") => Rule::RaiseNotImplemented,

// pylint
(Pylint, "C0414") => Rule::UselessImportAlias,
(Pylint, "C1901") => Rule::CompareToEmptyString,
(Pylint, "C3002") => Rule::UnnecessaryDirectLambdaCall,
(Pylint, "E0100") => Rule::YieldInInit,
(Pylint, "E0101") => Rule::ReturnInInit,
(Pylint, "E0117") => Rule::NonlocalWithoutBinding,
(Pylint, "E0118") => Rule::UsedPriorGlobalDeclaration,
(Pylint, "E0604") => Rule::InvalidAllObject,
(Pylint, "E0605") => Rule::InvalidAllFormat,
(Pylint, "E1142") => Rule::AwaitOutsideAsync,
(Pylint, "E1205") => Rule::LoggingTooManyArgs,
(Pylint, "E1206") => Rule::LoggingTooFewArgs,
(Pylint, "E1307") => Rule::BadStringFormatType,
(Pylint, "E2502") => Rule::BidirectionalUnicode,
(Pylint, "E1310") => Rule::BadStrStripCall,
(Pylint, "C0414") => Rule::UselessImportAlias,
(Pylint, "C3002") => Rule::UnnecessaryDirectLambdaCall,
(Pylint, "E0117") => Rule::NonlocalWithoutBinding,
(Pylint, "E0118") => Rule::UsedPriorGlobalDeclaration,
(Pylint, "E1142") => Rule::AwaitOutsideAsync,
(Pylint, "E2502") => Rule::BidirectionalUnicode,
(Pylint, "R0133") => Rule::ComparisonOfConstant,
(Pylint, "R0206") => Rule::PropertyWithParameters,
(Pylint, "R0402") => Rule::ConsiderUsingFromImport,
(Pylint, "R0133") => Rule::ComparisonOfConstant,
(Pylint, "R0911") => Rule::TooManyReturnStatements,
(Pylint, "R0912") => Rule::TooManyBranches,
(Pylint, "R0913") => Rule::TooManyArguments,
(Pylint, "R0915") => Rule::TooManyStatements,
(Pylint, "R1701") => Rule::ConsiderMergingIsinstance,
(Pylint, "R1722") => Rule::ConsiderUsingSysExit,
(Pylint, "R5501") => Rule::CollapsibleElseIf,
(Pylint, "R2004") => Rule::MagicValueComparison,
(Pylint, "R5501") => Rule::CollapsibleElseIf,
(Pylint, "W0120") => Rule::UselessElseOnLoop,
(Pylint, "W0602") => Rule::GlobalVariableNotAssigned,
(Pylint, "W0603") => Rule::GlobalStatement,
(Pylint, "R0911") => Rule::TooManyReturnStatements,
(Pylint, "R0913") => Rule::TooManyArguments,
(Pylint, "R0912") => Rule::TooManyBranches,
(Pylint, "R0915") => Rule::TooManyStatements,
(Pylint, "W2901") => Rule::RedefinedLoopName,

// flake8-builtins
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ ruff_macros::register_rules!(
rules::pylint::rules::PropertyWithParameters,
rules::pylint::rules::ReturnInInit,
rules::pylint::rules::ConsiderUsingFromImport,
rules::pylint::rules::CompareToEmptyString,
rules::pylint::rules::ComparisonOfConstant,
rules::pylint::rules::ConsiderMergingIsinstance,
rules::pylint::rules::ConsiderUsingSysExit,
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/rules/pylint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod tests {
#[test_case(Rule::NonlocalWithoutBinding, Path::new("nonlocal_without_binding.py"); "PLE0117")]
#[test_case(Rule::UsedPriorGlobalDeclaration, Path::new("used_prior_global_declaration.py"); "PLE0118")]
#[test_case(Rule::AwaitOutsideAsync, Path::new("await_outside_async.py"); "PLE1142")]
#[test_case(Rule::CompareToEmptyString, Path::new("compare_to_empty_string.py"); "PLC1901")]
#[test_case(Rule::ComparisonOfConstant, Path::new("comparison_of_constant.py"); "PLR0133")]
#[test_case(Rule::PropertyWithParameters, Path::new("property_with_parameters.py"); "PLR0206")]
#[test_case(Rule::ConsiderUsingFromImport, Path::new("import_aliasing.py"); "PLR0402")]
Expand Down
125 changes: 125 additions & 0 deletions crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use anyhow::bail;
use itertools::Itertools;
use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind};

use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{unparse_constant, unparse_expr};
use ruff_python_ast::types::Range;

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

#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum EmptyStringCmpop {
Is,
IsNot,
Eq,
NotEq,
}

impl TryFrom<&Cmpop> for EmptyStringCmpop {
type Error = anyhow::Error;

fn try_from(value: &Cmpop) -> Result<Self, Self::Error> {
match value {
Cmpop::Is => Ok(Self::Is),
Cmpop::IsNot => Ok(Self::IsNot),
Cmpop::Eq => Ok(Self::Eq),
Cmpop::NotEq => Ok(Self::NotEq),
_ => bail!("{value:?} cannot be converted to EmptyStringCmpop"),
}
}
}

impl EmptyStringCmpop {
pub fn into_unary(self) -> &'static str {
match self {
Self::Is | Self::Eq => "",
Self::IsNot | Self::NotEq => "not ",
}
}
}

impl std::fmt::Display for EmptyStringCmpop {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let repr = match self {
Self::Is => "is",
Self::IsNot => "is not",
Self::Eq => "==",
Self::NotEq => "!=",
};
write!(f, "{repr}")
}
}

#[violation]
pub struct CompareToEmptyString {
pub existing: String,
pub replacement: String,
}

impl Violation for CompareToEmptyString {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"`{}` can be simplified to `{}` as an empty string is falsey",
self.existing, self.replacement,
)
}
}

pub fn compare_to_empty_string(
checker: &mut Checker,
left: &Expr,
ops: &[Cmpop],
comparators: &[Expr],
) {
let mut first = true;
for ((lhs, rhs), op) in std::iter::once(left)
.chain(comparators.iter())
.tuple_windows::<(&Expr<_>, &Expr<_>)>()
.zip(ops)
{
if let Ok(op) = EmptyStringCmpop::try_from(op) {
if std::mem::take(&mut first) {
// Check the left-most expression.
if let ExprKind::Constant { value, .. } = &lhs.node {
if let Constant::Str(s) = value {
if s.is_empty() {
let constant = unparse_constant(value, checker.stylist);
let expr = unparse_expr(rhs, checker.stylist);
let existing = format!("{constant} {op} {expr}");
let replacement = format!("{}{expr}", op.into_unary());
checker.diagnostics.push(Diagnostic::new(
CompareToEmptyString {
existing,
replacement,
},
Range::from(lhs),
));
}
}
}
}

// Check all right-hand expressions.
if let ExprKind::Constant { value, .. } = &rhs.node {
if let Constant::Str(s) = value {
if s.is_empty() {
let expr = unparse_expr(lhs, checker.stylist);
let constant = unparse_constant(value, checker.stylist);
let existing = format!("{expr} {op} {constant}");
let replacement = format!("{}{expr}", op.into_unary());
checker.diagnostics.push(Diagnostic::new(
CompareToEmptyString {
existing,
replacement,
},
Range::from(rhs),
));
}
}
}
}
}
}
2 changes: 2 additions & 0 deletions crates/ruff/src/rules/pylint/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub use bad_str_strip_call::{bad_str_strip_call, BadStrStripCall};
pub use bad_string_format_type::{bad_string_format_type, BadStringFormatType};
pub use bidirectional_unicode::{bidirectional_unicode, BidirectionalUnicode};
pub use collapsible_else_if::{collapsible_else_if, CollapsibleElseIf};
pub use compare_to_empty_string::{compare_to_empty_string, CompareToEmptyString};
pub use comparison_of_constant::{comparison_of_constant, ComparisonOfConstant};
pub use consider_using_sys_exit::{consider_using_sys_exit, ConsiderUsingSysExit};
pub use global_statement::{global_statement, GlobalStatement};
Expand Down Expand Up @@ -36,6 +37,7 @@ mod bad_str_strip_call;
mod bad_string_format_type;
mod bidirectional_unicode;
mod collapsible_else_if;
mod compare_to_empty_string;
mod comparison_of_constant;
mod consider_using_sys_exit;
mod global_statement;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
source: crates/ruff/src/rules/pylint/mod.rs
expression: diagnostics
---
- kind:
name: CompareToEmptyString
body: "`x is \"\"` can be simplified to `x` as an empty string is falsey"
suggestion: ~
fixable: false
location:
row: 7
column: 12
end_location:
row: 7
column: 14
fix: ~
parent: ~
- kind:
name: CompareToEmptyString
body: "`x == \"\"` can be simplified to `x` as an empty string is falsey"
suggestion: ~
fixable: false
location:
row: 7
column: 23
end_location:
row: 7
column: 25
fix: ~
parent: ~
- kind:
name: CompareToEmptyString
body: "`y is not \"\"` can be simplified to `not y` as an empty string is falsey"
suggestion: ~
fixable: false
location:
row: 10
column: 16
end_location:
row: 10
column: 18
fix: ~
parent: ~
- kind:
name: CompareToEmptyString
body: "`y != \"\"` can be simplified to `not y` as an empty string is falsey"
suggestion: ~
fixable: false
location:
row: 10
column: 27
end_location:
row: 10
column: 29
fix: ~
parent: ~
- kind:
name: CompareToEmptyString
body: "`\"\" != z` can be simplified to `not z` as an empty string is falsey"
suggestion: ~
fixable: false
location:
row: 13
column: 7
end_location:
row: 13
column: 9
fix: ~
parent: ~

4 changes: 4 additions & 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 952307d

Please sign in to comment.