Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pylint] - add unnecessary-list-index-lookup (PLR1736) + autofix #7999

Merged
merged 20 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
letters = ["a", "b", "c"]


def fix_these():
[letters[index] for index, letter in enumerate(letters)] # PLR1736
{letters[index] for index, letter in enumerate(letters)} # PLR1736
{letter: letters[index] for index, letter in enumerate(letters)} # PLR1736

for index, letter in enumerate(letters):
print(letters[index]) # PLR1736
blah = letters[index] # PLR1736
assert letters[index] == "d" # PLR1736


def dont_fix_these():
# once there is an assignment to the sequence[index], we stop emitting diagnostics
for index, letter in enumerate(letters):
letters[index] = "d" # Ok
assert letters[index] == "d" # Ok
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about a case where index is mutated? e.g. index += 1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a case for that, as well as the sequence itself being mutated! Thanks!



def value_intentionally_unused():
[letters[index] for index, _ in enumerate(letters)] # PLR1736
{letters[index] for index, _ in enumerate(letters)} # PLR1736
{index: letters[index] for index, _ in enumerate(letters)} # PLR1736

for index, _ in enumerate(letters):
print(letters[index]) # Ok
blah = letters[index] # Ok
letters[index] = "d" # Ok
12 changes: 12 additions & 0 deletions crates/ruff_linter/src/checkers/ast/analyze/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
},
) => {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_list_set_comprehension(
checker, expr, elt, generators,
Expand All @@ -1341,6 +1344,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
},
) => {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_list_set_comprehension(
checker, expr, elt, generators,
Expand All @@ -1364,6 +1370,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
generators,
range: _,
}) => {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_dict_comprehension(
checker, expr, key, value, generators,
Expand All @@ -1388,6 +1397,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
},
) => {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::FunctionUsesLoopVariable) {
flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Expr(expr));
}
Expand Down
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/checkers/ast/analyze/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListCast) {
perflint::rules::unnecessary_list_cast(checker, iter, body);
}
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup(checker, for_stmt);
}
if !is_async {
if checker.enabled(Rule::ReimplementedBuiltin) {
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);
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 @@ -258,6 +258,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R1714") => (RuleGroup::Stable, rules::pylint::rules::RepeatedEqualityComparison),
(Pylint, "R1706") => (RuleGroup::Preview, rules::pylint::rules::AndOrTernary),
(Pylint, "R1722") => (RuleGroup::Stable, rules::pylint::rules::SysExitAlias),
(Pylint, "R1736") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryListIndexLookup),
(Pylint, "R2004") => (RuleGroup::Stable, rules::pylint::rules::MagicValueComparison),
(Pylint, "R5501") => (RuleGroup::Stable, rules::pylint::rules::CollapsibleElseIf),
(Pylint, "R6201") => (RuleGroup::Preview, rules::pylint::rules::LiteralMembership),
Expand Down
4 changes: 4 additions & 0 deletions crates/ruff_linter/src/rules/pylint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ mod tests {
Rule::RepeatedKeywordArgument,
Path::new("repeated_keyword_argument.py")
)]
#[test_case(
Rule::UnnecessaryListIndexLookup,
Path::new("unnecessary_list_index_lookup.py")
)]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/pylint/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub(crate) use type_param_name_mismatch::*;
pub(crate) use unexpected_special_method_signature::*;
pub(crate) use unnecessary_direct_lambda_call::*;
pub(crate) use unnecessary_lambda::*;
pub(crate) use unnecessary_list_index_lookup::*;
pub(crate) use unspecified_encoding::*;
pub(crate) use useless_else_on_loop::*;
pub(crate) use useless_import_alias::*;
Expand Down Expand Up @@ -136,6 +137,7 @@ mod type_param_name_mismatch;
mod unexpected_special_method_signature;
mod unnecessary_direct_lambda_call;
mod unnecessary_lambda;
mod unnecessary_list_index_lookup;
mod unspecified_encoding;
mod useless_else_on_loop;
mod useless_import_alias;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
use ruff_python_ast::{self as ast, Expr, Stmt, StmtFor};

use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
use ruff_text_size::TextRange;

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

/// ## What it does
/// Checks for uses of enumeration and accessing the value by index lookup.
diceroll123 marked this conversation as resolved.
Show resolved Hide resolved
///
/// ## Why is this bad?
/// The value is already accessible by the 2nd variable from the enumeration.
diceroll123 marked this conversation as resolved.
Show resolved Hide resolved
///
/// ## Example
/// ```python
/// letters = ["a", "b", "c"]
///
/// for index, letter in enumerate(letters):
/// print(letters[index])
/// ```
///
/// Use instead:
/// ```python
/// letters = ["a", "b", "c"]
///
/// for index, letter in enumerate(letters):
/// print(letter)
/// ```
#[violation]
pub struct UnnecessaryListIndexLookup;

impl AlwaysFixableViolation for UnnecessaryListIndexLookup {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary list index lookup")
diceroll123 marked this conversation as resolved.
Show resolved Hide resolved
}

fn fix_title(&self) -> String {
format!("Remove unnecessary list index lookup")
diceroll123 marked this conversation as resolved.
Show resolved Hide resolved
}
}

struct SubscriptVisitor<'a> {
sequence_name: &'a str,
index_name: &'a str,
diagnostic_ranges: Vec<TextRange>,
is_subcript_modified: bool,
}

