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

[RUF] Add rule to detect empty literal in deque call (RUF025) #15104

Merged
merged 8 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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,58 @@
from collections import deque
import collections


def f():
queue = collections.deque([]) # C421


def f():
queue = collections.deque([], maxlen=10) # C421


def f():
queue = deque([]) # C421


def f():
queue = deque(()) # C421


def f():
queue = deque({}) # C421


def f():
queue = deque(set()) # C421


def f():
queue = collections.deque([], maxlen=10) # C421


def f():
class FakeDeque:
pass

deque = FakeDeque
queue = deque([]) # Ok


def f():
class FakeSet:
pass

set = FakeSet
queue = deque(set()) # Ok


def f():
queue = deque([1, 2]) # Ok


def f():
queue = deque([1, 2], maxlen=10) # Ok


def f():
queue = deque() # 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 @@ -1108,6 +1108,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::PytestRaisesAmbiguousPattern) {
ruff::rules::pytest_raises_ambiguous_pattern(checker, call);
}
if checker.enabled(Rule::UnnecessaryLiteralWithinDequeCall) {
flake8_comprehensions::rules::unnecessary_literal_within_deque_call(checker, call);
}
}
Expr::Dict(dict) => {
if checker.any_enabled(&[
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 @@ -383,6 +383,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Comprehensions, "18") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryLiteralWithinDictCall),
(Flake8Comprehensions, "19") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryComprehensionInCall),
(Flake8Comprehensions, "20") => (RuleGroup::Preview, rules::flake8_comprehensions::rules::UnnecessaryDictComprehensionForIterable),
(Flake8Comprehensions, "21") => (RuleGroup::Preview, rules::flake8_comprehensions::rules::UnnecessaryLiteralWithinDequeCall),
MichaReiser marked this conversation as resolved.
Show resolved Hide resolved

