-
Notifications
You must be signed in to change notification settings - Fork 1.8k
[ruff] Implement quadratic-list-summation rule (RUF017)
#6489
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
Changes from all commits
a12a71a
b6d786f
e38e8c0
dbe62cc
0e268c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| x = [1, 2, 3] | ||
| y = [4, 5, 6] | ||
|
|
||
| # RUF017 | ||
| sum([x, y], start=[]) | ||
| sum([x, y], []) | ||
| sum([[1, 2, 3], [4, 5, 6]], start=[]) | ||
| sum([[1, 2, 3], [4, 5, 6]], []) | ||
| sum([[1, 2, 3], [4, 5, 6]], | ||
| []) | ||
|
|
||
| # OK | ||
| sum([x, y]) | ||
| sum([[1, 2, 3], [4, 5, 6]]) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| use anyhow::Result; | ||
|
|
||
| use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; | ||
| use ruff_macros::{derive_message_formats, violation}; | ||
| use ruff_python_ast::{self as ast, Arguments, Expr, Ranged}; | ||
| use ruff_python_semantic::SemanticModel; | ||
|
|
||
| use crate::importer::ImportRequest; | ||
| use crate::{checkers::ast::Checker, registry::Rule}; | ||
|
|
||
| /// ## What it does | ||
| /// Checks for the use of `sum()` to flatten lists of lists, which has | ||
| /// quadratic complexity. | ||
| /// | ||
| /// ## Why is this bad? | ||
| /// The use of `sum()` to flatten lists of lists is quadratic in the number of | ||
| /// lists, as `sum()` creates a new list for each element in the summation. | ||
| /// | ||
| /// Instead, consider using another method of flattening lists to avoid | ||
| /// quadratic complexity. The following methods are all linear in the number of | ||
| /// lists: | ||
| /// | ||
| /// - `functools.reduce(operator.iconcat, lists, [])` | ||
| /// - `list(itertools.chain.from_iterable(lists)` | ||
| /// - `[item for sublist in lists for item in sublist]` | ||
| /// | ||
| /// ## Example | ||
| /// ```python | ||
| /// lists = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] | ||
| /// joined = sum(lists, []) | ||
| /// ``` | ||
| /// | ||
| /// Use instead: | ||
| /// ```python | ||
| /// import functools | ||
| /// import operator | ||
| /// | ||
| /// | ||
| /// lists = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] | ||
| /// functools.reduce(operator.iconcat, lists, []) | ||
| /// ``` | ||
| /// | ||
| /// ## References | ||
| /// - [_How Not to Flatten a List of Lists in Python_](https://mathieularose.com/how-not-to-flatten-a-list-of-lists-in-python) | ||
| /// - [_How do I make a flat list out of a list of lists?_](https://stackoverflow.com/questions/952914/how-do-i-make-a-flat-list-out-of-a-list-of-lists/953097#953097) | ||
| #[violation] | ||
| pub struct QuadraticListSummation; | ||
|
|
||
| impl AlwaysAutofixableViolation for QuadraticListSummation { | ||
| #[derive_message_formats] | ||
| fn message(&self) -> String { | ||
| format!("Avoid quadratic list summation") | ||
| } | ||
|
|
||
| fn autofix_title(&self) -> String { | ||
| format!("Replace with `functools.reduce`") | ||
| } | ||
| } | ||
|
|
||
| /// RUF017 | ||
| pub(crate) fn quadratic_list_summation(checker: &mut Checker, call: &ast::ExprCall) { | ||
| let ast::ExprCall { | ||
| func, | ||
| arguments, | ||
| range, | ||
| } = call; | ||
|
|
||
| if !func_is_builtin(func, "sum", checker.semantic()) { | ||
| return; | ||
| } | ||
|
|
||
| if !start_is_empty_list(arguments, checker.semantic()) { | ||
| return; | ||
| }; | ||
|
|
||
| let Some(iterable) = arguments.args.first() else { | ||
| return; | ||
| }; | ||
|
|
||
| let mut diagnostic = Diagnostic::new(QuadraticListSummation, *range); | ||
| if checker.patch(Rule::QuadraticListSummation) { | ||
| diagnostic.try_set_fix(|| convert_to_reduce(iterable, call, checker)); | ||
| } | ||
| checker.diagnostics.push(diagnostic); | ||
| } | ||
|
|
||
| /// Generate a [`Fix`] to convert a `sum()` call to a `functools.reduce()` call. | ||
| fn convert_to_reduce(iterable: &Expr, call: &ast::ExprCall, checker: &Checker) -> Result<Fix> { | ||
| let (reduce_edit, reduce_binding) = checker.importer().get_or_import_symbol( | ||
| &ImportRequest::import("functools", "reduce"), | ||
| call.start(), | ||
| checker.semantic(), | ||
| )?; | ||
|
|
||
| let (iadd_edit, iadd_binding) = checker.importer().get_or_import_symbol( | ||
| &ImportRequest::import("operator", "iadd"), | ||
| iterable.start(), | ||
| checker.semantic(), | ||
| )?; | ||
|
|
||
| let iterable = checker.locator().slice(iterable.range()); | ||
|
|
||
| Ok(Fix::suggested_edits( | ||
| Edit::range_replacement( | ||
| format!("{reduce_binding}({iadd_binding}, {iterable}, [])"), | ||
| call.range(), | ||
| ), | ||
| [reduce_edit, iadd_edit], | ||
| )) | ||
| } | ||
|
|
||
| /// Check if a function is a builtin with a given name. | ||
| fn func_is_builtin(func: &Expr, name: &str, semantic: &SemanticModel) -> bool { | ||
| let Expr::Name(ast::ExprName { id, .. }) = func else { | ||
| return false; | ||
| }; | ||
| id == name && semantic.is_builtin(id) | ||
| } | ||
|
|
||
| /// Returns `true` if the `start` argument to a `sum()` call is an empty list. | ||
| fn start_is_empty_list(arguments: &Arguments, semantic: &SemanticModel) -> bool { | ||
| let Some(keyword) = arguments.find_keyword("start") else { | ||
| return false; | ||
| }; | ||
|
|
||
| match &keyword.value { | ||
| Expr::Call(ast::ExprCall { | ||
| func, arguments, .. | ||
| }) => arguments.is_empty() && func_is_builtin(func, "list", semantic), | ||
| Expr::List(ast::ExprList { elts, ctx, .. }) => elts.is_empty() && ctx.is_load(), | ||
| _ => false, | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| --- | ||
| source: crates/ruff/src/rules/ruff/mod.rs | ||
| --- | ||
| RUF017.py:5:1: RUF017 [*] Avoid quadratic list summation | ||
| | | ||
| 4 | # RUF017 | ||
| 5 | sum([x, y], start=[]) | ||
| | ^^^^^^^^^^^^^^^^^^^^^ RUF017 | ||
| 6 | sum([x, y], []) | ||
| 7 | sum([[1, 2, 3], [4, 5, 6]], start=[]) | ||
| | | ||
| = help: Replace with `functools.reduce` | ||
|
|
||
| ℹ Suggested fix | ||
| 1 |+import functools | ||
| 2 |+import operator | ||
| 1 3 | x = [1, 2, 3] | ||
| 2 4 | y = [4, 5, 6] | ||
| 3 5 | | ||
| 4 6 | # RUF017 | ||
| 5 |-sum([x, y], start=[]) | ||
| 7 |+functools.reduce(operator.iadd, [x, y], []) | ||
| 6 8 | sum([x, y], []) | ||
| 7 9 | sum([[1, 2, 3], [4, 5, 6]], start=[]) | ||
| 8 10 | sum([[1, 2, 3], [4, 5, 6]], []) | ||
|
|
||
| RUF017.py:7:1: RUF017 [*] Avoid quadratic list summation | ||
| | | ||
| 5 | sum([x, y], start=[]) | ||
| 6 | sum([x, y], []) | ||
| 7 | sum([[1, 2, 3], [4, 5, 6]], start=[]) | ||
| | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF017 | ||
| 8 | sum([[1, 2, 3], [4, 5, 6]], []) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it intentional this only complains when start is passed by kwarg? If so, what's the reasoning?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @charliermarsh I think this is from e38e8c0. Any reason we switched from
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just my mistake, @evanrittenhouse had it right initially. I looked at the docs and saw the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks both! Easy fix, will open a PR
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| 9 | sum([[1, 2, 3], [4, 5, 6]], | ||
| | | ||
| = help: Replace with `functools.reduce` | ||
|
|
||
| ℹ Suggested fix | ||
| 1 |+import functools | ||
| 2 |+import operator | ||
| 1 3 | x = [1, 2, 3] | ||
| 2 4 | y = [4, 5, 6] | ||
| 3 5 | | ||
| 4 6 | # RUF017 | ||
| 5 7 | sum([x, y], start=[]) | ||
| 6 8 | sum([x, y], []) | ||
| 7 |-sum([[1, 2, 3], [4, 5, 6]], start=[]) | ||
| 9 |+functools.reduce(operator.iadd, [[1, 2, 3], [4, 5, 6]], []) | ||
| 8 10 | sum([[1, 2, 3], [4, 5, 6]], []) | ||
| 9 11 | sum([[1, 2, 3], [4, 5, 6]], | ||
| 10 12 | []) | ||
|
|
||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.