|
| 1 | +use crate::checkers::ast::Checker; |
| 2 | +use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; |
| 3 | +use ruff_macros::{derive_message_formats, ViolationMetadata}; |
| 4 | +use ruff_python_ast::{self as ast, Expr}; |
| 5 | +use ruff_text_size::Ranged; |
| 6 | + |
| 7 | +/// ## What it does |
| 8 | +/// Checks for usages of `collections.deque` that have an empty iterable as the first argument. |
| 9 | +/// |
| 10 | +/// ## Why is this bad? |
| 11 | +/// It's unnecessary to use an empty literal as a deque's iterable, since this is already the default behavior. |
| 12 | +/// |
| 13 | +/// ## Examples |
| 14 | +/// |
| 15 | +/// ```python |
| 16 | +/// from collections import deque |
| 17 | +/// |
| 18 | +/// queue = deque(set()) |
| 19 | +/// queue = deque([], 10) |
| 20 | +/// ``` |
| 21 | +/// |
| 22 | +/// Use instead: |
| 23 | +/// |
| 24 | +/// ```python |
| 25 | +/// from collections import deque |
| 26 | +/// |
| 27 | +/// queue = deque() |
| 28 | +/// queue = deque(maxlen=10) |
| 29 | +/// ``` |
| 30 | +/// |
| 31 | +/// ## References |
| 32 | +/// - [Python documentation: `collections.deque`](https://docs.python.org/3/library/collections.html#collections.deque) |
| 33 | +#[derive(ViolationMetadata)] |
| 34 | +pub(crate) struct UnnecessaryEmptyIterableWithinDequeCall { |
| 35 | + has_maxlen: bool, |
| 36 | +} |
| 37 | + |
| 38 | +impl AlwaysFixableViolation for UnnecessaryEmptyIterableWithinDequeCall { |
| 39 | + #[derive_message_formats] |
| 40 | + fn message(&self) -> String { |
| 41 | + "Unnecessary empty iterable within a deque call".to_string() |
| 42 | + } |
| 43 | + |
| 44 | + fn fix_title(&self) -> String { |
| 45 | + let title = if self.has_maxlen { |
| 46 | + "Replace with `deque(maxlen=...)`" |
| 47 | + } else { |
| 48 | + "Replace with `deque()`" |
| 49 | + }; |
| 50 | + title.to_string() |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +/// RUF025 |
| 55 | +pub(crate) fn unnecessary_literal_within_deque_call(checker: &mut Checker, deque: &ast::ExprCall) { |
| 56 | + let ast::ExprCall { |
| 57 | + func, arguments, .. |
| 58 | + } = deque; |
| 59 | + |
| 60 | + let Some(qualified) = checker.semantic().resolve_qualified_name(func) else { |
| 61 | + return; |
| 62 | + }; |
| 63 | + if !matches!(qualified.segments(), ["collections", "deque"]) || arguments.len() > 2 { |
| 64 | + return; |
| 65 | + } |
| 66 | + |
| 67 | + let Some(iterable) = arguments.find_argument_value("iterable", 0) else { |
| 68 | + return; |
| 69 | + }; |
| 70 | + |
| 71 | + let maxlen = arguments.find_argument_value("maxlen", 1); |
| 72 | + |
| 73 | + let is_empty_literal = match iterable { |
| 74 | + Expr::Dict(dict) => dict.is_empty(), |
| 75 | + Expr::List(list) => list.is_empty(), |
| 76 | + Expr::Tuple(tuple) => tuple.is_empty(), |
| 77 | + Expr::Call(call) => { |
| 78 | + checker |
| 79 | + .semantic() |
| 80 | + .resolve_builtin_symbol(&call.func) |
| 81 | + // other lints should handle empty list/dict/tuple calls, |
| 82 | + // but this means that the lint still applies before those are fixed |
| 83 | + .is_some_and(|name| { |
| 84 | + name == "set" || name == "list" || name == "dict" || name == "tuple" |
| 85 | + }) |
| 86 | + && call.arguments.is_empty() |
| 87 | + } |
| 88 | + _ => false, |
| 89 | + }; |
| 90 | + if !is_empty_literal { |
| 91 | + return; |
| 92 | + } |
| 93 | + |
| 94 | + let mut diagnostic = Diagnostic::new( |
| 95 | + UnnecessaryEmptyIterableWithinDequeCall { |
| 96 | + has_maxlen: maxlen.is_some(), |
| 97 | + }, |
| 98 | + deque.range, |
| 99 | + ); |
| 100 | + |
| 101 | + diagnostic.set_fix(fix_unnecessary_literal_in_deque(checker, deque, maxlen)); |
| 102 | + |
| 103 | + checker.diagnostics.push(diagnostic); |
| 104 | +} |
| 105 | + |
| 106 | +fn fix_unnecessary_literal_in_deque( |
| 107 | + checker: &Checker, |
| 108 | + deque: &ast::ExprCall, |
| 109 | + maxlen: Option<&Expr>, |
| 110 | +) -> Fix { |
| 111 | + let deque_name = checker.locator().slice(deque.func.range()); |
| 112 | + let deque_str = match maxlen { |
| 113 | + Some(maxlen) => { |
| 114 | + let len_str = checker.locator().slice(maxlen); |
| 115 | + format!("{deque_name}(maxlen={len_str})") |
| 116 | + } |
| 117 | + None => format!("{deque_name}()"), |
| 118 | + }; |
| 119 | + Fix::safe_edit(Edit::range_replacement(deque_str, deque.range)) |
| 120 | +} |
0 commit comments