// flake8-debugger
(Flake8Debugger, "0") => (RuleGroup::Stable, rules::flake8_debugger::rules::Debugger),
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mod tests {
#[test_case(Rule::UnnecessaryComprehensionInCall, Path::new("C419.py"))]
#[test_case(Rule::UnnecessaryComprehensionInCall, Path::new("C419_2.py"))]
#[test_case(Rule::UnnecessaryDictComprehensionForIterable, Path::new("C420.py"))]
#[test_case(Rule::UnnecessaryLiteralWithinDequeCall, Path::new("C421.py"))]
#[test_case(Rule::UnnecessaryDoubleCastOrProcess, Path::new("C414.py"))]
#[test_case(Rule::UnnecessaryGeneratorDict, Path::new("C402.py"))]
#[test_case(Rule::UnnecessaryGeneratorList, Path::new("C400.py"))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub(crate) use unnecessary_list_comprehension_dict::*;
pub(crate) use unnecessary_list_comprehension_set::*;
pub(crate) use unnecessary_literal_dict::*;
pub(crate) use unnecessary_literal_set::*;
pub(crate) use unnecessary_literal_within_deque_call::*;
pub(crate) use unnecessary_literal_within_dict_call::*;
pub(crate) use unnecessary_literal_within_list_call::*;
pub(crate) use unnecessary_literal_within_tuple_call::*;
Expand All @@ -33,6 +34,7 @@ mod unnecessary_list_comprehension_dict;
mod unnecessary_list_comprehension_set;
mod unnecessary_literal_dict;
mod unnecessary_literal_set;
mod unnecessary_literal_within_deque_call;
mod unnecessary_literal_within_dict_call;
mod unnecessary_literal_within_list_call;
mod unnecessary_literal_within_tuple_call;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged;

/// ## What it does
/// Checks for usages of `collections.deque` that have an empty literal as the first argument.
///
/// ## Why is this bad?
/// It's unnecessary to use an empty literal as a deque's iterable, since this is already the default behavior.
///
/// ## Examples
/// ```python
/// from collections import deque
/// queue = deque(set())
/// queue = deque([], 10)
/// ```
///
/// Use instead:
/// ```python
/// from collections import deque
/// queue = deque()
/// queue = deque(maxlen=10)
/// ```
///
/// ## References
/// - [Python documentation: `collections.deque`](https://docs.python.org/3/library/collections.html#collections.deque)
#[derive(ViolationMetadata)]
pub(crate) struct UnnecessaryLiteralWithinDequeCall {
w0nder1ng marked this conversation as resolved.
Show resolved Hide resolved
has_maxlen: bool,
}

impl Violation for UnnecessaryLiteralWithinDequeCall {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always;

#[derive_message_formats]
fn message(&self) -> String {
"Unnecessary literal within a deque call; remove the literal".to_string()
}

fn fix_title(&self) -> Option<String> {
let title = if self.has_maxlen {
"Replace with `deque(maxlen=...)`"
} else {
"Replace with `deque()`"
};
Some(title.to_string())
}
}

/// C421
pub(crate) fn unnecessary_literal_within_deque_call(checker: &mut Checker, deque: &ast::ExprCall) {
let ast::ExprCall {
func, arguments, ..
} = deque;

let Some(qualified) = checker.semantic().resolve_qualified_name(func) else {
return;
};
if !matches!(qualified.segments(), ["collections", "deque"]) {
return;
}

let Some(iterable) = arguments
w0nder1ng marked this conversation as resolved.
Show resolved Hide resolved
.find_positional(0)
.or_else(|| arguments.find_keyword("iterable").map(|kw| &kw.value))
else {
return;
};
let maxlen = arguments
.find_positional(1)
.or_else(|| arguments.find_keyword("maxlen").map(|kw| &kw.value));

let is_empty_literal = match iterable {
Expr::Dict(dict) => dict.is_empty(),
Expr::List(list) => list.is_empty(),
Expr::Tuple(tuple) => tuple.is_empty(),
Expr::Call(call) => {
checker
.semantic()
.resolve_builtin_symbol(&call.func)
// other lints should handle empty list/dict/tuple calls,
// but this means that the lint still applies before those are fixed
.is_some_and(|name| {
name == "set" || name == "list" || name == "dict" || name == "tuple"
})
&& call.arguments.is_empty()
}
_ => false,
};
if !is_empty_literal {
return;
}

let mut diagnostic = Diagnostic::new(
UnnecessaryLiteralWithinDequeCall {
has_maxlen: maxlen.is_some(),
},
deque.range,
);

diagnostic.set_fix(fix_unnecessary_literal_in_deque(checker, deque, maxlen));

checker.diagnostics.push(diagnostic);
}

fn fix_unnecessary_literal_in_deque(
checker: &Checker,
deque: &ast::ExprCall,
maxlen: Option<&Expr>,
) -> Fix {
let deque_name = checker.locator().slice(deque.func.range());
let deque_str = match maxlen {
Some(maxlen) => {
let len_str = checker.locator().slice(maxlen);
format!("{deque_name}(maxlen={len_str})")
}
None => format!("{deque_name}()"),
};
Fix::safe_edit(Edit::range_replacement(deque_str, deque.range))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
source: crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs
---
C421.py:6:13: C421 [*] Unnecessary literal inside a deque expression; remove the literal
|
5 | def f():
6 | queue = collections.deque([]) # C421
| ^^^^^^^^^^^^^^^^^^^^^ C421
|
= help: Replace with `deque()`

ℹ Safe fix
3 3 |
4 4 |
5 5 | def f():
6 |- queue = collections.deque([]) # C421
6 |+ queue = collections.deque() # C421
7 7 |
8 8 |
9 9 | def f():

C421.py:10:13: C421 [*] Unnecessary literal inside a deque expression; remove the literal
|
9 | def f():
10 | queue = collections.deque([], maxlen=10) # C421
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C421
|
= help: Replace with `deque(maxlen=...)`

ℹ Safe fix
7 7 |
8 8 |
9 9 | def f():
10 |- queue = collections.deque([], maxlen=10) # C421
10 |+ queue = collections.deque(maxlen=10) # C421
11 11 |
12 12 |
13 13 | def f():

C421.py:14:13: C421 [*] Unnecessary literal inside a deque expression; remove the literal
|
13 | def f():
14 | queue = deque([]) # C421
| ^^^^^^^^^ C421
|
= help: Replace with `deque()`

ℹ Safe fix
11 11 |
12 12 |
13 13 | def f():
14 |- queue = deque([]) # C421
14 |+ queue = deque() # C421
15 15 |
16 16 |
17 17 | def f():

C421.py:18:13: C421 [*] Unnecessary literal inside a deque expression; remove the literal
|
17 | def f():
18 | queue = deque(()) # C421
| ^^^^^^^^^ C421
|
= help: Replace with `deque()`

ℹ Safe fix
15 15 |
16 16 |
17 17 | def f():
18 |- queue = deque(()) # C421
18 |+ queue = deque() # C421
19 19 |
20 20 |
21 21 | def f():

C421.py:22:13: C421 [*] Unnecessary literal inside a deque expression; remove the literal
|
21 | def f():
22 | queue = deque({}) # C421
| ^^^^^^^^^ C421
|
= help: Replace with `deque()`

ℹ Safe fix
19 19 |
20 20 |
21 21 | def f():
22 |- queue = deque({}) # C421
22 |+ queue = deque() # C421
23 23 |
24 24 |
25 25 | def f():

C421.py:26:13: C421 [*] Unnecessary literal inside a deque expression; remove the literal
|
25 | def f():
26 | queue = deque(set()) # C421
| ^^^^^^^^^^^^ C421
|
= help: Replace with `deque()`

ℹ Safe fix
23 23 |
24 24 |
25 25 | def f():
26 |- queue = deque(set()) # C421
26 |+ queue = deque() # C421
27 27 |
28 28 |
29 29 | def f():

C421.py:30:13: C421 [*] Unnecessary literal inside a deque expression; remove the literal
|
29 | def f():
30 | queue = collections.deque([], maxlen=10) # C421
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C421
|
= help: Replace with `deque(maxlen=...)`

ℹ Safe fix
27 27 |
28 28 |
29 29 | def f():
30 |- queue = collections.deque([], maxlen=10) # C421
30 |+ queue = collections.deque(maxlen=10) # C421
31 31 |
32 32 |
33 33 | def f():
Loading