impl<'a> SubscriptVisitor<'a> {
fn new(sequence_name: &'a str, index_name: &'a str) -> Self {
Self {
sequence_name,
index_name,
diagnostic_ranges: Vec::new(),
is_subcript_modified: false,
}
}
}

fn check_target_for_assignment(expr: &Expr, sequence_name: &str, index_name: &str) -> bool {
// if we see the sequence subscript being modified, we'll stop emitting diagnostics
match expr {
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
if let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() {
if id == sequence_name {
if let Expr::Name(ast::ExprName { id, .. }) = slice.as_ref() {
if id == index_name {
return true;
}
}
}
}
false
}
_ => false,
}
}

impl<'a> Visitor<'_> for SubscriptVisitor<'a> {
fn visit_expr(&mut self, expr: &Expr) {
if self.is_subcript_modified {
return;
}
match expr {
Expr::Subscript(ast::ExprSubscript {
value,
slice,
range,
..
}) => {
if let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() {
if id == self.sequence_name {
if let Expr::Name(ast::ExprName { id, .. }) = slice.as_ref() {
if id == self.index_name {
self.diagnostic_ranges.push(*range);
}
}
}
}
dhruvmanila marked this conversation as resolved.
Show resolved Hide resolved
}
_ => visitor::walk_expr(self, expr),
}
}

fn visit_stmt(&mut self, stmt: &Stmt) {
if self.is_subcript_modified {
return;
}
match stmt {
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
self.is_subcript_modified = targets.iter().any(|target| {
check_target_for_assignment(target, self.sequence_name, self.index_name)
});
self.visit_expr(value);
}
Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => {
if let Some(value) = value {
self.is_subcript_modified =
check_target_for_assignment(target, self.sequence_name, self.index_name);
self.visit_expr(value);
}
}
Stmt::AugAssign(ast::StmtAugAssign { target, value, .. }) => {
self.is_subcript_modified =
check_target_for_assignment(target, self.sequence_name, self.index_name);
self.visit_expr(value);
}
_ => visitor::walk_stmt(self, stmt),
}
}
}

/// PLR1736
pub(crate) fn unnecessary_list_index_lookup(checker: &mut Checker, stmt_for: &StmtFor) {
let Some((sequence, index_name, value_name)) =
enumerate_items(checker, &stmt_for.iter, &stmt_for.target)
else {
return;
};

let mut visitor = SubscriptVisitor::new(&sequence, &index_name);

visitor.visit_body(&stmt_for.body);
visitor.visit_body(&stmt_for.orelse);

for range in visitor.diagnostic_ranges {
let mut diagnostic = Diagnostic::new(UnnecessaryListIndexLookup, range);

diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
value_name.clone(),
range,
)));

checker.diagnostics.push(diagnostic);
}
}

/// PLR1736
pub(crate) fn unnecessary_list_index_lookup_comprehension(checker: &mut Checker, expr: &Expr) {
match expr {
Expr::GeneratorExp(ast::ExprGeneratorExp {
elt, generators, ..
})
| Expr::DictComp(ast::ExprDictComp {
value: elt,
generators,
..
})
| Expr::SetComp(ast::ExprSetComp {
elt, generators, ..
})
| Expr::ListComp(ast::ExprListComp {
elt, generators, ..
}) => {
for comp in generators {
if let Some((sequence, index_name, value_name)) =
enumerate_items(checker, &comp.iter, &comp.target)
dhruvmanila marked this conversation as resolved.
Show resolved Hide resolved
{
let mut visitor = SubscriptVisitor::new(&sequence, &index_name);

visitor.visit_expr(elt.as_ref());

for range in visitor.diagnostic_ranges {
let mut diagnostic = Diagnostic::new(UnnecessaryListIndexLookup, range);

diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
value_name.clone(),
range,
)));

checker.diagnostics.push(diagnostic);
}
}
}
}
_ => (),
}
}

fn enumerate_items(
checker: &mut Checker,
call_expr: &Expr,
tuple_expr: &Expr,
) -> Option<(String, String, String)> {
let Expr::Call(ast::ExprCall {
func, arguments, ..
}) = call_expr
else {
return None;
};
dhruvmanila marked this conversation as resolved.
Show resolved Hide resolved

// Check that the function is the `enumerate` builtin.
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
return None;
};
if id != "enumerate" {
return None;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this use checker.semantic().resolve_call_path(func)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good change, I've also added detection for builtins.enumerate along with it, thanks!

if !checker.semantic().is_builtin("enumerate") {
return None;
};

let Expr::Tuple(ast::ExprTuple { elts, .. }) = tuple_expr else {
return None;
};
let [index, value] = elts.as_slice() else {
return None;
};

// Grab the variable names
let Expr::Name(ast::ExprName { id: index_name, .. }) = index else {
return None;
};

let Expr::Name(ast::ExprName { id: value_name, .. }) = value else {
return None;
};

// If either of the variable names are intentionally ignored by naming them `_`, then don't emit
if index_name == "_" || value_name == "_" {
return None;
}

// Get the first argument of the enumerate call
let Some(Expr::Name(ast::ExprName { id: sequence, .. })) = arguments.args.first() else {
return None;
};

Some((sequence.clone(), index_name.clone(), value_name.clone()))
}
Loading
Loading