From da6e3c1c929e0b096a757bff0d1b9d5bd26c70ab Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Wed, 10 Jan 2024 11:58:43 +0000 Subject: [PATCH 01/77] Get it working for single-line definitions --- .../resources/test/fixtures/ruff/RUF022.py | 35 ++++ .../src/checkers/ast/analyze/statement.rs | 3 + crates/ruff_linter/src/codes.rs | 1 + crates/ruff_linter/src/rules/ruff/mod.rs | 1 + .../ruff_linter/src/rules/ruff/rules/mod.rs | 2 + .../src/rules/ruff/rules/sort_dunder_all.rs | 162 ++++++++++++++++++ ..._rules__ruff__tests__RUF022_RUF022.py.snap | 119 +++++++++++++ ruff.schema.json | 1 + 8 files changed, 324 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py create mode 100644 crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py new file mode 100644 index 0000000000000..374bcf7c36ca1 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -0,0 +1,35 @@ +__all__ = ["d", "c", "b", "a"] # a comment that is untouched +__all__ += ["foo", "bar", "antipasti"] +__all__ = ("d", "c", "b", "a") + +if bool(): + __all__ += ("x", "m", "a", "s") +else: + __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) + +################################### +# These should all not get flagged: +################################### + +__all__ = () +__all__ = [] +__all__ = ("single_item",) +__all__ = ["single_item",] +__all__ = ("not_a_tuple_just_a_string") +__all__ = ["a", "b", "c", "d"] +__all__ += ["e", "f", "g"] +__all__ = ("a", "b", "c", "d") + +if bool(): + __all__ += ("e", "f", "g") +else: + __all__ += ["omega", "alpha"] + +class IntroducesNonModuleScope: + __all__ = ("b", "a", "e", "d") + __all__ = ["b", "a", "e", "d"] + __all__ += ["foo", "bar", "antipasti"] + +__all__ = {"look", "a", "set"} +__all__ = {"very": "strange", "not": "sorted", "we don't": "care"} +["not", "an", "assignment", "just", "an", "expression"] diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 0165e431bc422..89b03525d02ff 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -19,6 +19,9 @@ use crate::settings::types::PythonVersion; /// Run lint rules over a [`Stmt`] syntax node. pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { + if checker.settings.rules.enabled(Rule::UnsortedDunderAll) { + ruff::rules::sort_dunder_all(checker, stmt); + } match stmt { Stmt::Global(ast::StmtGlobal { names, range: _ }) => { if checker.enabled(Rule::GlobalAtModuleLevel) { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 43ef44c2bc920..ffb4775fa77e5 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -924,6 +924,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "019") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryKeyCheck), (Ruff, "020") => (RuleGroup::Preview, rules::ruff::rules::NeverUnion), (Ruff, "021") => (RuleGroup::Preview, rules::ruff::rules::ParenthesizeChainedOperators), + (Ruff, "022") => (RuleGroup::Preview, rules::ruff::rules::UnsortedDunderAll), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), (Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index bda96268fdbae..f7ae3b6ccc4a1 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -47,6 +47,7 @@ mod tests { #[test_case(Rule::UnnecessaryKeyCheck, Path::new("RUF019.py"))] #[test_case(Rule::NeverUnion, Path::new("RUF020.py"))] #[test_case(Rule::ParenthesizeChainedOperators, Path::new("RUF021.py"))] + #[test_case(Rule::UnsortedDunderAll, Path::new("RUF022.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index 37bc4e9c91260..0709694982fe5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -13,6 +13,7 @@ pub(crate) use never_union::*; pub(crate) use pairwise_over_zipped::*; pub(crate) use parenthesize_logical_operators::*; pub(crate) use quadratic_list_summation::*; +pub(crate) use sort_dunder_all::*; pub(crate) use static_key_dict_comprehension::*; pub(crate) use unnecessary_iterable_allocation_for_first_element::*; pub(crate) use unnecessary_key_check::*; @@ -36,6 +37,7 @@ mod mutable_dataclass_default; mod never_union; mod pairwise_over_zipped; mod parenthesize_logical_operators; +mod sort_dunder_all; mod static_key_dict_comprehension; mod unnecessary_iterable_allocation_for_first_element; mod unnecessary_key_check; diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs new file mode 100644 index 0000000000000..cf8881db57ed8 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -0,0 +1,162 @@ +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast as ast; +use ruff_python_parser::{lexer, Mode}; +use ruff_source_file::Locator; +use ruff_text_size::{Ranged, TextRange}; + +use crate::checkers::ast::Checker; + +use strum_macros::EnumIs; + +#[violation] +pub struct UnsortedDunderAll; + +impl Violation for UnsortedDunderAll { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + format!("`__all__` is not alphabetically sorted") + } + + fn fix_title(&self) -> Option { + Some("Sort `__all__` alphabetically".to_string()) + } +} + +#[derive(EnumIs)] +enum DunderAllKind { + List, + Tuple { is_parenthesized: bool }, +} + +struct DunderAllValue<'a> { + kind: DunderAllKind, + items: Vec<&'a ast::ExprStringLiteral>, + range: &'a TextRange, + ctx: &'a ast::ExprContext, +} + +impl<'a> DunderAllValue<'a> { + fn from_expr(value: &'a ast::Expr, locator: &Locator) -> Option> { + let (kind, elts, range, ctx) = match value { + ast::Expr::List(ast::ExprList { elts, range, ctx }) => { + (DunderAllKind::List, elts, range, ctx) + } + ast::Expr::Tuple(ast::ExprTuple { elts, range, ctx }) => { + let is_parenthesized = + lexer::lex_starts_at(locator.slice(range), Mode::Expression, range.start()) + .next()? + .ok()? + .0 + .is_lpar(); + (DunderAllKind::Tuple { is_parenthesized }, elts, range, ctx) + } + _ => return None, + }; + let mut items = vec![]; + for elt in elts { + let string_literal = elt.as_string_literal_expr()?; + if string_literal.value.is_implicit_concatenated() { + return None; + } + items.push(string_literal) + } + Some(DunderAllValue { + kind, + items, + range, + ctx, + }) + } + + fn sorted_items(&self) -> Vec<&ast::ExprStringLiteral> { + let mut sorted_items = self.items.clone(); + sorted_items.sort_by_key(|item| item.value.to_str()); + sorted_items + } +} + +impl Ranged for DunderAllValue<'_> { + fn range(&self) -> TextRange { + *self.range + } +} + +pub(crate) fn sort_dunder_all(checker: &mut Checker, stmt: &ast::Stmt) { + // We're only interested in `__all__` in the global scope + if !checker.semantic().current_scope().kind.is_module() { + return; + } + + // We're only interested in `__all__ = ...` and `__all__ += ...` + let (target, original_value) = match stmt { + ast::Stmt::Assign(ast::StmtAssign { value, targets, .. }) => match targets.as_slice() { + [ast::Expr::Name(ast::ExprName { id, .. })] => (id, value.as_ref()), + _ => return, + }, + ast::Stmt::AugAssign(ast::StmtAugAssign { + value, + target, + op: ast::Operator::Add, + .. + }) => match target.as_ref() { + ast::Expr::Name(ast::ExprName { id, .. }) => (id, value.as_ref()), + _ => return, + }, + _ => return, + }; + + if target != "__all__" { + return; + } + + let Some(dunder_all_val) = DunderAllValue::from_expr(original_value, checker.locator()) else { + return; + }; + + if dunder_all_val.items.len() < 2 { + return; + } + + let sorted_items = dunder_all_val.sorted_items(); + if sorted_items == dunder_all_val.items { + return; + } + + let dunder_all_range = dunder_all_val.range(); + let mut diagnostic = Diagnostic::new(UnsortedDunderAll, dunder_all_range); + + if !checker.locator().contains_line_break(dunder_all_range) { + let new_elts = sorted_items + .iter() + .map(|elt| ast::Expr::StringLiteral(elt.to_owned().clone())) + .collect(); + let new_node = match dunder_all_val.kind { + DunderAllKind::List => ast::Expr::List(ast::ExprList { + range: dunder_all_range, + elts: new_elts, + ctx: *dunder_all_val.ctx, + }), + DunderAllKind::Tuple { .. } => ast::Expr::Tuple(ast::ExprTuple { + range: dunder_all_range, + elts: new_elts, + ctx: *dunder_all_val.ctx, + }), + }; + let mut content = checker.generator().expr(&new_node); + if let DunderAllKind::Tuple { + is_parenthesized: true, + } = dunder_all_val.kind + { + content = format!("({})", content); + } + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + content, + dunder_all_range, + ))); + } + + checker.diagnostics.push(diagnostic); +} diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap new file mode 100644 index 0000000000000..a66eca045f61e --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -0,0 +1,119 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF022.py:1:11: RUF022 [*] `__all__` is not alphabetically sorted + | +1 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched + | ^^^^^^^^^^^^^^^^^^^^ RUF022 +2 | __all__ += ["foo", "bar", "antipasti"] +3 | __all__ = ("d", "c", "b", "a") + | + = help: Sort `__all__` alphabetically + +ℹ Safe fix +1 |-__all__ = ["d", "c", "b", "a"] # a comment that is untouched + 1 |+__all__ = ["a", "b", "c", "d"] # a comment that is untouched +2 2 | __all__ += ["foo", "bar", "antipasti"] +3 3 | __all__ = ("d", "c", "b", "a") +4 4 | + +RUF022.py:2:12: RUF022 [*] `__all__` is not alphabetically sorted + | +1 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched +2 | __all__ += ["foo", "bar", "antipasti"] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF022 +3 | __all__ = ("d", "c", "b", "a") + | + = help: Sort `__all__` alphabetically + +ℹ Safe fix +1 1 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched +2 |-__all__ += ["foo", "bar", "antipasti"] + 2 |+__all__ += ["antipasti", "bar", "foo"] +3 3 | __all__ = ("d", "c", "b", "a") +4 4 | +5 5 | if bool(): + +RUF022.py:3:11: RUF022 [*] `__all__` is not alphabetically sorted + | +1 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched +2 | __all__ += ["foo", "bar", "antipasti"] +3 | __all__ = ("d", "c", "b", "a") + | ^^^^^^^^^^^^^^^^^^^^ RUF022 +4 | +5 | if bool(): + | + = help: Sort `__all__` alphabetically + +ℹ Safe fix +1 1 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched +2 2 | __all__ += ["foo", "bar", "antipasti"] +3 |-__all__ = ("d", "c", "b", "a") + 3 |+__all__ = ("a", "b", "c", "d") +4 4 | +5 5 | if bool(): +6 6 | __all__ += ("x", "m", "a", "s") + +RUF022.py:6:16: RUF022 [*] `__all__` is not alphabetically sorted + | +5 | if bool(): +6 | __all__ += ("x", "m", "a", "s") + | ^^^^^^^^^^^^^^^^^^^^ RUF022 +7 | else: +8 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) + | + = help: Sort `__all__` alphabetically + +ℹ Safe fix +3 3 | __all__ = ("d", "c", "b", "a") +4 4 | +5 5 | if bool(): +6 |- __all__ += ("x", "m", "a", "s") + 6 |+ __all__ += ("a", "m", "s", "x") +7 7 | else: +8 8 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +9 9 | + +RUF022.py:8:16: RUF022 [*] `__all__` is not alphabetically sorted + | + 6 | __all__ += ("x", "m", "a", "s") + 7 | else: + 8 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) + | ^^^^^^^^^^^^^^^^^^^^^^ RUF022 + 9 | +10 | ################################### + | + = help: Sort `__all__` alphabetically + +ℹ Safe fix +5 5 | if bool(): +6 6 | __all__ += ("x", "m", "a", "s") +7 7 | else: +8 |- __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) + 8 |+ __all__ += "foo1", "foo2", "foo3" # NB: an implicit tuple (without parens) +9 9 | +10 10 | ################################### +11 11 | # These should all not get flagged: + +RUF022.py:26:16: RUF022 [*] `__all__` is not alphabetically sorted + | +24 | __all__ += ("e", "f", "g") +25 | else: +26 | __all__ += ["omega", "alpha"] + | ^^^^^^^^^^^^^^^^^^ RUF022 +27 | +28 | class IntroducesNonModuleScope: + | + = help: Sort `__all__` alphabetically + +ℹ Safe fix +23 23 | if bool(): +24 24 | __all__ += ("e", "f", "g") +25 25 | else: +26 |- __all__ += ["omega", "alpha"] + 26 |+ __all__ += ["alpha", "omega"] +27 27 | +28 28 | class IntroducesNonModuleScope: +29 29 | __all__ = ("b", "a", "e", "d") + + diff --git a/ruff.schema.json b/ruff.schema.json index 02295d9ba2ad3..511867aa99036 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3442,6 +3442,7 @@ "RUF02", "RUF020", "RUF021", + "RUF022", "RUF1", "RUF10", "RUF100", From 47fa963293cd910a279836fb160e2404a7402a26 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Wed, 10 Jan 2024 23:07:13 +0000 Subject: [PATCH 02/77] Get it working for multiline `__all__` definitions --- .../resources/test/fixtures/ruff/RUF022.py | 41 +++ .../src/rules/ruff/rules/sort_dunder_all.rs | 343 +++++++++++++++--- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 270 ++++++++++---- 3 files changed, 528 insertions(+), 126 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index 374bcf7c36ca1..9d7d80c483e27 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -1,3 +1,7 @@ +################################################## +# Single-line __all__ definitions (nice 'n' easy!) +################################################## + __all__ = ["d", "c", "b", "a"] # a comment that is untouched __all__ += ["foo", "bar", "antipasti"] __all__ = ("d", "c", "b", "a") @@ -7,6 +11,41 @@ else: __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +#################################### +# Neat multiline __all__ definitions +#################################### + +__all__ = ( + "d", + "c", # a comment regarding 'c' + "b", + # a comment regarding 'a': + "a" +) + +__all__ = [ + "d", + "c", # a comment regarding 'c' + "b", + # a comment regarding 'a': + "a" +] + +########################################## +# Messier multiline __all__ definitions... +########################################## + +# comment0 +__all__ = ("d", "a", # comment1 + # comment2 + "f", "b", + "strangely", # comment3 + # comment4 + "formatted", + # comment5 +) # comment6 +# comment7 + ################################### # These should all not get flagged: ################################### @@ -33,3 +72,5 @@ class IntroducesNonModuleScope: __all__ = {"look", "a", "set"} __all__ = {"very": "strange", "not": "sorted", "we don't": "care"} ["not", "an", "assignment", "just", "an", "expression"] +__all__ = (9, 8, 7) +__all__ = ("don't" "care" "about", "__all__" "with", "concatenated" "strings") diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index cf8881db57ed8..c816b246adb52 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -1,12 +1,17 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; +use std::cmp::Ordering; + +use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast as ast; -use ruff_python_parser::{lexer, Mode}; +use ruff_python_ast::whitespace::indentation; +use ruff_python_codegen::Stylist; +use ruff_python_parser::{lexer, Mode, Tok}; use ruff_source_file::Locator; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; +use itertools::Itertools; use strum_macros::EnumIs; #[violation] @@ -25,56 +30,311 @@ impl Violation for UnsortedDunderAll { } } -#[derive(EnumIs)] +#[derive(EnumIs, Eq, PartialEq)] enum DunderAllKind { List, Tuple { is_parenthesized: bool }, } +impl DunderAllKind { + fn parens(&self, is_multiline: bool) -> (&str, &str) { + match (self, is_multiline) { + (Self::List, _) => ("[", "]"), + ( + Self::Tuple { + is_parenthesized: false, + }, + false, + ) => ("", ""), + _ => ("(", ")"), + } + } +} + +fn tuple_is_parenthesized(tuple: &ast::ExprTuple, locator: &Locator) -> bool { + let toks = lexer::lex(locator.slice(tuple).trim(), Mode::Expression).collect::>(); + matches!( + (toks.first(), toks.get(toks.len() - 2)), + (Some(Ok((Tok::Lpar, _))), Some(Ok((Tok::Rpar, _)))) + ) +} + +#[derive(Clone, Debug)] +struct DunderAllItem { + value: String, + // Each `AllItem` in any given list should have a unique `original_index`: + original_index: u16, + // Note that this range might include comments, etc. + range: TextRange, + additional_comments: Option, +} + +impl Ranged for DunderAllItem { + fn range(&self) -> TextRange { + self.range + } +} + +impl PartialEq for DunderAllItem { + fn eq(&self, other: &Self) -> bool { + self.original_index == other.original_index + } +} + +impl Eq for DunderAllItem {} + +impl DunderAllItem { + fn sort_index(&self) -> (&str, u16) { + (&self.value, self.original_index) + } +} + +impl Ord for DunderAllItem { + fn cmp(&self, other: &Self) -> Ordering { + self.sort_index().cmp(&other.sort_index()) + } +} + +impl PartialOrd for DunderAllItem { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +fn collect_dunder_all_items(lines: Vec) -> Vec { + let mut all_items = vec![]; + + let mut this_range = None; + let mut idx = 0; + for line in lines { + let DunderAllLine { items, comment } = line; + match (items.as_slice(), comment) { + ([], Some(_)) => { + assert!(this_range.is_none()); + this_range = comment; + } + ([(first_val, first_range), rest @ ..], _) => { + let range = this_range.map_or(*first_range, |r| { + TextRange::new(r.start(), first_range.end()) + }); + all_items.push(DunderAllItem { + value: first_val.clone(), + original_index: idx, + range, + additional_comments: comment, + }); + this_range = None; + idx += 1; + for (value, range) in rest { + all_items.push(DunderAllItem { + value: value.clone(), + original_index: idx, + range: *range, + additional_comments: None, + }); + idx += 1; + } + } + _ => unreachable!( + "This should be unreachable. + Any lines that have neither comments nor items + should have been filtered out by this point." + ), + } + } + + all_items +} + +#[derive(Debug)] +struct DunderAllLine { + items: Vec<(String, TextRange)>, + comment: Option, +} + +impl DunderAllLine { + fn new(items: &[(String, TextRange)], comment: Option, offset: TextSize) -> Self { + assert!(comment.is_some() || !items.is_empty()); + Self { + items: items + .iter() + .map(|(s, r)| (s.to_owned(), r + offset)) + .collect(), + comment: comment.map(|c| c + offset), + } + } +} + +fn collect_dunder_all_lines(range: TextRange, locator: &Locator) -> Option> { + let mut parentheses_open = false; + let mut lines = vec![]; + + let mut items_in_line = vec![]; + let mut comment_in_line = None; + for pair in lexer::lex(locator.slice(range).trim(), Mode::Expression) { + let (tok, subrange) = pair.ok()?; + match tok { + Tok::Lpar | Tok::Lsqb => { + if parentheses_open { + return None; + } + parentheses_open = true; + } + Tok::Rpar | Tok::Rsqb | Tok::Newline => { + if !(items_in_line.is_empty() && comment_in_line.is_none()) { + lines.push(DunderAllLine::new( + &items_in_line, + comment_in_line, + range.start(), + )); + } + break; + } + Tok::NonLogicalNewline => { + if !(items_in_line.is_empty() && comment_in_line.is_none()) { + lines.push(DunderAllLine::new( + &items_in_line, + comment_in_line, + range.start(), + )); + items_in_line = vec![]; + comment_in_line = None; + } + } + Tok::Comment(_) => comment_in_line = Some(subrange), + Tok::String { value, .. } => items_in_line.push((value, subrange)), + Tok::Comma => continue, + _ => return None, + } + } + Some(lines) +} + +#[derive(PartialEq, Eq)] struct DunderAllValue<'a> { kind: DunderAllKind, - items: Vec<&'a ast::ExprStringLiteral>, + items: Vec, range: &'a TextRange, ctx: &'a ast::ExprContext, + multiline: bool, +} + +struct SortedDunderAll { + was_already_sorted: bool, + new_dunder_all: Option, +} + +fn multiline_dunder_all_from_items( + sorted_items: &[DunderAllItem], + locator: &Locator, + parent: &ast::Stmt, + stylist: &Stylist, + parens: (&str, &str), +) -> Option { + let Some(indent) = indentation(locator, parent) else { + return None; + }; + let (opening_paren, closing_paren) = parens; + let added_indent = stylist.indentation(); + let newline = stylist.line_ending().as_str(); + + let mut new_dunder_all = String::from(opening_paren); + new_dunder_all.push_str(newline); + for item in sorted_items { + new_dunder_all.push_str(indent); + new_dunder_all.push_str(added_indent); + new_dunder_all.push_str(locator.slice(item)); + new_dunder_all.push(','); + if let Some(comment) = item.additional_comments { + new_dunder_all.push_str(" "); + new_dunder_all.push_str(locator.slice(comment)); + } + new_dunder_all.push_str(newline); + } + new_dunder_all.push_str(indent); + new_dunder_all.push_str(closing_paren); + + Some(new_dunder_all) +} + +fn single_line_dunder_all_from_items( + sorted_items: &[DunderAllItem], + locator: &Locator, + parens: (&str, &str), +) -> String { + let joined_items = sorted_items + .iter() + .map(|item| locator.slice(item)) + .join(", "); + let (opening_paren, closing_paren) = parens; + format!("{opening_paren}{joined_items}{closing_paren}") } impl<'a> DunderAllValue<'a> { fn from_expr(value: &'a ast::Expr, locator: &Locator) -> Option> { + let is_multiline = locator.contains_line_break(value.range()); let (kind, elts, range, ctx) = match value { ast::Expr::List(ast::ExprList { elts, range, ctx }) => { (DunderAllKind::List, elts, range, ctx) } - ast::Expr::Tuple(ast::ExprTuple { elts, range, ctx }) => { - let is_parenthesized = - lexer::lex_starts_at(locator.slice(range), Mode::Expression, range.start()) - .next()? - .ok()? - .0 - .is_lpar(); + ast::Expr::Tuple(tuple @ ast::ExprTuple { elts, range, ctx }) => { + let is_parenthesized = tuple_is_parenthesized(tuple, locator); (DunderAllKind::Tuple { is_parenthesized }, elts, range, ctx) } _ => return None, }; - let mut items = vec![]; + // An `__all__` definition with <2 elements can't be unsorted; + // no point in proceeding any further here + if elts.len() < 2 { + return None; + } for elt in elts { + // Only consider sorting it if __all__ only has strings in it let string_literal = elt.as_string_literal_expr()?; + // And if any strings are implicitly concatenated, don't bother if string_literal.value.is_implicit_concatenated() { return None; } - items.push(string_literal) } + let lines = collect_dunder_all_lines(*range, locator)?; + let items = collect_dunder_all_items(lines); Some(DunderAllValue { kind, items, range, ctx, + multiline: is_multiline, }) } - fn sorted_items(&self) -> Vec<&ast::ExprStringLiteral> { + fn construct_sorted_all( + &self, + locator: &Locator, + parent: &ast::Stmt, + stylist: &Stylist, + ) -> SortedDunderAll { let mut sorted_items = self.items.clone(); - sorted_items.sort_by_key(|item| item.value.to_str()); - sorted_items + sorted_items.sort(); + if sorted_items == self.items { + return SortedDunderAll { + was_already_sorted: true, + new_dunder_all: None, + }; + } + let parens = self.kind.parens(self.multiline); + let new_dunder_all = if self.multiline { + multiline_dunder_all_from_items(&sorted_items, locator, parent, stylist, parens) + } else { + Some(single_line_dunder_all_from_items( + &sorted_items, + locator, + parens, + )) + }; + SortedDunderAll { + was_already_sorted: false, + new_dunder_all, + } } } @@ -112,50 +372,33 @@ pub(crate) fn sort_dunder_all(checker: &mut Checker, stmt: &ast::Stmt) { return; } - let Some(dunder_all_val) = DunderAllValue::from_expr(original_value, checker.locator()) else { + let locator = checker.locator(); + + let Some(dunder_all_val) = DunderAllValue::from_expr(original_value, locator) else { return; }; - if dunder_all_val.items.len() < 2 { - return; - } + let sorting_result = dunder_all_val.construct_sorted_all(locator, stmt, checker.stylist()); - let sorted_items = dunder_all_val.sorted_items(); - if sorted_items == dunder_all_val.items { + if sorting_result.was_already_sorted { return; } let dunder_all_range = dunder_all_val.range(); let mut diagnostic = Diagnostic::new(UnsortedDunderAll, dunder_all_range); - if !checker.locator().contains_line_break(dunder_all_range) { - let new_elts = sorted_items - .iter() - .map(|elt| ast::Expr::StringLiteral(elt.to_owned().clone())) - .collect(); - let new_node = match dunder_all_val.kind { - DunderAllKind::List => ast::Expr::List(ast::ExprList { - range: dunder_all_range, - elts: new_elts, - ctx: *dunder_all_val.ctx, - }), - DunderAllKind::Tuple { .. } => ast::Expr::Tuple(ast::ExprTuple { - range: dunder_all_range, - elts: new_elts, - ctx: *dunder_all_val.ctx, - }), + if let Some(new_dunder_all) = sorting_result.new_dunder_all { + let applicability = { + if dunder_all_val.multiline { + Applicability::Unsafe + } else { + Applicability::Safe + } }; - let mut content = checker.generator().expr(&new_node); - if let DunderAllKind::Tuple { - is_parenthesized: true, - } = dunder_all_val.kind - { - content = format!("({})", content); - } - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - content, - dunder_all_range, - ))); + diagnostic.set_fix(Fix::applicable_edit( + Edit::range_replacement(new_dunder_all, dunder_all_range), + applicability, + )); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index a66eca045f61e..02d34fc6965ae 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -1,119 +1,237 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF022.py:1:11: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:5:11: RUF022 [*] `__all__` is not alphabetically sorted | -1 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched +3 | ################################################## +4 | +5 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched | ^^^^^^^^^^^^^^^^^^^^ RUF022 -2 | __all__ += ["foo", "bar", "antipasti"] -3 | __all__ = ("d", "c", "b", "a") +6 | __all__ += ["foo", "bar", "antipasti"] +7 | __all__ = ("d", "c", "b", "a") | = help: Sort `__all__` alphabetically ℹ Safe fix -1 |-__all__ = ["d", "c", "b", "a"] # a comment that is untouched - 1 |+__all__ = ["a", "b", "c", "d"] # a comment that is untouched -2 2 | __all__ += ["foo", "bar", "antipasti"] -3 3 | __all__ = ("d", "c", "b", "a") +2 2 | # Single-line __all__ definitions (nice 'n' easy!) +3 3 | ################################################## 4 4 | +5 |-__all__ = ["d", "c", "b", "a"] # a comment that is untouched + 5 |+__all__ = ["a", "b", "c", "d"] # a comment that is untouched +6 6 | __all__ += ["foo", "bar", "antipasti"] +7 7 | __all__ = ("d", "c", "b", "a") +8 8 | -RUF022.py:2:12: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:6:12: RUF022 [*] `__all__` is not alphabetically sorted | -1 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched -2 | __all__ += ["foo", "bar", "antipasti"] +5 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched +6 | __all__ += ["foo", "bar", "antipasti"] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF022 -3 | __all__ = ("d", "c", "b", "a") +7 | __all__ = ("d", "c", "b", "a") | = help: Sort `__all__` alphabetically ℹ Safe fix -1 1 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched -2 |-__all__ += ["foo", "bar", "antipasti"] - 2 |+__all__ += ["antipasti", "bar", "foo"] -3 3 | __all__ = ("d", "c", "b", "a") +3 3 | ################################################## 4 4 | -5 5 | if bool(): +5 5 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched +6 |-__all__ += ["foo", "bar", "antipasti"] + 6 |+__all__ += ["antipasti", "bar", "foo"] +7 7 | __all__ = ("d", "c", "b", "a") +8 8 | +9 9 | if bool(): -RUF022.py:3:11: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:7:11: RUF022 [*] `__all__` is not alphabetically sorted | -1 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched -2 | __all__ += ["foo", "bar", "antipasti"] -3 | __all__ = ("d", "c", "b", "a") +5 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched +6 | __all__ += ["foo", "bar", "antipasti"] +7 | __all__ = ("d", "c", "b", "a") | ^^^^^^^^^^^^^^^^^^^^ RUF022 -4 | -5 | if bool(): +8 | +9 | if bool(): | = help: Sort `__all__` alphabetically ℹ Safe fix -1 1 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched -2 2 | __all__ += ["foo", "bar", "antipasti"] -3 |-__all__ = ("d", "c", "b", "a") - 3 |+__all__ = ("a", "b", "c", "d") 4 4 | -5 5 | if bool(): -6 6 | __all__ += ("x", "m", "a", "s") +5 5 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched +6 6 | __all__ += ["foo", "bar", "antipasti"] +7 |-__all__ = ("d", "c", "b", "a") + 7 |+__all__ = ("a", "b", "c", "d") +8 8 | +9 9 | if bool(): +10 10 | __all__ += ("x", "m", "a", "s") -RUF022.py:6:16: RUF022 [*] `__all__` is not alphabetically sorted - | -5 | if bool(): -6 | __all__ += ("x", "m", "a", "s") - | ^^^^^^^^^^^^^^^^^^^^ RUF022 -7 | else: -8 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) - | - = help: Sort `__all__` alphabetically +RUF022.py:10:16: RUF022 [*] `__all__` is not alphabetically sorted + | + 9 | if bool(): +10 | __all__ += ("x", "m", "a", "s") + | ^^^^^^^^^^^^^^^^^^^^ RUF022 +11 | else: +12 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) + | + = help: Sort `__all__` alphabetically ℹ Safe fix -3 3 | __all__ = ("d", "c", "b", "a") -4 4 | -5 5 | if bool(): -6 |- __all__ += ("x", "m", "a", "s") - 6 |+ __all__ += ("a", "m", "s", "x") -7 7 | else: -8 8 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) -9 9 | - -RUF022.py:8:16: RUF022 [*] `__all__` is not alphabetically sorted +7 7 | __all__ = ("d", "c", "b", "a") +8 8 | +9 9 | if bool(): +10 |- __all__ += ("x", "m", "a", "s") + 10 |+ __all__ += ("a", "m", "s", "x") +11 11 | else: +12 12 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +13 13 | + +RUF022.py:12:16: RUF022 [*] `__all__` is not alphabetically sorted | - 6 | __all__ += ("x", "m", "a", "s") - 7 | else: - 8 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +10 | __all__ += ("x", "m", "a", "s") +11 | else: +12 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) | ^^^^^^^^^^^^^^^^^^^^^^ RUF022 - 9 | -10 | ################################### +13 | +14 | #################################### | = help: Sort `__all__` alphabetically ℹ Safe fix -5 5 | if bool(): -6 6 | __all__ += ("x", "m", "a", "s") -7 7 | else: -8 |- __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) - 8 |+ __all__ += "foo1", "foo2", "foo3" # NB: an implicit tuple (without parens) -9 9 | -10 10 | ################################### -11 11 | # These should all not get flagged: - -RUF022.py:26:16: RUF022 [*] `__all__` is not alphabetically sorted +9 9 | if bool(): +10 10 | __all__ += ("x", "m", "a", "s") +11 11 | else: +12 |- __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) + 12 |+ __all__ += "foo1", "foo2", "foo3" # NB: an implicit tuple (without parens) +13 13 | +14 14 | #################################### +15 15 | # Neat multiline __all__ definitions + +RUF022.py:18:11: RUF022 [*] `__all__` is not alphabetically sorted + | +16 | #################################### +17 | +18 | __all__ = ( + | ___________^ +19 | | "d", +20 | | "c", # a comment regarding 'c' +21 | | "b", +22 | | # a comment regarding 'a': +23 | | "a" +24 | | ) + | |_^ RUF022 +25 | +26 | __all__ = [ + | + = help: Sort `__all__` alphabetically + +ℹ Unsafe fix +16 16 | #################################### +17 17 | +18 18 | __all__ = ( + 19 |+ # a comment regarding 'a': + 20 |+ "a", + 21 |+ "b", + 22 |+ "c", # a comment regarding 'c' +19 23 | "d", +20 |- "c", # a comment regarding 'c' +21 |- "b", +22 |- # a comment regarding 'a': +23 |- "a" +24 24 | ) +25 25 | +26 26 | __all__ = [ + +RUF022.py:26:11: RUF022 [*] `__all__` is not alphabetically sorted + | +24 | ) +25 | +26 | __all__ = [ + | ___________^ +27 | | "d", +28 | | "c", # a comment regarding 'c' +29 | | "b", +30 | | # a comment regarding 'a': +31 | | "a" +32 | | ] + | |_^ RUF022 +33 | +34 | ########################################## + | + = help: Sort `__all__` alphabetically + +ℹ Unsafe fix +24 24 | ) +25 25 | +26 26 | __all__ = [ + 27 |+ # a comment regarding 'a': + 28 |+ "a", + 29 |+ "b", + 30 |+ "c", # a comment regarding 'c' +27 31 | "d", +28 |- "c", # a comment regarding 'c' +29 |- "b", +30 |- # a comment regarding 'a': +31 |- "a" +32 32 | ] +33 33 | +34 34 | ########################################## + +RUF022.py:39:11: RUF022 [*] `__all__` is not alphabetically sorted + | +38 | # comment0 +39 | __all__ = ("d", "a", # comment1 + | ___________^ +40 | | # comment2 +41 | | "f", "b", +42 | | "strangely", # comment3 +43 | | # comment4 +44 | | "formatted", +45 | | # comment5 +46 | | ) # comment6 + | |_^ RUF022 +47 | # comment7 + | + = help: Sort `__all__` alphabetically + +ℹ Unsafe fix +36 36 | ########################################## +37 37 | +38 38 | # comment0 +39 |-__all__ = ("d", "a", # comment1 +40 |- # comment2 +41 |- "f", "b", +42 |- "strangely", # comment3 +43 |- # comment4 + 39 |+__all__ = ( + 40 |+ "a", + 41 |+ "b", + 42 |+ "d", # comment1 + 43 |+ # comment2 + 44 |+ "f", + 45 |+ # comment4 +44 46 | "formatted", +45 |- # comment5 + 47 |+ "strangely", # comment3 +46 48 | ) # comment6 +47 49 | # comment7 +48 50 | + +RUF022.py:65:16: RUF022 [*] `__all__` is not alphabetically sorted | -24 | __all__ += ("e", "f", "g") -25 | else: -26 | __all__ += ["omega", "alpha"] +63 | __all__ += ("e", "f", "g") +64 | else: +65 | __all__ += ["omega", "alpha"] | ^^^^^^^^^^^^^^^^^^ RUF022 -27 | -28 | class IntroducesNonModuleScope: +66 | +67 | class IntroducesNonModuleScope: | = help: Sort `__all__` alphabetically ℹ Safe fix -23 23 | if bool(): -24 24 | __all__ += ("e", "f", "g") -25 25 | else: -26 |- __all__ += ["omega", "alpha"] - 26 |+ __all__ += ["alpha", "omega"] -27 27 | -28 28 | class IntroducesNonModuleScope: -29 29 | __all__ = ("b", "a", "e", "d") +62 62 | if bool(): +63 63 | __all__ += ("e", "f", "g") +64 64 | else: +65 |- __all__ += ["omega", "alpha"] + 65 |+ __all__ += ["alpha", "omega"] +66 66 | +67 67 | class IntroducesNonModuleScope: +68 68 | __all__ = ("b", "a", "e", "d") From f15c0c4babe1fd975b6d317a28dfef5ae29741ef Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Thu, 11 Jan 2024 16:33:26 +0000 Subject: [PATCH 03/77] =?UTF-8?q?All=20tests=20passing=20=F0=9F=A5=B3?= =?UTF-8?q?=F0=9F=A5=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/test/fixtures/ruff/RUF022.py | 49 ++++- .../src/rules/ruff/rules/sort_dunder_all.rs | 95 +++++----- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 171 ++++++++++++++---- 3 files changed, 229 insertions(+), 86 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index 9d7d80c483e27..5ecc7728029ff 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -16,11 +16,11 @@ #################################### __all__ = ( - "d", - "c", # a comment regarding 'c' - "b", - # a comment regarding 'a': - "a" + "d0", + "c0", # a comment regarding 'c0' + "b0", + # a comment regarding 'a0': + "a0" ) __all__ = [ @@ -46,6 +46,30 @@ ) # comment6 # comment7 +__all__ = [ # comment0 + # comment1 + # comment2 + "dx", "cx", "bx", "ax" # comment3 + # comment4 + # comment5 + # comment6 +] # comment7 + +__all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", + "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", + "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", + "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", + "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", + "StreamReader", "StreamWriter", + "StreamReaderWriter", "StreamRecoder", + "getencoder", "getdecoder", "getincrementalencoder", + "getincrementaldecoder", "getreader", "getwriter", + "encode", "decode", "iterencode", "iterdecode", + "strict_errors", "ignore_errors", "replace_errors", + "xmlcharrefreplace_errors", + "backslashreplace_errors", "namereplace_errors", + "register_error", "lookup_error"] + ################################### # These should all not get flagged: ################################### @@ -62,7 +86,7 @@ if bool(): __all__ += ("e", "f", "g") else: - __all__ += ["omega", "alpha"] + __all__ += ["alpha", "omega"] class IntroducesNonModuleScope: __all__ = ("b", "a", "e", "d") @@ -74,3 +98,16 @@ class IntroducesNonModuleScope: ["not", "an", "assignment", "just", "an", "expression"] __all__ = (9, 8, 7) __all__ = ("don't" "care" "about", "__all__" "with", "concatenated" "strings") + +__all__ = ( + "look", + ( + "a_veeeeeeeeeeeeeeeeeeery_long_parenthesized_item_we_dont_care_about" + ), +) + +__all__ = ( # This is just an empty tuple, + # but, + # it's very well +) # documented + diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index c816b246adb52..3fc825b92c970 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -36,21 +36,6 @@ enum DunderAllKind { Tuple { is_parenthesized: bool }, } -impl DunderAllKind { - fn parens(&self, is_multiline: bool) -> (&str, &str) { - match (self, is_multiline) { - (Self::List, _) => ("[", "]"), - ( - Self::Tuple { - is_parenthesized: false, - }, - false, - ) => ("", ""), - _ => ("(", ")"), - } - } -} - fn tuple_is_parenthesized(tuple: &ast::ExprTuple, locator: &Locator) -> bool { let toks = lexer::lex(locator.slice(tuple).trim(), Mode::Expression).collect::>(); matches!( @@ -110,7 +95,6 @@ fn collect_dunder_all_items(lines: Vec) -> Vec { let DunderAllLine { items, comment } = line; match (items.as_slice(), comment) { ([], Some(_)) => { - assert!(this_range.is_none()); this_range = comment; } ([(first_val, first_range), rest @ ..], _) => { @@ -224,50 +208,40 @@ struct SortedDunderAll { new_dunder_all: Option, } -fn multiline_dunder_all_from_items( +fn join_multiline_dunder_all_items( sorted_items: &[DunderAllItem], locator: &Locator, parent: &ast::Stmt, - stylist: &Stylist, - parens: (&str, &str), + additional_indent: &str, + newline: &str, ) -> Option { let Some(indent) = indentation(locator, parent) else { return None; }; - let (opening_paren, closing_paren) = parens; - let added_indent = stylist.indentation(); - let newline = stylist.line_ending().as_str(); - let mut new_dunder_all = String::from(opening_paren); - new_dunder_all.push_str(newline); - for item in sorted_items { + let mut new_dunder_all = String::new(); + for (i, item) in sorted_items.iter().enumerate() { new_dunder_all.push_str(indent); - new_dunder_all.push_str(added_indent); + new_dunder_all.push_str(additional_indent); new_dunder_all.push_str(locator.slice(item)); new_dunder_all.push(','); if let Some(comment) = item.additional_comments { new_dunder_all.push_str(" "); new_dunder_all.push_str(locator.slice(comment)); } - new_dunder_all.push_str(newline); + if i < (sorted_items.len() - 1) { + new_dunder_all.push_str(newline); + } } - new_dunder_all.push_str(indent); - new_dunder_all.push_str(closing_paren); Some(new_dunder_all) } -fn single_line_dunder_all_from_items( - sorted_items: &[DunderAllItem], - locator: &Locator, - parens: (&str, &str), -) -> String { - let joined_items = sorted_items +fn join_singleline_dunder_all_items(sorted_items: &[DunderAllItem], locator: &Locator) -> String { + sorted_items .iter() .map(|item| locator.slice(item)) - .join(", "); - let (opening_paren, closing_paren) = parens; - format!("{opening_paren}{joined_items}{closing_paren}") + .join(", ") } impl<'a> DunderAllValue<'a> { @@ -321,16 +295,45 @@ impl<'a> DunderAllValue<'a> { new_dunder_all: None, }; } - let parens = self.kind.parens(self.multiline); - let new_dunder_all = if self.multiline { - multiline_dunder_all_from_items(&sorted_items, locator, parent, stylist, parens) + let prelude_end = { + if let Some(first_item) = self.items.first() { + let first_item_line_offset = locator.line_start(first_item.start()); + if first_item_line_offset == locator.line_start(self.start()) { + first_item.start() + } else { + first_item_line_offset + } + } else { + self.start() + TextSize::new(1) + } + }; + let postlude_start = { + if let Some(last_item) = self.items.last() { + let last_item_line_offset = locator.line_end(last_item.end()); + if last_item_line_offset == locator.line_end(self.end()) { + last_item.end() + } else { + last_item_line_offset + } + } else { + self.end() - TextSize::new(1) + } + }; + let mut prelude = locator + .slice(TextRange::new(self.start(), prelude_end)) + .to_string(); + let postlude = locator.slice(TextRange::new(postlude_start, self.end())); + + let joined_items = if self.multiline { + let indentation = stylist.indentation(); + let newline = stylist.line_ending().as_str(); + prelude = format!("{}{}", prelude.trim_end(), newline); + join_multiline_dunder_all_items(&sorted_items, locator, parent, indentation, newline) } else { - Some(single_line_dunder_all_from_items( - &sorted_items, - locator, - parens, - )) + Some(join_singleline_dunder_all_items(&sorted_items, locator)) }; + + let new_dunder_all = joined_items.map(|items| format!("{prelude}{items}{postlude}")); SortedDunderAll { was_already_sorted: false, new_dunder_all, diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index 02d34fc6965ae..db3d90c59db78 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -109,11 +109,11 @@ RUF022.py:18:11: RUF022 [*] `__all__` is not alphabetically sorted 17 | 18 | __all__ = ( | ___________^ -19 | | "d", -20 | | "c", # a comment regarding 'c' -21 | | "b", -22 | | # a comment regarding 'a': -23 | | "a" +19 | | "d0", +20 | | "c0", # a comment regarding 'c0' +21 | | "b0", +22 | | # a comment regarding 'a0': +23 | | "a0" 24 | | ) | |_^ RUF022 25 | @@ -125,15 +125,15 @@ RUF022.py:18:11: RUF022 [*] `__all__` is not alphabetically sorted 16 16 | #################################### 17 17 | 18 18 | __all__ = ( - 19 |+ # a comment regarding 'a': - 20 |+ "a", - 21 |+ "b", - 22 |+ "c", # a comment regarding 'c' -19 23 | "d", -20 |- "c", # a comment regarding 'c' -21 |- "b", -22 |- # a comment regarding 'a': -23 |- "a" + 19 |+ # a comment regarding 'a0': + 20 |+ "a0", + 21 |+ "b0", + 22 |+ "c0", # a comment regarding 'c0' +19 23 | "d0", +20 |- "c0", # a comment regarding 'c0' +21 |- "b0", +22 |- # a comment regarding 'a0': +23 |- "a0" 24 24 | ) 25 25 | 26 26 | __all__ = [ @@ -207,31 +207,134 @@ RUF022.py:39:11: RUF022 [*] `__all__` is not alphabetically sorted 44 |+ "f", 45 |+ # comment4 44 46 | "formatted", -45 |- # comment5 47 |+ "strangely", # comment3 -46 48 | ) # comment6 -47 49 | # comment7 -48 50 | +45 48 | # comment5 +46 49 | ) # comment6 +47 50 | # comment7 -RUF022.py:65:16: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:49:11: RUF022 [*] `__all__` is not alphabetically sorted | -63 | __all__ += ("e", "f", "g") -64 | else: -65 | __all__ += ["omega", "alpha"] - | ^^^^^^^^^^^^^^^^^^ RUF022 -66 | -67 | class IntroducesNonModuleScope: +47 | # comment7 +48 | +49 | __all__ = [ # comment0 + | ___________^ +50 | | # comment1 +51 | | # comment2 +52 | | "dx", "cx", "bx", "ax" # comment3 +53 | | # comment4 +54 | | # comment5 +55 | | # comment6 +56 | | ] # comment7 + | |_^ RUF022 +57 | +58 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", | = help: Sort `__all__` alphabetically -ℹ Safe fix -62 62 | if bool(): -63 63 | __all__ += ("e", "f", "g") -64 64 | else: -65 |- __all__ += ["omega", "alpha"] - 65 |+ __all__ += ["alpha", "omega"] -66 66 | -67 67 | class IntroducesNonModuleScope: -68 68 | __all__ = ("b", "a", "e", "d") +ℹ Unsafe fix +48 48 | +49 49 | __all__ = [ # comment0 +50 50 | # comment1 + 51 |+ "ax", + 52 |+ "bx", + 53 |+ "cx", +51 54 | # comment2 +52 |- "dx", "cx", "bx", "ax" # comment3 + 55 |+ "dx", # comment3 +53 56 | # comment4 +54 57 | # comment5 +55 58 | # comment6 + +RUF022.py:58:11: RUF022 [*] `__all__` is not alphabetically sorted + | +56 | ] # comment7 +57 | +58 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", + | ___________^ +59 | | "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", +60 | | "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", +61 | | "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", +62 | | "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", +63 | | "StreamReader", "StreamWriter", +64 | | "StreamReaderWriter", "StreamRecoder", +65 | | "getencoder", "getdecoder", "getincrementalencoder", +66 | | "getincrementaldecoder", "getreader", "getwriter", +67 | | "encode", "decode", "iterencode", "iterdecode", +68 | | "strict_errors", "ignore_errors", "replace_errors", +69 | | "xmlcharrefreplace_errors", +70 | | "backslashreplace_errors", "namereplace_errors", +71 | | "register_error", "lookup_error"] + | |____________________________________________^ RUF022 +72 | +73 | ################################### + | + = help: Sort `__all__` alphabetically + +ℹ Unsafe fix +55 55 | # comment6 +56 56 | ] # comment7 +57 57 | +58 |-__all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", +59 |- "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", +60 |- "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", +61 |- "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", +62 |- "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", +63 |- "StreamReader", "StreamWriter", +64 |- "StreamReaderWriter", "StreamRecoder", +65 |- "getencoder", "getdecoder", "getincrementalencoder", +66 |- "getincrementaldecoder", "getreader", "getwriter", +67 |- "encode", "decode", "iterencode", "iterdecode", +68 |- "strict_errors", "ignore_errors", "replace_errors", +69 |- "xmlcharrefreplace_errors", +70 |- "backslashreplace_errors", "namereplace_errors", +71 |- "register_error", "lookup_error"] + 58 |+__all__ = [ + 59 |+ "BOM", + 60 |+ "BOM32_BE", + 61 |+ "BOM32_LE", + 62 |+ "BOM64_BE", + 63 |+ "BOM64_LE", + 64 |+ "BOM_BE", + 65 |+ "BOM_LE", + 66 |+ "BOM_UTF16", + 67 |+ "BOM_UTF16_BE", + 68 |+ "BOM_UTF16_LE", + 69 |+ "BOM_UTF32", + 70 |+ "BOM_UTF32_BE", + 71 |+ "BOM_UTF32_LE", + 72 |+ "BOM_UTF8", + 73 |+ "Codec", + 74 |+ "CodecInfo", + 75 |+ "EncodedFile", + 76 |+ "IncrementalDecoder", + 77 |+ "IncrementalEncoder", + 78 |+ "StreamReader", + 79 |+ "StreamReaderWriter", + 80 |+ "StreamRecoder", + 81 |+ "StreamWriter", + 82 |+ "backslashreplace_errors", + 83 |+ "decode", + 84 |+ "encode", + 85 |+ "getdecoder", + 86 |+ "getencoder", + 87 |+ "getincrementaldecoder", + 88 |+ "getincrementalencoder", + 89 |+ "getreader", + 90 |+ "getwriter", + 91 |+ "ignore_errors", + 92 |+ "iterdecode", + 93 |+ "iterencode", + 94 |+ "lookup", + 95 |+ "lookup_error", + 96 |+ "namereplace_errors", + 97 |+ "open", + 98 |+ "register", + 99 |+ "register_error", + 100 |+ "replace_errors", + 101 |+ "strict_errors", + 102 |+ "xmlcharrefreplace_errors",] +72 103 | +73 104 | ################################### +74 105 | # These should all not get flagged: From 5d2335b28fdb215dbf8a16913eb510fcf9056480 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Thu, 11 Jan 2024 16:46:54 +0000 Subject: [PATCH 04/77] clean --- .../src/rules/ruff/rules/sort_dunder_all.rs | 473 +++++++++--------- 1 file changed, 224 insertions(+), 249 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 3fc825b92c970..670a3a7054402 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -12,7 +12,6 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; use itertools::Itertools; -use strum_macros::EnumIs; #[violation] pub struct UnsortedDunderAll; @@ -30,231 +29,78 @@ impl Violation for UnsortedDunderAll { } } -#[derive(EnumIs, Eq, PartialEq)] -enum DunderAllKind { - List, - Tuple { is_parenthesized: bool }, -} - -fn tuple_is_parenthesized(tuple: &ast::ExprTuple, locator: &Locator) -> bool { - let toks = lexer::lex(locator.slice(tuple).trim(), Mode::Expression).collect::>(); - matches!( - (toks.first(), toks.get(toks.len() - 2)), - (Some(Ok((Tok::Lpar, _))), Some(Ok((Tok::Rpar, _)))) - ) -} - -#[derive(Clone, Debug)] -struct DunderAllItem { - value: String, - // Each `AllItem` in any given list should have a unique `original_index`: - original_index: u16, - // Note that this range might include comments, etc. - range: TextRange, - additional_comments: Option, -} - -impl Ranged for DunderAllItem { - fn range(&self) -> TextRange { - self.range - } -} - -impl PartialEq for DunderAllItem { - fn eq(&self, other: &Self) -> bool { - self.original_index == other.original_index - } -} - -impl Eq for DunderAllItem {} - -impl DunderAllItem { - fn sort_index(&self) -> (&str, u16) { - (&self.value, self.original_index) +pub(crate) fn sort_dunder_all(checker: &mut Checker, stmt: &ast::Stmt) { + // We're only interested in `__all__` in the global scope + if !checker.semantic().current_scope().kind.is_module() { + return; } -} -impl Ord for DunderAllItem { - fn cmp(&self, other: &Self) -> Ordering { - self.sort_index().cmp(&other.sort_index()) - } -} + // We're only interested in `__all__ = ...` and `__all__ += ...` + let (target, original_value) = match stmt { + ast::Stmt::Assign(ast::StmtAssign { value, targets, .. }) => match targets.as_slice() { + [ast::Expr::Name(ast::ExprName { id, .. })] => (id, value.as_ref()), + _ => return, + }, + ast::Stmt::AugAssign(ast::StmtAugAssign { + value, + target, + op: ast::Operator::Add, + .. + }) => match target.as_ref() { + ast::Expr::Name(ast::ExprName { id, .. }) => (id, value.as_ref()), + _ => return, + }, + _ => return, + }; -impl PartialOrd for DunderAllItem { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) + if target != "__all__" { + return; } -} - -fn collect_dunder_all_items(lines: Vec) -> Vec { - let mut all_items = vec![]; - let mut this_range = None; - let mut idx = 0; - for line in lines { - let DunderAllLine { items, comment } = line; - match (items.as_slice(), comment) { - ([], Some(_)) => { - this_range = comment; - } - ([(first_val, first_range), rest @ ..], _) => { - let range = this_range.map_or(*first_range, |r| { - TextRange::new(r.start(), first_range.end()) - }); - all_items.push(DunderAllItem { - value: first_val.clone(), - original_index: idx, - range, - additional_comments: comment, - }); - this_range = None; - idx += 1; - for (value, range) in rest { - all_items.push(DunderAllItem { - value: value.clone(), - original_index: idx, - range: *range, - additional_comments: None, - }); - idx += 1; - } - } - _ => unreachable!( - "This should be unreachable. - Any lines that have neither comments nor items - should have been filtered out by this point." - ), - } - } + let locator = checker.locator(); - all_items -} + let Some(dunder_all_val) = DunderAllValue::from_expr(original_value, locator) else { + return; + }; -#[derive(Debug)] -struct DunderAllLine { - items: Vec<(String, TextRange)>, - comment: Option, -} + let sorting_result = dunder_all_val.construct_sorted_all(locator, stmt, checker.stylist()); -impl DunderAllLine { - fn new(items: &[(String, TextRange)], comment: Option, offset: TextSize) -> Self { - assert!(comment.is_some() || !items.is_empty()); - Self { - items: items - .iter() - .map(|(s, r)| (s.to_owned(), r + offset)) - .collect(), - comment: comment.map(|c| c + offset), - } + if sorting_result.was_already_sorted { + return; } -} -fn collect_dunder_all_lines(range: TextRange, locator: &Locator) -> Option> { - let mut parentheses_open = false; - let mut lines = vec![]; + let dunder_all_range = dunder_all_val.range(); + let mut diagnostic = Diagnostic::new(UnsortedDunderAll, dunder_all_range); - let mut items_in_line = vec![]; - let mut comment_in_line = None; - for pair in lexer::lex(locator.slice(range).trim(), Mode::Expression) { - let (tok, subrange) = pair.ok()?; - match tok { - Tok::Lpar | Tok::Lsqb => { - if parentheses_open { - return None; - } - parentheses_open = true; - } - Tok::Rpar | Tok::Rsqb | Tok::Newline => { - if !(items_in_line.is_empty() && comment_in_line.is_none()) { - lines.push(DunderAllLine::new( - &items_in_line, - comment_in_line, - range.start(), - )); - } - break; - } - Tok::NonLogicalNewline => { - if !(items_in_line.is_empty() && comment_in_line.is_none()) { - lines.push(DunderAllLine::new( - &items_in_line, - comment_in_line, - range.start(), - )); - items_in_line = vec![]; - comment_in_line = None; - } + if let Some(new_dunder_all) = sorting_result.new_dunder_all { + let applicability = { + if dunder_all_val.multiline { + Applicability::Unsafe + } else { + Applicability::Safe } - Tok::Comment(_) => comment_in_line = Some(subrange), - Tok::String { value, .. } => items_in_line.push((value, subrange)), - Tok::Comma => continue, - _ => return None, - } + }; + diagnostic.set_fix(Fix::applicable_edit( + Edit::range_replacement(new_dunder_all, dunder_all_range), + applicability, + )); } - Some(lines) + + checker.diagnostics.push(diagnostic); } -#[derive(PartialEq, Eq)] struct DunderAllValue<'a> { - kind: DunderAllKind, items: Vec, range: &'a TextRange, - ctx: &'a ast::ExprContext, multiline: bool, } -struct SortedDunderAll { - was_already_sorted: bool, - new_dunder_all: Option, -} - -fn join_multiline_dunder_all_items( - sorted_items: &[DunderAllItem], - locator: &Locator, - parent: &ast::Stmt, - additional_indent: &str, - newline: &str, -) -> Option { - let Some(indent) = indentation(locator, parent) else { - return None; - }; - - let mut new_dunder_all = String::new(); - for (i, item) in sorted_items.iter().enumerate() { - new_dunder_all.push_str(indent); - new_dunder_all.push_str(additional_indent); - new_dunder_all.push_str(locator.slice(item)); - new_dunder_all.push(','); - if let Some(comment) = item.additional_comments { - new_dunder_all.push_str(" "); - new_dunder_all.push_str(locator.slice(comment)); - } - if i < (sorted_items.len() - 1) { - new_dunder_all.push_str(newline); - } - } - - Some(new_dunder_all) -} - -fn join_singleline_dunder_all_items(sorted_items: &[DunderAllItem], locator: &Locator) -> String { - sorted_items - .iter() - .map(|item| locator.slice(item)) - .join(", ") -} - impl<'a> DunderAllValue<'a> { fn from_expr(value: &'a ast::Expr, locator: &Locator) -> Option> { let is_multiline = locator.contains_line_break(value.range()); - let (kind, elts, range, ctx) = match value { - ast::Expr::List(ast::ExprList { elts, range, ctx }) => { - (DunderAllKind::List, elts, range, ctx) - } - ast::Expr::Tuple(tuple @ ast::ExprTuple { elts, range, ctx }) => { - let is_parenthesized = tuple_is_parenthesized(tuple, locator); - (DunderAllKind::Tuple { is_parenthesized }, elts, range, ctx) - } + let (elts, range) = match value { + ast::Expr::List(ast::ExprList { elts, range, .. }) => (elts, range), + ast::Expr::Tuple(ast::ExprTuple { elts, range, .. }) => (elts, range), _ => return None, }; // An `__all__` definition with <2 elements can't be unsorted; @@ -273,10 +119,8 @@ impl<'a> DunderAllValue<'a> { let lines = collect_dunder_all_lines(*range, locator)?; let items = collect_dunder_all_items(lines); Some(DunderAllValue { - kind, items, range, - ctx, multiline: is_multiline, }) } @@ -347,62 +191,193 @@ impl Ranged for DunderAllValue<'_> { } } -pub(crate) fn sort_dunder_all(checker: &mut Checker, stmt: &ast::Stmt) { - // We're only interested in `__all__` in the global scope - if !checker.semantic().current_scope().kind.is_module() { - return; +struct SortedDunderAll { + was_already_sorted: bool, + new_dunder_all: Option, +} + +fn collect_dunder_all_lines(range: TextRange, locator: &Locator) -> Option> { + let mut parentheses_open = false; + let mut lines = vec![]; + + let mut items_in_line = vec![]; + let mut comment_in_line = None; + for pair in lexer::lex(locator.slice(range).trim(), Mode::Expression) { + let (tok, subrange) = pair.ok()?; + match tok { + Tok::Lpar | Tok::Lsqb => { + if parentheses_open { + return None; + } + parentheses_open = true; + } + Tok::Rpar | Tok::Rsqb | Tok::Newline => { + if !(items_in_line.is_empty() && comment_in_line.is_none()) { + lines.push(DunderAllLine::new( + &items_in_line, + comment_in_line, + range.start(), + )); + } + break; + } + Tok::NonLogicalNewline => { + if !(items_in_line.is_empty() && comment_in_line.is_none()) { + lines.push(DunderAllLine::new( + &items_in_line, + comment_in_line, + range.start(), + )); + items_in_line = vec![]; + comment_in_line = None; + } + } + Tok::Comment(_) => comment_in_line = Some(subrange), + Tok::String { value, .. } => items_in_line.push((value, subrange)), + Tok::Comma => continue, + _ => return None, + } } + Some(lines) +} - // We're only interested in `__all__ = ...` and `__all__ += ...` - let (target, original_value) = match stmt { - ast::Stmt::Assign(ast::StmtAssign { value, targets, .. }) => match targets.as_slice() { - [ast::Expr::Name(ast::ExprName { id, .. })] => (id, value.as_ref()), - _ => return, - }, - ast::Stmt::AugAssign(ast::StmtAugAssign { - value, - target, - op: ast::Operator::Add, - .. - }) => match target.as_ref() { - ast::Expr::Name(ast::ExprName { id, .. }) => (id, value.as_ref()), - _ => return, - }, - _ => return, - }; +#[derive(Debug)] +struct DunderAllLine { + items: Vec<(String, TextRange)>, + comment: Option, +} - if target != "__all__" { - return; +impl DunderAllLine { + fn new(items: &[(String, TextRange)], comment: Option, offset: TextSize) -> Self { + assert!(comment.is_some() || !items.is_empty()); + Self { + items: items + .iter() + .map(|(s, r)| (s.to_owned(), r + offset)) + .collect(), + comment: comment.map(|c| c + offset), + } } +} - let locator = checker.locator(); +fn collect_dunder_all_items(lines: Vec) -> Vec { + let mut all_items = vec![]; - let Some(dunder_all_val) = DunderAllValue::from_expr(original_value, locator) else { - return; - }; + let mut this_range = None; + let mut idx = 0; + for line in lines { + let DunderAllLine { items, comment } = line; + match (items.as_slice(), comment) { + ([], Some(_)) => { + this_range = comment; + } + ([(first_val, first_range), rest @ ..], _) => { + let range = this_range.map_or(*first_range, |r| { + TextRange::new(r.start(), first_range.end()) + }); + all_items.push(DunderAllItem { + value: first_val.clone(), + original_index: idx, + range, + additional_comments: comment, + }); + this_range = None; + idx += 1; + for (value, range) in rest { + all_items.push(DunderAllItem { + value: value.clone(), + original_index: idx, + range: *range, + additional_comments: None, + }); + idx += 1; + } + } + _ => unreachable!( + "This should be unreachable. + Any lines that have neither comments nor items + should have been filtered out by this point." + ), + } + } - let sorting_result = dunder_all_val.construct_sorted_all(locator, stmt, checker.stylist()); + all_items +} - if sorting_result.was_already_sorted { - return; +#[derive(Clone, Debug)] +struct DunderAllItem { + value: String, + // Each `AllItem` in any given list should have a unique `original_index`: + original_index: u16, + // Note that this range might include comments, etc. + range: TextRange, + additional_comments: Option, +} + +impl Ranged for DunderAllItem { + fn range(&self) -> TextRange { + self.range } +} - let dunder_all_range = dunder_all_val.range(); - let mut diagnostic = Diagnostic::new(UnsortedDunderAll, dunder_all_range); +impl PartialEq for DunderAllItem { + fn eq(&self, other: &Self) -> bool { + self.original_index == other.original_index + } +} - if let Some(new_dunder_all) = sorting_result.new_dunder_all { - let applicability = { - if dunder_all_val.multiline { - Applicability::Unsafe - } else { - Applicability::Safe - } - }; - diagnostic.set_fix(Fix::applicable_edit( - Edit::range_replacement(new_dunder_all, dunder_all_range), - applicability, - )); +impl Eq for DunderAllItem {} + +impl DunderAllItem { + fn sort_index(&self) -> (&str, u16) { + (&self.value, self.original_index) } +} - checker.diagnostics.push(diagnostic); +impl Ord for DunderAllItem { + fn cmp(&self, other: &Self) -> Ordering { + self.sort_index().cmp(&other.sort_index()) + } +} + +impl PartialOrd for DunderAllItem { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +fn join_singleline_dunder_all_items(sorted_items: &[DunderAllItem], locator: &Locator) -> String { + sorted_items + .iter() + .map(|item| locator.slice(item)) + .join(", ") +} + +fn join_multiline_dunder_all_items( + sorted_items: &[DunderAllItem], + locator: &Locator, + parent: &ast::Stmt, + additional_indent: &str, + newline: &str, +) -> Option { + let Some(indent) = indentation(locator, parent) else { + return None; + }; + + let mut new_dunder_all = String::new(); + for (i, item) in sorted_items.iter().enumerate() { + new_dunder_all.push_str(indent); + new_dunder_all.push_str(additional_indent); + new_dunder_all.push_str(locator.slice(item)); + new_dunder_all.push(','); + if let Some(comment) = item.additional_comments { + new_dunder_all.push_str(" "); + new_dunder_all.push_str(locator.slice(comment)); + } + if i < (sorted_items.len() - 1) { + new_dunder_all.push_str(newline); + } + } + + Some(new_dunder_all) } From 265b5908aa4226d1e2554f79a0f00ce218322d88 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Thu, 11 Jan 2024 17:06:34 +0000 Subject: [PATCH 05/77] Document; adjust when it is marked as safe --- .../src/rules/ruff/rules/sort_dunder_all.rs | 48 ++++++++++++++++++- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 2 +- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 670a3a7054402..afc82976c1986 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -13,6 +13,47 @@ use crate::checkers::ast::Checker; use itertools::Itertools; +/// ## What it does +/// Checks for `__all__` definitions that are not alphabetically sorted. +/// +/// ## Why is this bad? +/// Consistency is good. Use a common convention for `__all__` to make your +/// code more readable and idiomatic. +/// +/// ## Example +/// ```python +/// import sys +/// +/// __all__ = [ +/// "b", +/// "c", +/// "a" +/// ] +/// +/// if sys.platform == "win32": +/// __all__ += ["z", "y"] +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// __all__ = [ +/// "a", +/// "b", +/// "c" +/// ] +/// +/// if sys.platform == "win32": +/// __all__ += ["y", "z"] +/// ``` +/// +/// ## Fix safety +/// This rule's fix should be safe for single-line `__all__` definitions +/// and for multiline `__all__` definitions without comments. +/// For multiline `__all__` definitions that include comments, +/// the fix is marked as unsafe, as it can be hard to tell where the comments +/// should be moved to when sorting the contents of `__all__`. #[violation] pub struct UnsortedDunderAll; @@ -74,7 +115,12 @@ pub(crate) fn sort_dunder_all(checker: &mut Checker, stmt: &ast::Stmt) { if let Some(new_dunder_all) = sorting_result.new_dunder_all { let applicability = { - if dunder_all_val.multiline { + if dunder_all_val.multiline + && checker + .indexer() + .comment_ranges() + .intersects(original_value.range()) + { Applicability::Unsafe } else { Applicability::Safe diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index db3d90c59db78..e405ae6c7b67b 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -270,7 +270,7 @@ RUF022.py:58:11: RUF022 [*] `__all__` is not alphabetically sorted | = help: Sort `__all__` alphabetically -ℹ Unsafe fix +ℹ Safe fix 55 55 | # comment6 56 56 | ] # comment7 57 57 | From 8619dad5d830d23b52674feb2b1e5d7858d8fc73 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Thu, 11 Jan 2024 17:23:18 +0000 Subject: [PATCH 06/77] More comments; misc nits --- .../src/rules/ruff/rules/sort_dunder_all.rs | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index afc82976c1986..34496e593a7da 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -70,6 +70,7 @@ impl Violation for UnsortedDunderAll { } } +/// RUF022 pub(crate) fn sort_dunder_all(checker: &mut Checker, stmt: &ast::Stmt) { // We're only interested in `__all__` in the global scope if !checker.semantic().current_scope().kind.is_module() { @@ -143,6 +144,7 @@ struct DunderAllValue<'a> { impl<'a> DunderAllValue<'a> { fn from_expr(value: &'a ast::Expr, locator: &Locator) -> Option> { + // Step (1): inspect the AST to check that we're looking at something vaguely sane: let is_multiline = locator.contains_line_break(value.range()); let (elts, range) = match value { ast::Expr::List(ast::ExprList { elts, range, .. }) => (elts, range), @@ -162,8 +164,21 @@ impl<'a> DunderAllValue<'a> { return None; } } + // Step (2): parse the `__all__` definition using the raw tokens. + // + // (2a). Start by collecting information on each line individually: let lines = collect_dunder_all_lines(*range, locator)?; + + // (2b). Group lines together into sortable "items": + // - Any "item" contains a single element of the `__all__` list/tuple + // - "Items" are ordered according to the element they contain + // - Assume that any comments on their own line are meant to be grouped + // with the element immediately below them: if the element moves, + // the comments above the element move with it. + // - The same goes for any comments on the same line as an element: + // if the element moves, the comment moves with it. let items = collect_dunder_all_items(lines); + Some(DunderAllValue { items, range, @@ -185,6 +200,13 @@ impl<'a> DunderAllValue<'a> { new_dunder_all: None, }; } + // As well as the "items" in the `__all__` definition, + // there is also a "prelude" and a "postlude": + // - Prelude == the region of source code from the opening parenthesis + // (if there was one), up to the start of the first element in `__all__`. + // - Postlude == the region of source code from the end of the last + // element in `__all__` up to and including the closing parenthesis + // (if there was one). let prelude_end = { if let Some(first_item) = self.items.first() { let first_item_line_offset = locator.line_start(first_item.start()); @@ -243,9 +265,11 @@ struct SortedDunderAll { } fn collect_dunder_all_lines(range: TextRange, locator: &Locator) -> Option> { + // Collect data on each line of `__all__`. + // Return `None` if `__all__` appears to be invalid, + // or if it's an edge case we don't care about. let mut parentheses_open = false; let mut lines = vec![]; - let mut items_in_line = vec![]; let mut comment_in_line = None; for pair in lexer::lex(locator.slice(range).trim(), Mode::Expression) { @@ -307,8 +331,11 @@ impl DunderAllLine { } fn collect_dunder_all_items(lines: Vec) -> Vec { + // Given data on each line in `__all__`, group lines together into "items". + // Each item contains exactly one element, + // but might contain multiple comments attached to that element + // that must move with the element when `__all__` is sorted. let mut all_items = vec![]; - let mut this_range = None; let mut idx = 0; for line in lines { @@ -346,7 +373,6 @@ fn collect_dunder_all_items(lines: Vec) -> Vec { ), } } - all_items } @@ -409,7 +435,6 @@ fn join_multiline_dunder_all_items( let Some(indent) = indentation(locator, parent) else { return None; }; - let mut new_dunder_all = String::new(); for (i, item) in sorted_items.iter().enumerate() { new_dunder_all.push_str(indent); @@ -424,6 +449,5 @@ fn join_multiline_dunder_all_items( new_dunder_all.push_str(newline); } } - Some(new_dunder_all) } From 5bacac077a1421cb5074e0e3ec21b340717bfecd Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Thu, 11 Jan 2024 17:50:26 +0000 Subject: [PATCH 07/77] make mkdocs happy --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 34496e593a7da..e0723802252d2 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -27,7 +27,7 @@ use itertools::Itertools; /// __all__ = [ /// "b", /// "c", -/// "a" +/// "a", /// ] /// /// if sys.platform == "win32": @@ -41,7 +41,7 @@ use itertools::Itertools; /// __all__ = [ /// "a", /// "b", -/// "c" +/// "c", /// ] /// /// if sys.platform == "win32": From e4842bdc6f38da4ea54982d83d55a88f7a5b8d5e Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Thu, 11 Jan 2024 18:46:17 +0000 Subject: [PATCH 08/77] Don't add trailing comma where unnecessary; fix misc nits --- .../src/rules/ruff/rules/sort_dunder_all.rs | 52 ++++++++++--------- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 2 +- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index e0723802252d2..e6d5001b91352 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -177,7 +177,7 @@ impl<'a> DunderAllValue<'a> { // the comments above the element move with it. // - The same goes for any comments on the same line as an element: // if the element moves, the comment moves with it. - let items = collect_dunder_all_items(lines); + let items = collect_dunder_all_items(&lines); Some(DunderAllValue { items, @@ -208,27 +208,23 @@ impl<'a> DunderAllValue<'a> { // element in `__all__` up to and including the closing parenthesis // (if there was one). let prelude_end = { - if let Some(first_item) = self.items.first() { - let first_item_line_offset = locator.line_start(first_item.start()); - if first_item_line_offset == locator.line_start(self.start()) { - first_item.start() - } else { - first_item_line_offset - } + // Should be safe: we should already have returned by now if there are 0 items + let first_item = &self.items[0]; + let first_item_line_offset = locator.line_start(first_item.start()); + if first_item_line_offset == locator.line_start(self.start()) { + first_item.start() } else { - self.start() + TextSize::new(1) + first_item_line_offset } }; - let postlude_start = { - if let Some(last_item) = self.items.last() { - let last_item_line_offset = locator.line_end(last_item.end()); - if last_item_line_offset == locator.line_end(self.end()) { - last_item.end() - } else { - last_item_line_offset - } + let (needs_trailing_comma, postlude_start) = { + // Should be safe: we should already have returned by now if there are 0 items + let last_item = &self.items[self.items.len() - 1]; + let last_item_line_offset = locator.line_end(last_item.end()); + if last_item_line_offset == locator.line_end(self.end()) { + (false, last_item.end()) } else { - self.end() - TextSize::new(1) + (true, last_item_line_offset) } }; let mut prelude = locator @@ -240,7 +236,9 @@ impl<'a> DunderAllValue<'a> { let indentation = stylist.indentation(); let newline = stylist.line_ending().as_str(); prelude = format!("{}{}", prelude.trim_end(), newline); - join_multiline_dunder_all_items(&sorted_items, locator, parent, indentation, newline) + join_multiline_dunder_all_items( + &sorted_items, locator, parent, indentation, newline, needs_trailing_comma + ) } else { Some(join_singleline_dunder_all_items(&sorted_items, locator)) }; @@ -298,7 +296,7 @@ fn collect_dunder_all_lines(range: TextRange, locator: &Locator) -> Option) -> Vec { +fn collect_dunder_all_items(lines: &[DunderAllLine]) -> Vec { // Given data on each line in `__all__`, group lines together into "items". // Each item contains exactly one element, // but might contain multiple comments attached to that element @@ -342,7 +340,7 @@ fn collect_dunder_all_items(lines: Vec) -> Vec { let DunderAllLine { items, comment } = line; match (items.as_slice(), comment) { ([], Some(_)) => { - this_range = comment; + this_range = *comment; } ([(first_val, first_range), rest @ ..], _) => { let range = this_range.map_or(*first_range, |r| { @@ -352,7 +350,7 @@ fn collect_dunder_all_items(lines: Vec) -> Vec { value: first_val.clone(), original_index: idx, range, - additional_comments: comment, + additional_comments: *comment, }); this_range = None; idx += 1; @@ -431,6 +429,7 @@ fn join_multiline_dunder_all_items( parent: &ast::Stmt, additional_indent: &str, newline: &str, + needs_trailing_comma: bool, ) -> Option { let Some(indent) = indentation(locator, parent) else { return None; @@ -440,12 +439,15 @@ fn join_multiline_dunder_all_items( new_dunder_all.push_str(indent); new_dunder_all.push_str(additional_indent); new_dunder_all.push_str(locator.slice(item)); - new_dunder_all.push(','); + let is_final_item = i == (sorted_items.len() - 1); + if !is_final_item || needs_trailing_comma { + new_dunder_all.push(','); + } if let Some(comment) = item.additional_comments { new_dunder_all.push_str(" "); new_dunder_all.push_str(locator.slice(comment)); } - if i < (sorted_items.len() - 1) { + if !is_final_item { new_dunder_all.push_str(newline); } } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index e405ae6c7b67b..232d9a82214e0 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -332,7 +332,7 @@ RUF022.py:58:11: RUF022 [*] `__all__` is not alphabetically sorted 99 |+ "register_error", 100 |+ "replace_errors", 101 |+ "strict_errors", - 102 |+ "xmlcharrefreplace_errors",] + 102 |+ "xmlcharrefreplace_errors"] 72 103 | 73 104 | ################################### 74 105 | # These should all not get flagged: From a7dc871756edd7a5215cb2bea211aefe336d35ed Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Thu, 11 Jan 2024 18:57:59 +0000 Subject: [PATCH 09/77] I'm in rustland where the enums are cool --- .../src/rules/ruff/rules/sort_dunder_all.rs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index e6d5001b91352..20e594dbbacec 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -105,16 +105,16 @@ pub(crate) fn sort_dunder_all(checker: &mut Checker, stmt: &ast::Stmt) { return; }; - let sorting_result = dunder_all_val.construct_sorted_all(locator, stmt, checker.stylist()); - - if sorting_result.was_already_sorted { - return; - } + let new_dunder_all = match dunder_all_val.construct_sorted_all(locator, stmt, checker.stylist()) + { + SortedDunderAll::AlreadySorted => return, + SortedDunderAll::Sorted(value) => value, + }; let dunder_all_range = dunder_all_val.range(); let mut diagnostic = Diagnostic::new(UnsortedDunderAll, dunder_all_range); - if let Some(new_dunder_all) = sorting_result.new_dunder_all { + if let Some(new_dunder_all) = new_dunder_all { let applicability = { if dunder_all_val.multiline && checker @@ -195,10 +195,7 @@ impl<'a> DunderAllValue<'a> { let mut sorted_items = self.items.clone(); sorted_items.sort(); if sorted_items == self.items { - return SortedDunderAll { - was_already_sorted: true, - new_dunder_all: None, - }; + return SortedDunderAll::AlreadySorted; } // As well as the "items" in the `__all__` definition, // there is also a "prelude" and a "postlude": @@ -237,17 +234,19 @@ impl<'a> DunderAllValue<'a> { let newline = stylist.line_ending().as_str(); prelude = format!("{}{}", prelude.trim_end(), newline); join_multiline_dunder_all_items( - &sorted_items, locator, parent, indentation, newline, needs_trailing_comma + &sorted_items, + locator, + parent, + indentation, + newline, + needs_trailing_comma, ) } else { Some(join_singleline_dunder_all_items(&sorted_items, locator)) }; let new_dunder_all = joined_items.map(|items| format!("{prelude}{items}{postlude}")); - SortedDunderAll { - was_already_sorted: false, - new_dunder_all, - } + SortedDunderAll::Sorted(new_dunder_all) } } @@ -257,9 +256,10 @@ impl Ranged for DunderAllValue<'_> { } } -struct SortedDunderAll { - was_already_sorted: bool, - new_dunder_all: Option, +#[derive(Debug)] +enum SortedDunderAll { + AlreadySorted, + Sorted(Option), } fn collect_dunder_all_lines(range: TextRange, locator: &Locator) -> Option> { From cc42bf9e68cd6ba77b39a0837cba609e56c6ba99 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Thu, 11 Jan 2024 21:22:05 +0000 Subject: [PATCH 10/77] Make it work for `AnnAssign` nodes, and use more type-safe hooks in `statement.rs` --- .../resources/test/fixtures/ruff/RUF022.py | 2 + .../src/checkers/ast/analyze/statement.rs | 14 +- .../src/rules/ruff/rules/sort_dunder_all.rs | 91 ++-- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 415 +++++++++--------- 4 files changed, 290 insertions(+), 232 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index 5ecc7728029ff..dcb978554424c 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -11,6 +11,8 @@ else: __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +__all__: list[str] = ["the", "three", "little", "pigs"] + #################################### # Neat multiline __all__ definitions #################################### diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 2cc434916efc1..4290fab5041f8 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -19,9 +19,6 @@ use crate::settings::types::PythonVersion; /// Run lint rules over a [`Stmt`] syntax node. pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { - if checker.settings.rules.enabled(Rule::UnsortedDunderAll) { - ruff::rules::sort_dunder_all(checker, stmt); - } match stmt { Stmt::Global(ast::StmtGlobal { names, range: _ }) => { if checker.enabled(Rule::GlobalAtModuleLevel) { @@ -1068,12 +1065,15 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { pylint::rules::misplaced_bare_raise(checker, raise); } } - Stmt::AugAssign(ast::StmtAugAssign { target, .. }) => { + Stmt::AugAssign(augassign @ ast::StmtAugAssign { target, .. }) => { if checker.enabled(Rule::GlobalStatement) { if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { pylint::rules::global_statement(checker, id); } } + if checker.enabled(Rule::UnsortedDunderAll) { + ruff::rules::sort_dunder_all_augassign(checker, augassign, stmt); + } } Stmt::If( if_ @ ast::StmtIf { @@ -1460,6 +1460,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.settings.rules.enabled(Rule::TypeBivariance) { pylint::rules::type_bivariance(checker, value); } + if checker.settings.rules.enabled(Rule::UnsortedDunderAll) { + ruff::rules::sort_dunder_all_assign(checker, assign, stmt); + } if checker.source_type.is_stub() { if checker.any_enabled(&[ Rule::UnprefixedTypeParam, @@ -1530,6 +1533,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::NonPEP695TypeAlias) { pyupgrade::rules::non_pep695_type_alias(checker, assign_stmt); } + if checker.settings.rules.enabled(Rule::UnsortedDunderAll) { + ruff::rules::sort_dunder_all_annassign(checker, assign_stmt, stmt); + } if checker.source_type.is_stub() { if let Some(value) = value { if checker.enabled(Rule::AssignmentDefaultInStub) { diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 20e594dbbacec..805fdca88ec95 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -70,46 +70,78 @@ impl Violation for UnsortedDunderAll { } } -/// RUF022 -pub(crate) fn sort_dunder_all(checker: &mut Checker, stmt: &ast::Stmt) { - // We're only interested in `__all__` in the global scope - if !checker.semantic().current_scope().kind.is_module() { +pub(crate) fn sort_dunder_all_assign( + checker: &mut Checker, + node: &ast::StmtAssign, + parent: &ast::Stmt, +) { + let ast::StmtAssign { value, targets, .. } = node; + let [ast::Expr::Name(ast::ExprName { id, .. })] = targets.as_slice() else { return; - } + }; + sort_dunder_all(checker, id, value, parent); +} - // We're only interested in `__all__ = ...` and `__all__ += ...` - let (target, original_value) = match stmt { - ast::Stmt::Assign(ast::StmtAssign { value, targets, .. }) => match targets.as_slice() { - [ast::Expr::Name(ast::ExprName { id, .. })] => (id, value.as_ref()), - _ => return, - }, - ast::Stmt::AugAssign(ast::StmtAugAssign { - value, - target, - op: ast::Operator::Add, - .. - }) => match target.as_ref() { - ast::Expr::Name(ast::ExprName { id, .. }) => (id, value.as_ref()), - _ => return, - }, - _ => return, +pub(crate) fn sort_dunder_all_augassign( + checker: &mut Checker, + node: &ast::StmtAugAssign, + parent: &ast::Stmt, +) { + let ast::StmtAugAssign { + value, + target, + op: ast::Operator::Add, + .. + } = node + else { + return; + }; + let ast::Expr::Name(ast::ExprName { id, .. }) = target.as_ref() else { + return; }; + sort_dunder_all(checker, id, value, parent); +} +pub(crate) fn sort_dunder_all_annassign( + checker: &mut Checker, + node: &ast::StmtAnnAssign, + parent: &ast::Stmt, +) { + let ast::StmtAnnAssign { + target, + value: Some(val), + .. + } = node + else { + return; + }; + let ast::Expr::Name(ast::ExprName { id, .. }) = target.as_ref() else { + return; + }; + sort_dunder_all(checker, id, val, parent); +} + +fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr, parent: &ast::Stmt) { if target != "__all__" { return; } + // We're only interested in `__all__` in the global scope + if !checker.semantic().current_scope().kind.is_module() { + return; + } + let locator = checker.locator(); - let Some(dunder_all_val) = DunderAllValue::from_expr(original_value, locator) else { + let Some(dunder_all_val) = DunderAllValue::from_expr(node, locator) else { return; }; - let new_dunder_all = match dunder_all_val.construct_sorted_all(locator, stmt, checker.stylist()) - { - SortedDunderAll::AlreadySorted => return, - SortedDunderAll::Sorted(value) => value, - }; + let new_dunder_all = + match dunder_all_val.construct_sorted_all(locator, parent, checker.stylist()) { + SortedDunderAll::AlreadySorted => return, + SortedDunderAll::Sorted(value) => value, + }; let dunder_all_range = dunder_all_val.range(); let mut diagnostic = Diagnostic::new(UnsortedDunderAll, dunder_all_range); @@ -117,10 +149,7 @@ pub(crate) fn sort_dunder_all(checker: &mut Checker, stmt: &ast::Stmt) { if let Some(new_dunder_all) = new_dunder_all { let applicability = { if dunder_all_val.multiline - && checker - .indexer() - .comment_ranges() - .intersects(original_value.range()) + && checker.indexer().comment_ranges().intersects(node.range()) { Applicability::Unsafe } else { diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index 232d9a82214e0..4df6920a74dfa 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -89,7 +89,7 @@ RUF022.py:12:16: RUF022 [*] `__all__` is not alphabetically sorted 12 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) | ^^^^^^^^^^^^^^^^^^^^^^ RUF022 13 | -14 | #################################### +14 | __all__: list[str] = ["the", "three", "little", "pigs"] | = help: Sort `__all__` alphabetically @@ -100,241 +100,262 @@ RUF022.py:12:16: RUF022 [*] `__all__` is not alphabetically sorted 12 |- __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) 12 |+ __all__ += "foo1", "foo2", "foo3" # NB: an implicit tuple (without parens) 13 13 | -14 14 | #################################### -15 15 | # Neat multiline __all__ definitions +14 14 | __all__: list[str] = ["the", "three", "little", "pigs"] +15 15 | -RUF022.py:18:11: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:14:22: RUF022 [*] `__all__` is not alphabetically sorted | -16 | #################################### -17 | -18 | __all__ = ( +12 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +13 | +14 | __all__: list[str] = ["the", "three", "little", "pigs"] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF022 +15 | +16 | #################################### + | + = help: Sort `__all__` alphabetically + +ℹ Safe fix +11 11 | else: +12 12 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +13 13 | +14 |-__all__: list[str] = ["the", "three", "little", "pigs"] + 14 |+__all__: list[str] = ["little", "pigs", "the", "three"] +15 15 | +16 16 | #################################### +17 17 | # Neat multiline __all__ definitions + +RUF022.py:20:11: RUF022 [*] `__all__` is not alphabetically sorted + | +18 | #################################### +19 | +20 | __all__ = ( | ___________^ -19 | | "d0", -20 | | "c0", # a comment regarding 'c0' -21 | | "b0", -22 | | # a comment regarding 'a0': -23 | | "a0" -24 | | ) +21 | | "d0", +22 | | "c0", # a comment regarding 'c0' +23 | | "b0", +24 | | # a comment regarding 'a0': +25 | | "a0" +26 | | ) | |_^ RUF022 -25 | -26 | __all__ = [ +27 | +28 | __all__ = [ | = help: Sort `__all__` alphabetically ℹ Unsafe fix -16 16 | #################################### -17 17 | -18 18 | __all__ = ( - 19 |+ # a comment regarding 'a0': - 20 |+ "a0", - 21 |+ "b0", - 22 |+ "c0", # a comment regarding 'c0' -19 23 | "d0", -20 |- "c0", # a comment regarding 'c0' -21 |- "b0", -22 |- # a comment regarding 'a0': -23 |- "a0" -24 24 | ) -25 25 | -26 26 | __all__ = [ +18 18 | #################################### +19 19 | +20 20 | __all__ = ( + 21 |+ # a comment regarding 'a0': + 22 |+ "a0", + 23 |+ "b0", + 24 |+ "c0", # a comment regarding 'c0' +21 25 | "d0", +22 |- "c0", # a comment regarding 'c0' +23 |- "b0", +24 |- # a comment regarding 'a0': +25 |- "a0" +26 26 | ) +27 27 | +28 28 | __all__ = [ -RUF022.py:26:11: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:28:11: RUF022 [*] `__all__` is not alphabetically sorted | -24 | ) -25 | -26 | __all__ = [ +26 | ) +27 | +28 | __all__ = [ | ___________^ -27 | | "d", -28 | | "c", # a comment regarding 'c' -29 | | "b", -30 | | # a comment regarding 'a': -31 | | "a" -32 | | ] +29 | | "d", +30 | | "c", # a comment regarding 'c' +31 | | "b", +32 | | # a comment regarding 'a': +33 | | "a" +34 | | ] | |_^ RUF022 -33 | -34 | ########################################## +35 | +36 | ########################################## | = help: Sort `__all__` alphabetically ℹ Unsafe fix -24 24 | ) -25 25 | -26 26 | __all__ = [ - 27 |+ # a comment regarding 'a': - 28 |+ "a", - 29 |+ "b", - 30 |+ "c", # a comment regarding 'c' -27 31 | "d", -28 |- "c", # a comment regarding 'c' -29 |- "b", -30 |- # a comment regarding 'a': -31 |- "a" -32 32 | ] -33 33 | -34 34 | ########################################## +26 26 | ) +27 27 | +28 28 | __all__ = [ + 29 |+ # a comment regarding 'a': + 30 |+ "a", + 31 |+ "b", + 32 |+ "c", # a comment regarding 'c' +29 33 | "d", +30 |- "c", # a comment regarding 'c' +31 |- "b", +32 |- # a comment regarding 'a': +33 |- "a" +34 34 | ] +35 35 | +36 36 | ########################################## -RUF022.py:39:11: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:41:11: RUF022 [*] `__all__` is not alphabetically sorted | -38 | # comment0 -39 | __all__ = ("d", "a", # comment1 +40 | # comment0 +41 | __all__ = ("d", "a", # comment1 | ___________^ -40 | | # comment2 -41 | | "f", "b", -42 | | "strangely", # comment3 -43 | | # comment4 -44 | | "formatted", -45 | | # comment5 -46 | | ) # comment6 +42 | | # comment2 +43 | | "f", "b", +44 | | "strangely", # comment3 +45 | | # comment4 +46 | | "formatted", +47 | | # comment5 +48 | | ) # comment6 | |_^ RUF022 -47 | # comment7 +49 | # comment7 | = help: Sort `__all__` alphabetically ℹ Unsafe fix -36 36 | ########################################## -37 37 | -38 38 | # comment0 -39 |-__all__ = ("d", "a", # comment1 -40 |- # comment2 -41 |- "f", "b", -42 |- "strangely", # comment3 -43 |- # comment4 - 39 |+__all__ = ( - 40 |+ "a", - 41 |+ "b", - 42 |+ "d", # comment1 - 43 |+ # comment2 - 44 |+ "f", - 45 |+ # comment4 -44 46 | "formatted", - 47 |+ "strangely", # comment3 -45 48 | # comment5 -46 49 | ) # comment6 -47 50 | # comment7 +38 38 | ########################################## +39 39 | +40 40 | # comment0 +41 |-__all__ = ("d", "a", # comment1 +42 |- # comment2 +43 |- "f", "b", +44 |- "strangely", # comment3 +45 |- # comment4 + 41 |+__all__ = ( + 42 |+ "a", + 43 |+ "b", + 44 |+ "d", # comment1 + 45 |+ # comment2 + 46 |+ "f", + 47 |+ # comment4 +46 48 | "formatted", + 49 |+ "strangely", # comment3 +47 50 | # comment5 +48 51 | ) # comment6 +49 52 | # comment7 -RUF022.py:49:11: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:51:11: RUF022 [*] `__all__` is not alphabetically sorted | -47 | # comment7 -48 | -49 | __all__ = [ # comment0 +49 | # comment7 +50 | +51 | __all__ = [ # comment0 | ___________^ -50 | | # comment1 -51 | | # comment2 -52 | | "dx", "cx", "bx", "ax" # comment3 -53 | | # comment4 -54 | | # comment5 -55 | | # comment6 -56 | | ] # comment7 +52 | | # comment1 +53 | | # comment2 +54 | | "dx", "cx", "bx", "ax" # comment3 +55 | | # comment4 +56 | | # comment5 +57 | | # comment6 +58 | | ] # comment7 | |_^ RUF022 -57 | -58 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", +59 | +60 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", | = help: Sort `__all__` alphabetically ℹ Unsafe fix -48 48 | -49 49 | __all__ = [ # comment0 -50 50 | # comment1 - 51 |+ "ax", - 52 |+ "bx", - 53 |+ "cx", -51 54 | # comment2 -52 |- "dx", "cx", "bx", "ax" # comment3 - 55 |+ "dx", # comment3 -53 56 | # comment4 -54 57 | # comment5 -55 58 | # comment6 +50 50 | +51 51 | __all__ = [ # comment0 +52 52 | # comment1 + 53 |+ "ax", + 54 |+ "bx", + 55 |+ "cx", +53 56 | # comment2 +54 |- "dx", "cx", "bx", "ax" # comment3 + 57 |+ "dx", # comment3 +55 58 | # comment4 +56 59 | # comment5 +57 60 | # comment6 -RUF022.py:58:11: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:60:11: RUF022 [*] `__all__` is not alphabetically sorted | -56 | ] # comment7 -57 | -58 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", +58 | ] # comment7 +59 | +60 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", | ___________^ -59 | | "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", -60 | | "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", -61 | | "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", -62 | | "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", -63 | | "StreamReader", "StreamWriter", -64 | | "StreamReaderWriter", "StreamRecoder", -65 | | "getencoder", "getdecoder", "getincrementalencoder", -66 | | "getincrementaldecoder", "getreader", "getwriter", -67 | | "encode", "decode", "iterencode", "iterdecode", -68 | | "strict_errors", "ignore_errors", "replace_errors", -69 | | "xmlcharrefreplace_errors", -70 | | "backslashreplace_errors", "namereplace_errors", -71 | | "register_error", "lookup_error"] +61 | | "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", +62 | | "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", +63 | | "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", +64 | | "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", +65 | | "StreamReader", "StreamWriter", +66 | | "StreamReaderWriter", "StreamRecoder", +67 | | "getencoder", "getdecoder", "getincrementalencoder", +68 | | "getincrementaldecoder", "getreader", "getwriter", +69 | | "encode", "decode", "iterencode", "iterdecode", +70 | | "strict_errors", "ignore_errors", "replace_errors", +71 | | "xmlcharrefreplace_errors", +72 | | "backslashreplace_errors", "namereplace_errors", +73 | | "register_error", "lookup_error"] | |____________________________________________^ RUF022 -72 | -73 | ################################### +74 | +75 | ################################### | = help: Sort `__all__` alphabetically ℹ Safe fix -55 55 | # comment6 -56 56 | ] # comment7 -57 57 | -58 |-__all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", -59 |- "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", -60 |- "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", -61 |- "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", -62 |- "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", -63 |- "StreamReader", "StreamWriter", -64 |- "StreamReaderWriter", "StreamRecoder", -65 |- "getencoder", "getdecoder", "getincrementalencoder", -66 |- "getincrementaldecoder", "getreader", "getwriter", -67 |- "encode", "decode", "iterencode", "iterdecode", -68 |- "strict_errors", "ignore_errors", "replace_errors", -69 |- "xmlcharrefreplace_errors", -70 |- "backslashreplace_errors", "namereplace_errors", -71 |- "register_error", "lookup_error"] - 58 |+__all__ = [ - 59 |+ "BOM", - 60 |+ "BOM32_BE", - 61 |+ "BOM32_LE", - 62 |+ "BOM64_BE", - 63 |+ "BOM64_LE", - 64 |+ "BOM_BE", - 65 |+ "BOM_LE", - 66 |+ "BOM_UTF16", - 67 |+ "BOM_UTF16_BE", - 68 |+ "BOM_UTF16_LE", - 69 |+ "BOM_UTF32", - 70 |+ "BOM_UTF32_BE", - 71 |+ "BOM_UTF32_LE", - 72 |+ "BOM_UTF8", - 73 |+ "Codec", - 74 |+ "CodecInfo", - 75 |+ "EncodedFile", - 76 |+ "IncrementalDecoder", - 77 |+ "IncrementalEncoder", - 78 |+ "StreamReader", - 79 |+ "StreamReaderWriter", - 80 |+ "StreamRecoder", - 81 |+ "StreamWriter", - 82 |+ "backslashreplace_errors", - 83 |+ "decode", - 84 |+ "encode", - 85 |+ "getdecoder", - 86 |+ "getencoder", - 87 |+ "getincrementaldecoder", - 88 |+ "getincrementalencoder", - 89 |+ "getreader", - 90 |+ "getwriter", - 91 |+ "ignore_errors", - 92 |+ "iterdecode", - 93 |+ "iterencode", - 94 |+ "lookup", - 95 |+ "lookup_error", - 96 |+ "namereplace_errors", - 97 |+ "open", - 98 |+ "register", - 99 |+ "register_error", - 100 |+ "replace_errors", - 101 |+ "strict_errors", - 102 |+ "xmlcharrefreplace_errors"] -72 103 | -73 104 | ################################### -74 105 | # These should all not get flagged: +57 57 | # comment6 +58 58 | ] # comment7 +59 59 | +60 |-__all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", +61 |- "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", +62 |- "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", +63 |- "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", +64 |- "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", +65 |- "StreamReader", "StreamWriter", +66 |- "StreamReaderWriter", "StreamRecoder", +67 |- "getencoder", "getdecoder", "getincrementalencoder", +68 |- "getincrementaldecoder", "getreader", "getwriter", +69 |- "encode", "decode", "iterencode", "iterdecode", +70 |- "strict_errors", "ignore_errors", "replace_errors", +71 |- "xmlcharrefreplace_errors", +72 |- "backslashreplace_errors", "namereplace_errors", +73 |- "register_error", "lookup_error"] + 60 |+__all__ = [ + 61 |+ "BOM", + 62 |+ "BOM32_BE", + 63 |+ "BOM32_LE", + 64 |+ "BOM64_BE", + 65 |+ "BOM64_LE", + 66 |+ "BOM_BE", + 67 |+ "BOM_LE", + 68 |+ "BOM_UTF16", + 69 |+ "BOM_UTF16_BE", + 70 |+ "BOM_UTF16_LE", + 71 |+ "BOM_UTF32", + 72 |+ "BOM_UTF32_BE", + 73 |+ "BOM_UTF32_LE", + 74 |+ "BOM_UTF8", + 75 |+ "Codec", + 76 |+ "CodecInfo", + 77 |+ "EncodedFile", + 78 |+ "IncrementalDecoder", + 79 |+ "IncrementalEncoder", + 80 |+ "StreamReader", + 81 |+ "StreamReaderWriter", + 82 |+ "StreamRecoder", + 83 |+ "StreamWriter", + 84 |+ "backslashreplace_errors", + 85 |+ "decode", + 86 |+ "encode", + 87 |+ "getdecoder", + 88 |+ "getencoder", + 89 |+ "getincrementaldecoder", + 90 |+ "getincrementalencoder", + 91 |+ "getreader", + 92 |+ "getwriter", + 93 |+ "ignore_errors", + 94 |+ "iterdecode", + 95 |+ "iterencode", + 96 |+ "lookup", + 97 |+ "lookup_error", + 98 |+ "namereplace_errors", + 99 |+ "open", + 100 |+ "register", + 101 |+ "register_error", + 102 |+ "replace_errors", + 103 |+ "strict_errors", + 104 |+ "xmlcharrefreplace_errors"] +74 105 | +75 106 | ################################### +76 107 | # These should all not get flagged: From 454e7301a15e4b3c05bb584338e711ec6e8d05dc Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Thu, 11 Jan 2024 21:42:18 +0000 Subject: [PATCH 11/77] Attempt to clarify invariants relied upon in `construct_sorted_all()` --- .../src/rules/ruff/rules/sort_dunder_all.rs | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 805fdca88ec95..cfa9c4db6a2ef 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -223,9 +223,17 @@ impl<'a> DunderAllValue<'a> { ) -> SortedDunderAll { let mut sorted_items = self.items.clone(); sorted_items.sort(); + + // As well as saving us unnecessary work, + // returning early here also means that we can rely on the invariant + // throughout the rest of this function that both `items` and `sorted_items` + // have length of at least two. If there are fewer than two items in `__all__`, + // it is impossible for them *not* to compare equal here: if sorted_items == self.items { return SortedDunderAll::AlreadySorted; } + assert!(self.items.len() >= 2); + // As well as the "items" in the `__all__` definition, // there is also a "prelude" and a "postlude": // - Prelude == the region of source code from the opening parenthesis @@ -234,8 +242,12 @@ impl<'a> DunderAllValue<'a> { // element in `__all__` up to and including the closing parenthesis // (if there was one). let prelude_end = { - // Should be safe: we should already have returned by now if there are 0 items - let first_item = &self.items[0]; + // We should already have returned by now if there are 0 items: + // see earlier comments in this function + let first_item = self + .items + .first() + .expect("Expected there to be at least two items in the list"); let first_item_line_offset = locator.line_start(first_item.start()); if first_item_line_offset == locator.line_start(self.start()) { first_item.start() @@ -244,8 +256,12 @@ impl<'a> DunderAllValue<'a> { } }; let (needs_trailing_comma, postlude_start) = { - // Should be safe: we should already have returned by now if there are 0 items - let last_item = &self.items[self.items.len() - 1]; + // We should already have returned by now if there are 0 items: + // see earlier comments in this function + let last_item = self + .items + .last() + .expect("Expected there to be at least two items in the list"); let last_item_line_offset = locator.line_end(last_item.end()); if last_item_line_offset == locator.line_end(self.end()) { (false, last_item.end()) @@ -394,8 +410,7 @@ fn collect_dunder_all_items(lines: &[DunderAllLine]) -> Vec { } } _ => unreachable!( - "This should be unreachable. - Any lines that have neither comments nor items + "Any lines that have neither comments nor items should have been filtered out by this point." ), } From e1c1f358e344232db6f30f6eb1c8517bc43538c8 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Thu, 11 Jan 2024 22:21:37 +0000 Subject: [PATCH 12/77] misc Charlie remarks --- .../src/rules/ruff/rules/sort_dunder_all.rs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index cfa9c4db6a2ef..953e50272833a 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -165,14 +165,14 @@ fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr, parent checker.diagnostics.push(diagnostic); } -struct DunderAllValue<'a> { +struct DunderAllValue { items: Vec, - range: &'a TextRange, + range: TextRange, multiline: bool, } -impl<'a> DunderAllValue<'a> { - fn from_expr(value: &'a ast::Expr, locator: &Locator) -> Option> { +impl DunderAllValue { + fn from_expr(value: &ast::Expr, locator: &Locator) -> Option { // Step (1): inspect the AST to check that we're looking at something vaguely sane: let is_multiline = locator.contains_line_break(value.range()); let (elts, range) = match value { @@ -210,7 +210,7 @@ impl<'a> DunderAllValue<'a> { Some(DunderAllValue { items, - range, + range: *range, multiline: is_multiline, }) } @@ -295,9 +295,9 @@ impl<'a> DunderAllValue<'a> { } } -impl Ranged for DunderAllValue<'_> { +impl Ranged for DunderAllValue { fn range(&self) -> TextRange { - *self.range + self.range } } @@ -381,8 +381,7 @@ fn collect_dunder_all_items(lines: &[DunderAllLine]) -> Vec { let mut all_items = vec![]; let mut this_range = None; let mut idx = 0; - for line in lines { - let DunderAllLine { items, comment } = line; + for DunderAllLine { items, comment } in lines { match (items.as_slice(), comment) { ([], Some(_)) => { this_range = *comment; @@ -475,9 +474,7 @@ fn join_multiline_dunder_all_items( newline: &str, needs_trailing_comma: bool, ) -> Option { - let Some(indent) = indentation(locator, parent) else { - return None; - }; + let indent = indentation(locator, parent)?; let mut new_dunder_all = String::new(); for (i, item) in sorted_items.iter().enumerate() { new_dunder_all.push_str(indent); From 4e405fb819d0c241d45109dfd8cfc40642f4eebf Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Thu, 11 Jan 2024 22:24:48 +0000 Subject: [PATCH 13/77] remove unnecessary assignment --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 953e50272833a..ab745e2fe6fd5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -143,8 +143,7 @@ fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr, parent SortedDunderAll::Sorted(value) => value, }; - let dunder_all_range = dunder_all_val.range(); - let mut diagnostic = Diagnostic::new(UnsortedDunderAll, dunder_all_range); + let mut diagnostic = Diagnostic::new(UnsortedDunderAll, dunder_all_val.range()); if let Some(new_dunder_all) = new_dunder_all { let applicability = { @@ -157,7 +156,7 @@ fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr, parent } }; diagnostic.set_fix(Fix::applicable_edit( - Edit::range_replacement(new_dunder_all, dunder_all_range), + Edit::range_replacement(new_dunder_all, dunder_all_val.range()), applicability, )); } From 26a2848b6cd014cfc842d7d1b26ba1966614161c Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 08:08:18 +0000 Subject: [PATCH 14/77] More misc Charlie comments --- .../src/checkers/ast/analyze/statement.rs | 6 +- .../src/rules/ruff/rules/sort_dunder_all.rs | 107 ++++++++++++------ 2 files changed, 75 insertions(+), 38 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 4290fab5041f8..7dc03815ecb43 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -1065,14 +1065,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { pylint::rules::misplaced_bare_raise(checker, raise); } } - Stmt::AugAssign(augassign @ ast::StmtAugAssign { target, .. }) => { + Stmt::AugAssign(aug_assign @ ast::StmtAugAssign { target, .. }) => { if checker.enabled(Rule::GlobalStatement) { if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { pylint::rules::global_statement(checker, id); } } if checker.enabled(Rule::UnsortedDunderAll) { - ruff::rules::sort_dunder_all_augassign(checker, augassign, stmt); + ruff::rules::sort_dunder_all_aug_assign(checker, aug_assign, stmt); } } Stmt::If( @@ -1534,7 +1534,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { pyupgrade::rules::non_pep695_type_alias(checker, assign_stmt); } if checker.settings.rules.enabled(Rule::UnsortedDunderAll) { - ruff::rules::sort_dunder_all_annassign(checker, assign_stmt, stmt); + ruff::rules::sort_dunder_all_ann_assign(checker, assign_stmt, stmt); } if checker.source_type.is_stub() { if let Some(value) = value { diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index ab745e2fe6fd5..5f6796ac761e8 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -7,7 +7,7 @@ use ruff_python_ast::whitespace::indentation; use ruff_python_codegen::Stylist; use ruff_python_parser::{lexer, Mode, Tok}; use ruff_source_file::Locator; -use ruff_text_size::{Ranged, TextRange, TextSize}; +use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -82,7 +82,7 @@ pub(crate) fn sort_dunder_all_assign( sort_dunder_all(checker, id, value, parent); } -pub(crate) fn sort_dunder_all_augassign( +pub(crate) fn sort_dunder_all_aug_assign( checker: &mut Checker, node: &ast::StmtAugAssign, parent: &ast::Stmt, @@ -102,7 +102,7 @@ pub(crate) fn sort_dunder_all_augassign( sort_dunder_all(checker, id, value, parent); } -pub(crate) fn sort_dunder_all_annassign( +pub(crate) fn sort_dunder_all_ann_assign( checker: &mut Checker, node: &ast::StmtAnnAssign, parent: &ast::Stmt, @@ -324,24 +324,37 @@ fn collect_dunder_all_lines(range: TextRange, locator: &Locator) -> Option { - if !(items_in_line.is_empty() && comment_in_line.is_none()) { - lines.push(DunderAllLine::new( + if items_in_line.is_empty() { + if let Some(comment) = comment_in_line { + lines.push(DunderAllLine::JustAComment(LineWithJustAComment::new( + comment, range, + ))); + } + } else { + lines.push(DunderAllLine::OneOrMoreItems(LineWithItems::new( &items_in_line, comment_in_line, - range.start(), - )); + range, + ))); } break; } Tok::NonLogicalNewline => { - if !(items_in_line.is_empty() && comment_in_line.is_none()) { - lines.push(DunderAllLine::new( + if items_in_line.is_empty() { + if let Some(comment) = comment_in_line { + lines.push(DunderAllLine::JustAComment(LineWithJustAComment::new( + comment, range, + ))); + comment_in_line = None; + } + } else { + lines.push(DunderAllLine::OneOrMoreItems(LineWithItems::new( &items_in_line, comment_in_line, - range.start(), - )); - items_in_line.clear(); + range, + ))); comment_in_line = None; + items_in_line.clear(); } } Tok::Comment(_) => comment_in_line = Some(subrange), @@ -354,24 +367,47 @@ fn collect_dunder_all_lines(range: TextRange, locator: &Locator) -> Option Self { + Self(comment_range + total_dunder_all_range.start()) + } +} + +#[derive(Debug)] +struct LineWithItems { items: Vec<(String, TextRange)>, - comment: Option, + comment_range: Option, } -impl DunderAllLine { - fn new(items: &[(String, TextRange)], comment: Option, offset: TextSize) -> Self { - assert!(comment.is_some() || !items.is_empty()); +impl LineWithItems { + fn new( + items: &[(String, TextRange)], + comment_range: Option, + total_dunder_all_range: TextRange, + ) -> Self { + assert!( + !items.is_empty(), + "Use the 'JustAComment' variant to represent lines with 0 items" + ); + let offset = total_dunder_all_range.start(); Self { items: items .iter() .map(|(s, r)| (s.to_owned(), r + offset)) .collect(), - comment: comment.map(|c| c + offset), + comment_range: comment_range.map(|c| c + offset), } } } +#[derive(Debug)] +enum DunderAllLine { + JustAComment(LineWithJustAComment), + OneOrMoreItems(LineWithItems), +} + fn collect_dunder_all_items(lines: &[DunderAllLine]) -> Vec { // Given data on each line in `__all__`, group lines together into "items". // Each item contains exactly one element, @@ -379,38 +415,39 @@ fn collect_dunder_all_items(lines: &[DunderAllLine]) -> Vec { // that must move with the element when `__all__` is sorted. let mut all_items = vec![]; let mut this_range = None; - let mut idx = 0; - for DunderAllLine { items, comment } in lines { - match (items.as_slice(), comment) { - ([], Some(_)) => { - this_range = *comment; + for line in lines { + match line { + DunderAllLine::JustAComment(LineWithJustAComment(comment_range)) => { + this_range = Some(*comment_range); } - ([(first_val, first_range), rest @ ..], _) => { + DunderAllLine::OneOrMoreItems(LineWithItems { + items, + comment_range, + }) => { + let [(first_val, first_range), rest @ ..] = items.as_slice() else { + unreachable!( + "LineWithItems::new() should uphold the invariant that this list is always non-empty" + ) + }; let range = this_range.map_or(*first_range, |r| { TextRange::new(r.start(), first_range.end()) }); all_items.push(DunderAllItem { value: first_val.clone(), - original_index: idx, + original_index: all_items.len(), range, - additional_comments: *comment, + additional_comments: *comment_range, }); this_range = None; - idx += 1; for (value, range) in rest { all_items.push(DunderAllItem { value: value.clone(), - original_index: idx, + original_index: all_items.len(), range: *range, additional_comments: None, }); - idx += 1; } } - _ => unreachable!( - "Any lines that have neither comments nor items - should have been filtered out by this point." - ), } } all_items @@ -420,7 +457,7 @@ fn collect_dunder_all_items(lines: &[DunderAllLine]) -> Vec { struct DunderAllItem { value: String, // Each `AllItem` in any given list should have a unique `original_index`: - original_index: u16, + original_index: usize, // Note that this range might include comments, etc. range: TextRange, additional_comments: Option, @@ -441,7 +478,7 @@ impl PartialEq for DunderAllItem { impl Eq for DunderAllItem {} impl DunderAllItem { - fn sort_index(&self) -> (&str, u16) { + fn sort_index(&self) -> (&str, usize) { (&self.value, self.original_index) } } From b0c5d667709f69053b72956ecbcc6dc739b8770c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 12 Jan 2024 10:17:48 +0000 Subject: [PATCH 15/77] Update crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs Co-authored-by: Micha Reiser --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 5f6796ac761e8..a1cf5fd3b041a 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -179,11 +179,13 @@ impl DunderAllValue { ast::Expr::Tuple(ast::ExprTuple { elts, range, .. }) => (elts, range), _ => return None, }; + // An `__all__` definition with <2 elements can't be unsorted; // no point in proceeding any further here if elts.len() < 2 { return None; } + for elt in elts { // Only consider sorting it if __all__ only has strings in it let string_literal = elt.as_string_literal_expr()?; @@ -192,6 +194,7 @@ impl DunderAllValue { return None; } } + // Step (2): parse the `__all__` definition using the raw tokens. // // (2a). Start by collecting information on each line individually: From e35f0886c61c915ef5f409c5be538a7bb0ef7116 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 12 Jan 2024 10:18:06 +0000 Subject: [PATCH 16/77] Update crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs Co-authored-by: Micha Reiser --- .../src/rules/ruff/rules/sort_dunder_all.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index a1cf5fd3b041a..cf0811c2d518b 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -442,14 +442,12 @@ fn collect_dunder_all_items(lines: &[DunderAllLine]) -> Vec { additional_comments: *comment_range, }); this_range = None; - for (value, range) in rest { - all_items.push(DunderAllItem { - value: value.clone(), - original_index: all_items.len(), - range: *range, - additional_comments: None, - }); - } + all_items.extend(rest.map(|(value, range)| DunderAllItem { + value, + original_index: all_items.len(), + range, + additional_comments: None, + })); } } } From 8d4a9f6668d26d1b35ae4e21815cb1ccaeb1be5a Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 12 Jan 2024 10:18:39 +0000 Subject: [PATCH 17/77] Update crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs Co-authored-by: Micha Reiser --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index cf0811c2d518b..1b172c64eca74 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -180,7 +180,7 @@ impl DunderAllValue { _ => return None, }; - // An `__all__` definition with <2 elements can't be unsorted; + // An `__all__` definition with < 2 elements can't be unsorted; // no point in proceeding any further here if elts.len() < 2 { return None; From 47ff9b20c95a0be813dc3b4e63aaeba5a1087b0c Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 10:28:35 +0000 Subject: [PATCH 18/77] Revert "Update crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs" This reverts commit e35f0886c61c915ef5f409c5be538a7bb0ef7116. --- .../src/rules/ruff/rules/sort_dunder_all.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 1b172c64eca74..9c261c6345984 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -442,12 +442,14 @@ fn collect_dunder_all_items(lines: &[DunderAllLine]) -> Vec { additional_comments: *comment_range, }); this_range = None; - all_items.extend(rest.map(|(value, range)| DunderAllItem { - value, - original_index: all_items.len(), - range, - additional_comments: None, - })); + for (value, range) in rest { + all_items.push(DunderAllItem { + value: value.clone(), + original_index: all_items.len(), + range: *range, + additional_comments: None, + }); + } } } } From 047e274e10f1bad1acc5f5e3876fc073349b1d5c Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 10:49:04 +0000 Subject: [PATCH 19/77] Get rid of one use of `.clone()` --- .../src/rules/ruff/rules/sort_dunder_all.rs | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 9c261c6345984..4d6775de7f00f 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -72,14 +72,13 @@ impl Violation for UnsortedDunderAll { pub(crate) fn sort_dunder_all_assign( checker: &mut Checker, - node: &ast::StmtAssign, + ast::StmtAssign {value, targets, ..}: &ast::StmtAssign, parent: &ast::Stmt, ) { - let ast::StmtAssign { value, targets, .. } = node; let [ast::Expr::Name(ast::ExprName { id, .. })] = targets.as_slice() else { return; }; - sort_dunder_all(checker, id, value, parent); + sort_dunder_all(checker, id, &value, parent); } pub(crate) fn sort_dunder_all_aug_assign( @@ -133,7 +132,7 @@ fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr, parent let locator = checker.locator(); - let Some(dunder_all_val) = DunderAllValue::from_expr(node, locator) else { + let Some(mut dunder_all_val) = DunderAllValue::from_expr(node, locator) else { return; }; @@ -179,7 +178,7 @@ impl DunderAllValue { ast::Expr::Tuple(ast::ExprTuple { elts, range, .. }) => (elts, range), _ => return None, }; - + // An `__all__` definition with < 2 elements can't be unsorted; // no point in proceeding any further here if elts.len() < 2 { @@ -194,7 +193,7 @@ impl DunderAllValue { return None; } } - + // Step (2): parse the `__all__` definition using the raw tokens. // // (2a). Start by collecting information on each line individually: @@ -217,21 +216,29 @@ impl DunderAllValue { }) } + /// Implementation of the unstable [`&[T].is_sorted`] function. + /// See https://github.com/rust-lang/rust/issues/53485 + fn is_already_sorted(&self) -> bool { + for (this, next) in self.items.iter().tuple_windows() { + if next < this { + return false; + } + } + true + } + fn construct_sorted_all( - &self, + &mut self, locator: &Locator, parent: &ast::Stmt, stylist: &Stylist, ) -> SortedDunderAll { - let mut sorted_items = self.items.clone(); - sorted_items.sort(); - // As well as saving us unnecessary work, // returning early here also means that we can rely on the invariant // throughout the rest of this function that both `items` and `sorted_items` // have length of at least two. If there are fewer than two items in `__all__`, // it is impossible for them *not* to compare equal here: - if sorted_items == self.items { + if self.is_already_sorted() { return SortedDunderAll::AlreadySorted; } assert!(self.items.len() >= 2); @@ -276,6 +283,9 @@ impl DunderAllValue { .to_string(); let postlude = locator.slice(TextRange::new(postlude_start, self.end())); + let sorted_items = &mut self.items; + sorted_items.sort(); + let joined_items = if self.multiline { let indentation = stylist.indentation(); let newline = stylist.line_ending().as_str(); From 9904db430eb778080c3128958978a17c0d43baaa Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 11:04:58 +0000 Subject: [PATCH 20/77] Remove all uses of `.clone()` --- .../src/rules/ruff/rules/sort_dunder_all.rs | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 4d6775de7f00f..f9ba0c6aa2767 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -72,7 +72,7 @@ impl Violation for UnsortedDunderAll { pub(crate) fn sort_dunder_all_assign( checker: &mut Checker, - ast::StmtAssign {value, targets, ..}: &ast::StmtAssign, + ast::StmtAssign { value, targets, .. }: &ast::StmtAssign, parent: &ast::Stmt, ) { let [ast::Expr::Name(ast::ExprName { id, .. })] = targets.as_slice() else { @@ -207,7 +207,7 @@ impl DunderAllValue { // the comments above the element move with it. // - The same goes for any comments on the same line as an element: // if the element moves, the comment moves with it. - let items = collect_dunder_all_items(&lines); + let items = collect_dunder_all_items(lines); Some(DunderAllValue { items, @@ -421,7 +421,7 @@ enum DunderAllLine { OneOrMoreItems(LineWithItems), } -fn collect_dunder_all_items(lines: &[DunderAllLine]) -> Vec { +fn collect_dunder_all_items(lines: Vec) -> Vec { // Given data on each line in `__all__`, group lines together into "items". // Each item contains exactly one element, // but might contain multiple comments attached to that element @@ -431,32 +431,31 @@ fn collect_dunder_all_items(lines: &[DunderAllLine]) -> Vec { for line in lines { match line { DunderAllLine::JustAComment(LineWithJustAComment(comment_range)) => { - this_range = Some(*comment_range); + this_range = Some(comment_range); } DunderAllLine::OneOrMoreItems(LineWithItems { items, comment_range, }) => { - let [(first_val, first_range), rest @ ..] = items.as_slice() else { - unreachable!( - "LineWithItems::new() should uphold the invariant that this list is always non-empty" - ) - }; - let range = this_range.map_or(*first_range, |r| { + let mut owned_items = items.into_iter(); + let (first_val, first_range) = owned_items + .next() + .expect("LineWithItems::new() should uphold the invariant that this list is always non-empty"); + let range = this_range.map_or(first_range, |r| { TextRange::new(r.start(), first_range.end()) }); all_items.push(DunderAllItem { - value: first_val.clone(), + value: first_val, original_index: all_items.len(), range, - additional_comments: *comment_range, + additional_comments: comment_range, }); this_range = None; - for (value, range) in rest { + for (value, range) in owned_items { all_items.push(DunderAllItem { - value: value.clone(), + value: value, original_index: all_items.len(), - range: *range, + range: range, additional_comments: None, }); } From d86ca1d31ec0dc12613443adc74cccd0f31623be Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 11:35:44 +0000 Subject: [PATCH 21/77] Use absolute ranges everywhere; get rid of more cloning --- .../src/rules/ruff/rules/sort_dunder_all.rs | 54 +++++++------------ 1 file changed, 19 insertions(+), 35 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index f9ba0c6aa2767..7b4e00249e86b 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -78,7 +78,7 @@ pub(crate) fn sort_dunder_all_assign( let [ast::Expr::Name(ast::ExprName { id, .. })] = targets.as_slice() else { return; }; - sort_dunder_all(checker, id, &value, parent); + sort_dunder_all(checker, id, value, parent); } pub(crate) fn sort_dunder_all_aug_assign( @@ -217,8 +217,10 @@ impl DunderAllValue { } /// Implementation of the unstable [`&[T].is_sorted`] function. - /// See https://github.com/rust-lang/rust/issues/53485 + /// See fn is_already_sorted(&self) -> bool { + // tuple_windows() clones, + // but here that's okay: we're only cloning *references*, rather than the items themselves for (this, next) in self.items.iter().tuple_windows() { if next < this { return false; @@ -291,7 +293,7 @@ impl DunderAllValue { let newline = stylist.line_ending().as_str(); prelude = format!("{}{}", prelude.trim_end(), newline); join_multiline_dunder_all_items( - &sorted_items, + sorted_items, locator, parent, indentation, @@ -299,7 +301,7 @@ impl DunderAllValue { needs_trailing_comma, ) } else { - Some(join_singleline_dunder_all_items(&sorted_items, locator)) + Some(join_singleline_dunder_all_items(sorted_items, locator)) }; let new_dunder_all = joined_items.map(|items| format!("{prelude}{items}{postlude}")); @@ -327,7 +329,10 @@ fn collect_dunder_all_lines(range: TextRange, locator: &Locator) -> Option { @@ -339,15 +344,12 @@ fn collect_dunder_all_lines(range: TextRange, locator: &Locator) -> Option { if items_in_line.is_empty() { if let Some(comment) = comment_in_line { - lines.push(DunderAllLine::JustAComment(LineWithJustAComment::new( - comment, range, - ))); + lines.push(DunderAllLine::JustAComment(LineWithJustAComment(comment))); } } else { lines.push(DunderAllLine::OneOrMoreItems(LineWithItems::new( - &items_in_line, + items_in_line, comment_in_line, - range, ))); } break; @@ -355,19 +357,15 @@ fn collect_dunder_all_lines(range: TextRange, locator: &Locator) -> Option { if items_in_line.is_empty() { if let Some(comment) = comment_in_line { - lines.push(DunderAllLine::JustAComment(LineWithJustAComment::new( - comment, range, - ))); + lines.push(DunderAllLine::JustAComment(LineWithJustAComment(comment))); comment_in_line = None; } } else { lines.push(DunderAllLine::OneOrMoreItems(LineWithItems::new( - &items_in_line, + std::mem::take(&mut items_in_line), comment_in_line, - range, ))); comment_in_line = None; - items_in_line.clear(); } } Tok::Comment(_) => comment_in_line = Some(subrange), @@ -382,12 +380,6 @@ fn collect_dunder_all_lines(range: TextRange, locator: &Locator) -> Option Self { - Self(comment_range + total_dunder_all_range.start()) - } -} - #[derive(Debug)] struct LineWithItems { items: Vec<(String, TextRange)>, @@ -395,22 +387,14 @@ struct LineWithItems { } impl LineWithItems { - fn new( - items: &[(String, TextRange)], - comment_range: Option, - total_dunder_all_range: TextRange, - ) -> Self { + fn new(items: Vec<(String, TextRange)>, comment_range: Option) -> Self { assert!( !items.is_empty(), "Use the 'JustAComment' variant to represent lines with 0 items" ); - let offset = total_dunder_all_range.start(); Self { - items: items - .iter() - .map(|(s, r)| (s.to_owned(), r + offset)) - .collect(), - comment_range: comment_range.map(|c| c + offset), + items, + comment_range, } } } @@ -453,9 +437,9 @@ fn collect_dunder_all_items(lines: Vec) -> Vec { this_range = None; for (value, range) in owned_items { all_items.push(DunderAllItem { - value: value, + value, original_index: all_items.len(), - range: range, + range, additional_comments: None, }); } From f02c635b0b5841613fb0bf03500bd01a7f6a21fb Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 12 Jan 2024 11:42:20 +0000 Subject: [PATCH 22/77] Update crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs Co-authored-by: Micha Reiser --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 7b4e00249e86b..367e06b610eee 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -410,7 +410,10 @@ fn collect_dunder_all_items(lines: Vec) -> Vec { // Each item contains exactly one element, // but might contain multiple comments attached to that element // that must move with the element when `__all__` is sorted. - let mut all_items = vec![]; + let mut all_items = Vec::with_capacity(match lines.as_slice() { + [DunderAllLine::OneOrMoreItems(single)] => single.items.len(), + _ => lines.len(), + }); let mut this_range = None; for line in lines { match line { From 00121c749197cbcf4ce1ee45a5543006c6c5d392 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 12:01:09 +0000 Subject: [PATCH 23/77] Rename `construct_sorted_all()`; make it consume self rather than take a reference --- .../src/rules/ruff/rules/sort_dunder_all.rs | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 367e06b610eee..00659c1bb96e5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -132,30 +132,33 @@ fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr, parent let locator = checker.locator(); - let Some(mut dunder_all_val) = DunderAllValue::from_expr(node, locator) else { + let Some( + dunder_all_val @ DunderAllValue { + range, multiline, .. + }, + ) = DunderAllValue::from_expr(node, locator) + else { return; }; let new_dunder_all = - match dunder_all_val.construct_sorted_all(locator, parent, checker.stylist()) { + match dunder_all_val.into_sorted_source_code(locator, parent, checker.stylist()) { SortedDunderAll::AlreadySorted => return, SortedDunderAll::Sorted(value) => value, }; - let mut diagnostic = Diagnostic::new(UnsortedDunderAll, dunder_all_val.range()); + let mut diagnostic = Diagnostic::new(UnsortedDunderAll, range); if let Some(new_dunder_all) = new_dunder_all { let applicability = { - if dunder_all_val.multiline - && checker.indexer().comment_ranges().intersects(node.range()) - { + if multiline && checker.indexer().comment_ranges().intersects(node.range()) { Applicability::Unsafe } else { Applicability::Safe } }; diagnostic.set_fix(Fix::applicable_edit( - Edit::range_replacement(new_dunder_all, dunder_all_val.range()), + Edit::range_replacement(new_dunder_all, range), applicability, )); } @@ -229,8 +232,8 @@ impl DunderAllValue { true } - fn construct_sorted_all( - &mut self, + fn into_sorted_source_code( + self, locator: &Locator, parent: &ast::Stmt, stylist: &Stylist, @@ -285,7 +288,7 @@ impl DunderAllValue { .to_string(); let postlude = locator.slice(TextRange::new(postlude_start, self.end())); - let sorted_items = &mut self.items; + let mut sorted_items = self.items; sorted_items.sort(); let joined_items = if self.multiline { @@ -293,7 +296,7 @@ impl DunderAllValue { let newline = stylist.line_ending().as_str(); prelude = format!("{}{}", prelude.trim_end(), newline); join_multiline_dunder_all_items( - sorted_items, + &sorted_items, locator, parent, indentation, @@ -301,7 +304,7 @@ impl DunderAllValue { needs_trailing_comma, ) } else { - Some(join_singleline_dunder_all_items(sorted_items, locator)) + Some(join_singleline_dunder_all_items(&sorted_items, locator)) }; let new_dunder_all = joined_items.map(|items| format!("{prelude}{items}{postlude}")); From 61c57b0053ab93169a83b863b71e58c8767eb8f9 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 12:12:56 +0000 Subject: [PATCH 24/77] suggestion by micha to use one `panic` instead of two `.expect()`s --- .../src/rules/ruff/rules/sort_dunder_all.rs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 00659c1bb96e5..9944cb9df5a1e 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -246,7 +246,9 @@ impl DunderAllValue { if self.is_already_sorted() { return SortedDunderAll::AlreadySorted; } - assert!(self.items.len() >= 2); + let [first_item, .., last_item] = self.items.as_slice() else { + panic!("Expected to have already returned if the list had < 2 items") + }; // As well as the "items" in the `__all__` definition, // there is also a "prelude" and a "postlude": @@ -256,12 +258,6 @@ impl DunderAllValue { // element in `__all__` up to and including the closing parenthesis // (if there was one). let prelude_end = { - // We should already have returned by now if there are 0 items: - // see earlier comments in this function - let first_item = self - .items - .first() - .expect("Expected there to be at least two items in the list"); let first_item_line_offset = locator.line_start(first_item.start()); if first_item_line_offset == locator.line_start(self.start()) { first_item.start() @@ -270,12 +266,6 @@ impl DunderAllValue { } }; let (needs_trailing_comma, postlude_start) = { - // We should already have returned by now if there are 0 items: - // see earlier comments in this function - let last_item = self - .items - .last() - .expect("Expected there to be at least two items in the list"); let last_item_line_offset = locator.line_end(last_item.end()); if last_item_line_offset == locator.line_end(self.end()) { (false, last_item.end()) From fd1dc54c80004d1b2377ca93ad30718d83e77587 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 13:44:55 +0000 Subject: [PATCH 25/77] fix bug relating to multiline comments --- .../resources/test/fixtures/ruff/RUF022.py | 9 ++++ .../src/rules/ruff/rules/sort_dunder_all.rs | 21 ++++++-- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 51 ++++++++++++++++--- 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index dcb978554424c..1345f644352cf 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -72,6 +72,15 @@ "backslashreplace_errors", "namereplace_errors", "register_error", "lookup_error"] +__all__: tuple[str, ...] = ( # a comment about the opening paren + # multiline comment about "bbb" part 1 + # multiline comment about "bbb" part 2 + "bbb", + # multiline comment about "aaa" part 1 + # multiline comment about "aaa" part 2 + "aaa", +) + ################################### # These should all not get flagged: ################################### diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 9944cb9df5a1e..2a3290069704c 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -210,7 +210,7 @@ impl DunderAllValue { // the comments above the element move with it. // - The same goes for any comments on the same line as an element: // if the element moves, the comment moves with it. - let items = collect_dunder_all_items(lines); + let items = collect_dunder_all_items(lines, *range, locator); Some(DunderAllValue { items, @@ -398,7 +398,11 @@ enum DunderAllLine { OneOrMoreItems(LineWithItems), } -fn collect_dunder_all_items(lines: Vec) -> Vec { +fn collect_dunder_all_items( + lines: Vec, + dunder_all_range: TextRange, + locator: &Locator, +) -> Vec { // Given data on each line in `__all__`, group lines together into "items". // Each item contains exactly one element, // but might contain multiple comments attached to that element @@ -407,16 +411,25 @@ fn collect_dunder_all_items(lines: Vec) -> Vec { [DunderAllLine::OneOrMoreItems(single)] => single.items.len(), _ => lines.len(), }); - let mut this_range = None; + let mut first_item_encountered = false; + let mut this_range: Option = None; for line in lines { match line { DunderAllLine::JustAComment(LineWithJustAComment(comment_range)) => { - this_range = Some(comment_range); + if first_item_encountered + || locator.line_start(comment_range.start()) + != locator.line_start(dunder_all_range.start()) + { + this_range = Some(this_range.map_or(comment_range, |range| { + TextRange::new(range.start(), comment_range.end()) + })); + } } DunderAllLine::OneOrMoreItems(LineWithItems { items, comment_range, }) => { + first_item_encountered = true; let mut owned_items = items.into_iter(); let (first_val, first_range) = owned_items .next() diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index 4df6920a74dfa..682f7d43fa658 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -253,12 +253,13 @@ RUF022.py:51:11: RUF022 [*] `__all__` is not alphabetically sorted = help: Sort `__all__` alphabetically ℹ Unsafe fix +49 49 | # comment7 50 50 | 51 51 | __all__ = [ # comment0 -52 52 | # comment1 - 53 |+ "ax", - 54 |+ "bx", - 55 |+ "cx", + 52 |+ "ax", + 53 |+ "bx", + 54 |+ "cx", +52 55 | # comment1 53 56 | # comment2 54 |- "dx", "cx", "bx", "ax" # comment3 57 |+ "dx", # comment3 @@ -287,7 +288,7 @@ RUF022.py:60:11: RUF022 [*] `__all__` is not alphabetically sorted 73 | | "register_error", "lookup_error"] | |____________________________________________^ RUF022 74 | -75 | ################################### +75 | __all__: tuple[str, ...] = ( # a comment about the opening paren | = help: Sort `__all__` alphabetically @@ -355,7 +356,43 @@ RUF022.py:60:11: RUF022 [*] `__all__` is not alphabetically sorted 103 |+ "strict_errors", 104 |+ "xmlcharrefreplace_errors"] 74 105 | -75 106 | ################################### -76 107 | # These should all not get flagged: +75 106 | __all__: tuple[str, ...] = ( # a comment about the opening paren +76 107 | # multiline comment about "bbb" part 1 + +RUF022.py:75:28: RUF022 [*] `__all__` is not alphabetically sorted + | +73 | "register_error", "lookup_error"] +74 | +75 | __all__: tuple[str, ...] = ( # a comment about the opening paren + | ____________________________^ +76 | | # multiline comment about "bbb" part 1 +77 | | # multiline comment about "bbb" part 2 +78 | | "bbb", +79 | | # multiline comment about "aaa" part 1 +80 | | # multiline comment about "aaa" part 2 +81 | | "aaa", +82 | | ) + | |_^ RUF022 +83 | +84 | ################################### + | + = help: Sort `__all__` alphabetically + +ℹ Unsafe fix +73 73 | "register_error", "lookup_error"] +74 74 | +75 75 | __all__: tuple[str, ...] = ( # a comment about the opening paren + 76 |+ # multiline comment about "aaa" part 1 + 77 |+ # multiline comment about "aaa" part 2 + 78 |+ "aaa", +76 79 | # multiline comment about "bbb" part 1 +77 80 | # multiline comment about "bbb" part 2 +78 81 | "bbb", +79 |- # multiline comment about "aaa" part 1 +80 |- # multiline comment about "aaa" part 2 +81 |- "aaa", +82 82 | ) +83 83 | +84 84 | ################################### From 5e129ade8e77090a2aa1017611258b2c2f65d942 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 13:53:27 +0000 Subject: [PATCH 26/77] Much bigger comment explaining `prelude` and `postlude` --- .../src/rules/ruff/rules/sort_dunder_all.rs | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 2a3290069704c..003ca2796ea98 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -253,10 +253,39 @@ impl DunderAllValue { // As well as the "items" in the `__all__` definition, // there is also a "prelude" and a "postlude": // - Prelude == the region of source code from the opening parenthesis - // (if there was one), up to the start of the first element in `__all__`. + // (if there was one), up to the start of the first item in `__all__`. // - Postlude == the region of source code from the end of the last - // element in `__all__` up to and including the closing parenthesis + // item in `__all__` up to and including the closing parenthesis // (if there was one). + // + // For example: + // + // ```python + // __all__ = [ # comment0 + // # comment1 + // "first item", + // "last item" # comment2 + // # comment3 + // ] # comment4 + // <-- Tokenizer emits a LogicalNewline here + // ``` + // + // - The prelude in the above example is the source code region + // starting at the opening `[` and ending just before `# comment1`. + // `comment0` here counts as part of the prelude because it is on + // the same line as the opening paren, and because we haven't encountered + // any elements of `__all__` yet, but `comment1` counts as part of the first item, + // as it's on its own line, and all comments on their own line are grouped + // with the next element below them to make "items", + // (an "item" being a region of source code that all moves as one unit + // when `__all__` is sorted). + // - The postlude in the above example is the source code region starting + // just after `# comment2` and ending just before the lgoical newline + // that follows the closing paren. `# comment2` is part of the last item, + // as it's an inline comment on the same line as an element, + // but `# comment3` becomes part of the postlude because there are no items + // below it. + // let prelude_end = { let first_item_line_offset = locator.line_start(first_item.start()); if first_item_line_offset == locator.line_start(self.start()) { From 382aed21239afd40b378600b3f80a01cd33e3178 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 14:59:18 +0000 Subject: [PATCH 27/77] Use a natural sort rather than an alphabetical sort --- .../resources/test/fixtures/ruff/RUF022.py | 9 ++ .../src/rules/ruff/rules/sort_dunder_all.rs | 17 ++-- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 97 ++++++++++++------- 3 files changed, 80 insertions(+), 43 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index 1345f644352cf..2f752bc0e6339 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -81,6 +81,15 @@ "aaa", ) +# we use natural sort for `__all__`, +# not alphabetical sort: +__all__ = ( + "aadvark237", + "aadvark10092", + "aadvark174", + "aadvark532", +) + ################################### # These should all not get flagged: ################################### diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 003ca2796ea98..82d7db7982b04 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -12,9 +12,11 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use itertools::Itertools; +use natord; /// ## What it does -/// Checks for `__all__` definitions that are not alphabetically sorted. +/// Checks for `__all__` definitions that are not ordered +/// according to a [natural sort](https://en.wikipedia.org/wiki/Natural_sort_order). /// /// ## Why is this bad? /// Consistency is good. Use a common convention for `__all__` to make your @@ -62,11 +64,11 @@ impl Violation for UnsortedDunderAll { #[derive_message_formats] fn message(&self) -> String { - format!("`__all__` is not alphabetically sorted") + format!("`__all__` is not sorted") } fn fix_title(&self) -> Option { - Some("Sort `__all__` alphabetically".to_string()) + Some("Sort `__all__` according to a natural sort".to_string()) } } @@ -511,15 +513,10 @@ impl PartialEq for DunderAllItem { impl Eq for DunderAllItem {} -impl DunderAllItem { - fn sort_index(&self) -> (&str, usize) { - (&self.value, self.original_index) - } -} - impl Ord for DunderAllItem { fn cmp(&self, other: &Self) -> Ordering { - self.sort_index().cmp(&other.sort_index()) + natord::compare(&self.value, &other.value) + .then_with(|| self.original_index.cmp(&other.original_index)) } } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index 682f7d43fa658..2fa1d3ccb2f84 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF022.py:5:11: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:5:11: RUF022 [*] `__all__` is not sorted | 3 | ################################################## 4 | @@ -10,7 +10,7 @@ RUF022.py:5:11: RUF022 [*] `__all__` is not alphabetically sorted 6 | __all__ += ["foo", "bar", "antipasti"] 7 | __all__ = ("d", "c", "b", "a") | - = help: Sort `__all__` alphabetically + = help: Sort `__all__` according to a natural sort ℹ Safe fix 2 2 | # Single-line __all__ definitions (nice 'n' easy!) @@ -22,14 +22,14 @@ RUF022.py:5:11: RUF022 [*] `__all__` is not alphabetically sorted 7 7 | __all__ = ("d", "c", "b", "a") 8 8 | -RUF022.py:6:12: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:6:12: RUF022 [*] `__all__` is not sorted | 5 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched 6 | __all__ += ["foo", "bar", "antipasti"] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF022 7 | __all__ = ("d", "c", "b", "a") | - = help: Sort `__all__` alphabetically + = help: Sort `__all__` according to a natural sort ℹ Safe fix 3 3 | ################################################## @@ -41,7 +41,7 @@ RUF022.py:6:12: RUF022 [*] `__all__` is not alphabetically sorted 8 8 | 9 9 | if bool(): -RUF022.py:7:11: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:7:11: RUF022 [*] `__all__` is not sorted | 5 | __all__ = ["d", "c", "b", "a"] # a comment that is untouched 6 | __all__ += ["foo", "bar", "antipasti"] @@ -50,7 +50,7 @@ RUF022.py:7:11: RUF022 [*] `__all__` is not alphabetically sorted 8 | 9 | if bool(): | - = help: Sort `__all__` alphabetically + = help: Sort `__all__` according to a natural sort ℹ Safe fix 4 4 | @@ -62,7 +62,7 @@ RUF022.py:7:11: RUF022 [*] `__all__` is not alphabetically sorted 9 9 | if bool(): 10 10 | __all__ += ("x", "m", "a", "s") -RUF022.py:10:16: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:10:16: RUF022 [*] `__all__` is not sorted | 9 | if bool(): 10 | __all__ += ("x", "m", "a", "s") @@ -70,7 +70,7 @@ RUF022.py:10:16: RUF022 [*] `__all__` is not alphabetically sorted 11 | else: 12 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) | - = help: Sort `__all__` alphabetically + = help: Sort `__all__` according to a natural sort ℹ Safe fix 7 7 | __all__ = ("d", "c", "b", "a") @@ -82,7 +82,7 @@ RUF022.py:10:16: RUF022 [*] `__all__` is not alphabetically sorted 12 12 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) 13 13 | -RUF022.py:12:16: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:12:16: RUF022 [*] `__all__` is not sorted | 10 | __all__ += ("x", "m", "a", "s") 11 | else: @@ -91,7 +91,7 @@ RUF022.py:12:16: RUF022 [*] `__all__` is not alphabetically sorted 13 | 14 | __all__: list[str] = ["the", "three", "little", "pigs"] | - = help: Sort `__all__` alphabetically + = help: Sort `__all__` according to a natural sort ℹ Safe fix 9 9 | if bool(): @@ -103,7 +103,7 @@ RUF022.py:12:16: RUF022 [*] `__all__` is not alphabetically sorted 14 14 | __all__: list[str] = ["the", "three", "little", "pigs"] 15 15 | -RUF022.py:14:22: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:14:22: RUF022 [*] `__all__` is not sorted | 12 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) 13 | @@ -112,7 +112,7 @@ RUF022.py:14:22: RUF022 [*] `__all__` is not alphabetically sorted 15 | 16 | #################################### | - = help: Sort `__all__` alphabetically + = help: Sort `__all__` according to a natural sort ℹ Safe fix 11 11 | else: @@ -124,7 +124,7 @@ RUF022.py:14:22: RUF022 [*] `__all__` is not alphabetically sorted 16 16 | #################################### 17 17 | # Neat multiline __all__ definitions -RUF022.py:20:11: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:20:11: RUF022 [*] `__all__` is not sorted | 18 | #################################### 19 | @@ -140,7 +140,7 @@ RUF022.py:20:11: RUF022 [*] `__all__` is not alphabetically sorted 27 | 28 | __all__ = [ | - = help: Sort `__all__` alphabetically + = help: Sort `__all__` according to a natural sort ℹ Unsafe fix 18 18 | #################################### @@ -159,7 +159,7 @@ RUF022.py:20:11: RUF022 [*] `__all__` is not alphabetically sorted 27 27 | 28 28 | __all__ = [ -RUF022.py:28:11: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:28:11: RUF022 [*] `__all__` is not sorted | 26 | ) 27 | @@ -175,7 +175,7 @@ RUF022.py:28:11: RUF022 [*] `__all__` is not alphabetically sorted 35 | 36 | ########################################## | - = help: Sort `__all__` alphabetically + = help: Sort `__all__` according to a natural sort ℹ Unsafe fix 26 26 | ) @@ -194,7 +194,7 @@ RUF022.py:28:11: RUF022 [*] `__all__` is not alphabetically sorted 35 35 | 36 36 | ########################################## -RUF022.py:41:11: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:41:11: RUF022 [*] `__all__` is not sorted | 40 | # comment0 41 | __all__ = ("d", "a", # comment1 @@ -209,7 +209,7 @@ RUF022.py:41:11: RUF022 [*] `__all__` is not alphabetically sorted | |_^ RUF022 49 | # comment7 | - = help: Sort `__all__` alphabetically + = help: Sort `__all__` according to a natural sort ℹ Unsafe fix 38 38 | ########################################## @@ -233,7 +233,7 @@ RUF022.py:41:11: RUF022 [*] `__all__` is not alphabetically sorted 48 51 | ) # comment6 49 52 | # comment7 -RUF022.py:51:11: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:51:11: RUF022 [*] `__all__` is not sorted | 49 | # comment7 50 | @@ -250,7 +250,7 @@ RUF022.py:51:11: RUF022 [*] `__all__` is not alphabetically sorted 59 | 60 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", | - = help: Sort `__all__` alphabetically + = help: Sort `__all__` according to a natural sort ℹ Unsafe fix 49 49 | # comment7 @@ -267,7 +267,7 @@ RUF022.py:51:11: RUF022 [*] `__all__` is not alphabetically sorted 56 59 | # comment5 57 60 | # comment6 -RUF022.py:60:11: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:60:11: RUF022 [*] `__all__` is not sorted | 58 | ] # comment7 59 | @@ -290,7 +290,7 @@ RUF022.py:60:11: RUF022 [*] `__all__` is not alphabetically sorted 74 | 75 | __all__: tuple[str, ...] = ( # a comment about the opening paren | - = help: Sort `__all__` alphabetically + = help: Sort `__all__` according to a natural sort ℹ Safe fix 57 57 | # comment6 @@ -318,13 +318,13 @@ RUF022.py:60:11: RUF022 [*] `__all__` is not alphabetically sorted 65 |+ "BOM64_LE", 66 |+ "BOM_BE", 67 |+ "BOM_LE", - 68 |+ "BOM_UTF16", - 69 |+ "BOM_UTF16_BE", - 70 |+ "BOM_UTF16_LE", - 71 |+ "BOM_UTF32", - 72 |+ "BOM_UTF32_BE", - 73 |+ "BOM_UTF32_LE", - 74 |+ "BOM_UTF8", + 68 |+ "BOM_UTF8", + 69 |+ "BOM_UTF16", + 70 |+ "BOM_UTF16_BE", + 71 |+ "BOM_UTF16_LE", + 72 |+ "BOM_UTF32", + 73 |+ "BOM_UTF32_BE", + 74 |+ "BOM_UTF32_LE", 75 |+ "Codec", 76 |+ "CodecInfo", 77 |+ "EncodedFile", @@ -359,7 +359,7 @@ RUF022.py:60:11: RUF022 [*] `__all__` is not alphabetically sorted 75 106 | __all__: tuple[str, ...] = ( # a comment about the opening paren 76 107 | # multiline comment about "bbb" part 1 -RUF022.py:75:28: RUF022 [*] `__all__` is not alphabetically sorted +RUF022.py:75:28: RUF022 [*] `__all__` is not sorted | 73 | "register_error", "lookup_error"] 74 | @@ -374,9 +374,9 @@ RUF022.py:75:28: RUF022 [*] `__all__` is not alphabetically sorted 82 | | ) | |_^ RUF022 83 | -84 | ################################### +84 | # we use natural sort for `__all__`, | - = help: Sort `__all__` alphabetically + = help: Sort `__all__` according to a natural sort ℹ Unsafe fix 73 73 | "register_error", "lookup_error"] @@ -393,6 +393,37 @@ RUF022.py:75:28: RUF022 [*] `__all__` is not alphabetically sorted 81 |- "aaa", 82 82 | ) 83 83 | -84 84 | ################################### +84 84 | # we use natural sort for `__all__`, + +RUF022.py:86:11: RUF022 [*] `__all__` is not sorted + | +84 | # we use natural sort for `__all__`, +85 | # not alphabetical sort: +86 | __all__ = ( + | ___________^ +87 | | "aadvark237", +88 | | "aadvark10092", +89 | | "aadvark174", +90 | | "aadvark532", +91 | | ) + | |_^ RUF022 +92 | +93 | ################################### + | + = help: Sort `__all__` according to a natural sort + +ℹ Safe fix +84 84 | # we use natural sort for `__all__`, +85 85 | # not alphabetical sort: +86 86 | __all__ = ( + 87 |+ "aadvark174", +87 88 | "aadvark237", + 89 |+ "aadvark532", +88 90 | "aadvark10092", +89 |- "aadvark174", +90 |- "aadvark532", +91 91 | ) +92 92 | +93 93 | ################################### From 6f5f55843c7bfc3ea3f7a336cacfceb33eeeb9c3 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 16:09:42 +0000 Subject: [PATCH 28/77] Don't add a trailing comma if it didn't already exist --- .../resources/test/fixtures/ruff/RUF022.py | 16 +- .../src/rules/ruff/rules/sort_dunder_all.rs | 27 +- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 621 ++++++++++-------- 3 files changed, 363 insertions(+), 301 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index 2f752bc0e6339..60e53891c5be4 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -6,6 +6,9 @@ __all__ += ["foo", "bar", "antipasti"] __all__ = ("d", "c", "b", "a") +__all__: list = ["b", "c", "a",] # note the trailing comma, which is retained +__all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained + if bool(): __all__ += ("x", "m", "a", "s") else: @@ -82,12 +85,14 @@ ) # we use natural sort for `__all__`, -# not alphabetical sort: +# not alphabetical sort. +# Also, this doesn't end with a trailing comma, +# so the autofix shouldn't introduce one: __all__ = ( "aadvark237", "aadvark10092", "aadvark174", - "aadvark532", + "aadvark532" ) ################################### @@ -97,7 +102,14 @@ __all__ = () __all__ = [] __all__ = ("single_item",) +__all__ = ( + "single_item_multiline", +) __all__ = ["single_item",] +__all__ = ["single_item_no_trailing_comma"] +__all__ = [ + "single_item_multiline_no_trailing_comma" +] __all__ = ("not_a_tuple_just_a_string") __all__ = ["a", "b", "c", "d"] __all__ += ["e", "f", "g"] diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 82d7db7982b04..ff5b254db6f29 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -172,6 +172,7 @@ struct DunderAllValue { items: Vec, range: TextRange, multiline: bool, + ends_with_trailing_comma: bool, } impl DunderAllValue { @@ -202,7 +203,7 @@ impl DunderAllValue { // Step (2): parse the `__all__` definition using the raw tokens. // // (2a). Start by collecting information on each line individually: - let lines = collect_dunder_all_lines(*range, locator)?; + let (lines, ends_with_trailing_comma) = collect_dunder_all_lines(*range, locator)?; // (2b). Group lines together into sortable "items": // - Any "item" contains a single element of the `__all__` list/tuple @@ -218,6 +219,7 @@ impl DunderAllValue { items, range: *range, multiline: is_multiline, + ends_with_trailing_comma, }) } @@ -296,12 +298,12 @@ impl DunderAllValue { first_item_line_offset } }; - let (needs_trailing_comma, postlude_start) = { + let postlude_start = { let last_item_line_offset = locator.line_end(last_item.end()); if last_item_line_offset == locator.line_end(self.end()) { - (false, last_item.end()) + last_item.end() } else { - (true, last_item_line_offset) + last_item_line_offset } }; let mut prelude = locator @@ -322,7 +324,7 @@ impl DunderAllValue { parent, indentation, newline, - needs_trailing_comma, + self.ends_with_trailing_comma, ) } else { Some(join_singleline_dunder_all_items(&sorted_items, locator)) @@ -345,7 +347,10 @@ enum SortedDunderAll { Sorted(Option), } -fn collect_dunder_all_lines(range: TextRange, locator: &Locator) -> Option> { +fn collect_dunder_all_lines( + range: TextRange, + locator: &Locator, +) -> Option<(Vec, bool)> { // Collect data on each line of `__all__`. // Return `None` if `__all__` appears to be invalid, // or if it's an edge case we don't care about. @@ -353,6 +358,7 @@ fn collect_dunder_all_lines(range: TextRange, locator: &Locator) -> Option Option comment_in_line = Some(subrange), - Tok::String { value, .. } => items_in_line.push((value, subrange)), - Tok::Comma => continue, + Tok::String { value, .. } => { + items_in_line.push((value, subrange)); + ends_with_trailing_comma = false; + } + Tok::Comma => ends_with_trailing_comma = true, _ => return None, } } - Some(lines) + Some((lines, ends_with_trailing_comma)) } #[derive(Debug)] diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index 2fa1d3ccb2f84..b97ad036fa26b 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -39,7 +39,7 @@ RUF022.py:6:12: RUF022 [*] `__all__` is not sorted 6 |+__all__ += ["antipasti", "bar", "foo"] 7 7 | __all__ = ("d", "c", "b", "a") 8 8 | -9 9 | if bool(): +9 9 | __all__: list = ["b", "c", "a",] # note the trailing comma, which is retained RUF022.py:7:11: RUF022 [*] `__all__` is not sorted | @@ -48,7 +48,7 @@ RUF022.py:7:11: RUF022 [*] `__all__` is not sorted 7 | __all__ = ("d", "c", "b", "a") | ^^^^^^^^^^^^^^^^^^^^ RUF022 8 | -9 | if bool(): +9 | __all__: list = ["b", "c", "a",] # note the trailing comma, which is retained | = help: Sort `__all__` according to a natural sort @@ -59,371 +59,412 @@ RUF022.py:7:11: RUF022 [*] `__all__` is not sorted 7 |-__all__ = ("d", "c", "b", "a") 7 |+__all__ = ("a", "b", "c", "d") 8 8 | -9 9 | if bool(): -10 10 | __all__ += ("x", "m", "a", "s") +9 9 | __all__: list = ["b", "c", "a",] # note the trailing comma, which is retained +10 10 | __all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained -RUF022.py:10:16: RUF022 [*] `__all__` is not sorted +RUF022.py:9:17: RUF022 [*] `__all__` is not sorted | - 9 | if bool(): -10 | __all__ += ("x", "m", "a", "s") - | ^^^^^^^^^^^^^^^^^^^^ RUF022 -11 | else: -12 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) + 7 | __all__ = ("d", "c", "b", "a") + 8 | + 9 | __all__: list = ["b", "c", "a",] # note the trailing comma, which is retained + | ^^^^^^^^^^^^^^^^ RUF022 +10 | __all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained + | + = help: Sort `__all__` according to a natural sort + +ℹ Safe fix +6 6 | __all__ += ["foo", "bar", "antipasti"] +7 7 | __all__ = ("d", "c", "b", "a") +8 8 | +9 |-__all__: list = ["b", "c", "a",] # note the trailing comma, which is retained + 9 |+__all__: list = ["a", "b", "c",] # note the trailing comma, which is retained +10 10 | __all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained +11 11 | +12 12 | if bool(): + +RUF022.py:10:18: RUF022 [*] `__all__` is not sorted + | + 9 | __all__: list = ["b", "c", "a",] # note the trailing comma, which is retained +10 | __all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained + | ^^^^^^^^^^^^^^^^ RUF022 +11 | +12 | if bool(): | = help: Sort `__all__` according to a natural sort ℹ Safe fix 7 7 | __all__ = ("d", "c", "b", "a") 8 8 | -9 9 | if bool(): -10 |- __all__ += ("x", "m", "a", "s") - 10 |+ __all__ += ("a", "m", "s", "x") -11 11 | else: -12 12 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) -13 13 | +9 9 | __all__: list = ["b", "c", "a",] # note the trailing comma, which is retained +10 |-__all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained + 10 |+__all__: tuple = ("a", "b", "c",) # note the trailing comma, which is retained +11 11 | +12 12 | if bool(): +13 13 | __all__ += ("x", "m", "a", "s") + +RUF022.py:13:16: RUF022 [*] `__all__` is not sorted + | +12 | if bool(): +13 | __all__ += ("x", "m", "a", "s") + | ^^^^^^^^^^^^^^^^^^^^ RUF022 +14 | else: +15 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) + | + = help: Sort `__all__` according to a natural sort + +ℹ Safe fix +10 10 | __all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained +11 11 | +12 12 | if bool(): +13 |- __all__ += ("x", "m", "a", "s") + 13 |+ __all__ += ("a", "m", "s", "x") +14 14 | else: +15 15 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +16 16 | -RUF022.py:12:16: RUF022 [*] `__all__` is not sorted +RUF022.py:15:16: RUF022 [*] `__all__` is not sorted | -10 | __all__ += ("x", "m", "a", "s") -11 | else: -12 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +13 | __all__ += ("x", "m", "a", "s") +14 | else: +15 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) | ^^^^^^^^^^^^^^^^^^^^^^ RUF022 -13 | -14 | __all__: list[str] = ["the", "three", "little", "pigs"] +16 | +17 | __all__: list[str] = ["the", "three", "little", "pigs"] | = help: Sort `__all__` according to a natural sort ℹ Safe fix -9 9 | if bool(): -10 10 | __all__ += ("x", "m", "a", "s") -11 11 | else: -12 |- __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) - 12 |+ __all__ += "foo1", "foo2", "foo3" # NB: an implicit tuple (without parens) -13 13 | -14 14 | __all__: list[str] = ["the", "three", "little", "pigs"] -15 15 | +12 12 | if bool(): +13 13 | __all__ += ("x", "m", "a", "s") +14 14 | else: +15 |- __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) + 15 |+ __all__ += "foo1", "foo2", "foo3" # NB: an implicit tuple (without parens) +16 16 | +17 17 | __all__: list[str] = ["the", "three", "little", "pigs"] +18 18 | -RUF022.py:14:22: RUF022 [*] `__all__` is not sorted +RUF022.py:17:22: RUF022 [*] `__all__` is not sorted | -12 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) -13 | -14 | __all__: list[str] = ["the", "three", "little", "pigs"] +15 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +16 | +17 | __all__: list[str] = ["the", "three", "little", "pigs"] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF022 -15 | -16 | #################################### +18 | +19 | #################################### | = help: Sort `__all__` according to a natural sort ℹ Safe fix -11 11 | else: -12 12 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) -13 13 | -14 |-__all__: list[str] = ["the", "three", "little", "pigs"] - 14 |+__all__: list[str] = ["little", "pigs", "the", "three"] -15 15 | -16 16 | #################################### -17 17 | # Neat multiline __all__ definitions +14 14 | else: +15 15 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +16 16 | +17 |-__all__: list[str] = ["the", "three", "little", "pigs"] + 17 |+__all__: list[str] = ["little", "pigs", "the", "three"] +18 18 | +19 19 | #################################### +20 20 | # Neat multiline __all__ definitions -RUF022.py:20:11: RUF022 [*] `__all__` is not sorted +RUF022.py:23:11: RUF022 [*] `__all__` is not sorted | -18 | #################################### -19 | -20 | __all__ = ( +21 | #################################### +22 | +23 | __all__ = ( | ___________^ -21 | | "d0", -22 | | "c0", # a comment regarding 'c0' -23 | | "b0", -24 | | # a comment regarding 'a0': -25 | | "a0" -26 | | ) +24 | | "d0", +25 | | "c0", # a comment regarding 'c0' +26 | | "b0", +27 | | # a comment regarding 'a0': +28 | | "a0" +29 | | ) | |_^ RUF022 -27 | -28 | __all__ = [ +30 | +31 | __all__ = [ | = help: Sort `__all__` according to a natural sort ℹ Unsafe fix -18 18 | #################################### -19 19 | -20 20 | __all__ = ( - 21 |+ # a comment regarding 'a0': - 22 |+ "a0", - 23 |+ "b0", - 24 |+ "c0", # a comment regarding 'c0' -21 25 | "d0", -22 |- "c0", # a comment regarding 'c0' -23 |- "b0", -24 |- # a comment regarding 'a0': -25 |- "a0" -26 26 | ) -27 27 | -28 28 | __all__ = [ +21 21 | #################################### +22 22 | +23 23 | __all__ = ( +24 |- "d0", + 24 |+ # a comment regarding 'a0': + 25 |+ "a0", + 26 |+ "b0", +25 27 | "c0", # a comment regarding 'c0' +26 |- "b0", +27 |- # a comment regarding 'a0': +28 |- "a0" + 28 |+ "d0" +29 29 | ) +30 30 | +31 31 | __all__ = [ -RUF022.py:28:11: RUF022 [*] `__all__` is not sorted +RUF022.py:31:11: RUF022 [*] `__all__` is not sorted | -26 | ) -27 | -28 | __all__ = [ +29 | ) +30 | +31 | __all__ = [ | ___________^ -29 | | "d", -30 | | "c", # a comment regarding 'c' -31 | | "b", -32 | | # a comment regarding 'a': -33 | | "a" -34 | | ] +32 | | "d", +33 | | "c", # a comment regarding 'c' +34 | | "b", +35 | | # a comment regarding 'a': +36 | | "a" +37 | | ] | |_^ RUF022 -35 | -36 | ########################################## +38 | +39 | ########################################## | = help: Sort `__all__` according to a natural sort ℹ Unsafe fix -26 26 | ) -27 27 | -28 28 | __all__ = [ - 29 |+ # a comment regarding 'a': - 30 |+ "a", - 31 |+ "b", - 32 |+ "c", # a comment regarding 'c' -29 33 | "d", -30 |- "c", # a comment regarding 'c' -31 |- "b", -32 |- # a comment regarding 'a': -33 |- "a" -34 34 | ] -35 35 | -36 36 | ########################################## +29 29 | ) +30 30 | +31 31 | __all__ = [ +32 |- "d", + 32 |+ # a comment regarding 'a': + 33 |+ "a", + 34 |+ "b", +33 35 | "c", # a comment regarding 'c' +34 |- "b", +35 |- # a comment regarding 'a': +36 |- "a" + 36 |+ "d" +37 37 | ] +38 38 | +39 39 | ########################################## -RUF022.py:41:11: RUF022 [*] `__all__` is not sorted +RUF022.py:44:11: RUF022 [*] `__all__` is not sorted | -40 | # comment0 -41 | __all__ = ("d", "a", # comment1 +43 | # comment0 +44 | __all__ = ("d", "a", # comment1 | ___________^ -42 | | # comment2 -43 | | "f", "b", -44 | | "strangely", # comment3 -45 | | # comment4 -46 | | "formatted", -47 | | # comment5 -48 | | ) # comment6 +45 | | # comment2 +46 | | "f", "b", +47 | | "strangely", # comment3 +48 | | # comment4 +49 | | "formatted", +50 | | # comment5 +51 | | ) # comment6 | |_^ RUF022 -49 | # comment7 +52 | # comment7 | = help: Sort `__all__` according to a natural sort ℹ Unsafe fix -38 38 | ########################################## -39 39 | -40 40 | # comment0 -41 |-__all__ = ("d", "a", # comment1 -42 |- # comment2 -43 |- "f", "b", -44 |- "strangely", # comment3 -45 |- # comment4 - 41 |+__all__ = ( - 42 |+ "a", - 43 |+ "b", - 44 |+ "d", # comment1 - 45 |+ # comment2 - 46 |+ "f", - 47 |+ # comment4 -46 48 | "formatted", - 49 |+ "strangely", # comment3 -47 50 | # comment5 -48 51 | ) # comment6 -49 52 | # comment7 +41 41 | ########################################## +42 42 | +43 43 | # comment0 +44 |-__all__ = ("d", "a", # comment1 +45 |- # comment2 +46 |- "f", "b", +47 |- "strangely", # comment3 +48 |- # comment4 + 44 |+__all__ = ( + 45 |+ "a", + 46 |+ "b", + 47 |+ "d", # comment1 + 48 |+ # comment2 + 49 |+ "f", + 50 |+ # comment4 +49 51 | "formatted", + 52 |+ "strangely", # comment3 +50 53 | # comment5 +51 54 | ) # comment6 +52 55 | # comment7 -RUF022.py:51:11: RUF022 [*] `__all__` is not sorted +RUF022.py:54:11: RUF022 [*] `__all__` is not sorted | -49 | # comment7 -50 | -51 | __all__ = [ # comment0 +52 | # comment7 +53 | +54 | __all__ = [ # comment0 | ___________^ -52 | | # comment1 -53 | | # comment2 -54 | | "dx", "cx", "bx", "ax" # comment3 -55 | | # comment4 -56 | | # comment5 -57 | | # comment6 -58 | | ] # comment7 +55 | | # comment1 +56 | | # comment2 +57 | | "dx", "cx", "bx", "ax" # comment3 +58 | | # comment4 +59 | | # comment5 +60 | | # comment6 +61 | | ] # comment7 | |_^ RUF022 -59 | -60 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", +62 | +63 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", | = help: Sort `__all__` according to a natural sort ℹ Unsafe fix -49 49 | # comment7 -50 50 | -51 51 | __all__ = [ # comment0 - 52 |+ "ax", - 53 |+ "bx", - 54 |+ "cx", -52 55 | # comment1 -53 56 | # comment2 -54 |- "dx", "cx", "bx", "ax" # comment3 - 57 |+ "dx", # comment3 -55 58 | # comment4 -56 59 | # comment5 -57 60 | # comment6 +52 52 | # comment7 +53 53 | +54 54 | __all__ = [ # comment0 + 55 |+ "ax", + 56 |+ "bx", + 57 |+ "cx", +55 58 | # comment1 +56 59 | # comment2 +57 |- "dx", "cx", "bx", "ax" # comment3 + 60 |+ "dx" # comment3 +58 61 | # comment4 +59 62 | # comment5 +60 63 | # comment6 -RUF022.py:60:11: RUF022 [*] `__all__` is not sorted +RUF022.py:63:11: RUF022 [*] `__all__` is not sorted | -58 | ] # comment7 -59 | -60 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", +61 | ] # comment7 +62 | +63 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", | ___________^ -61 | | "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", -62 | | "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", -63 | | "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", -64 | | "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", -65 | | "StreamReader", "StreamWriter", -66 | | "StreamReaderWriter", "StreamRecoder", -67 | | "getencoder", "getdecoder", "getincrementalencoder", -68 | | "getincrementaldecoder", "getreader", "getwriter", -69 | | "encode", "decode", "iterencode", "iterdecode", -70 | | "strict_errors", "ignore_errors", "replace_errors", -71 | | "xmlcharrefreplace_errors", -72 | | "backslashreplace_errors", "namereplace_errors", -73 | | "register_error", "lookup_error"] +64 | | "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", +65 | | "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", +66 | | "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", +67 | | "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", +68 | | "StreamReader", "StreamWriter", +69 | | "StreamReaderWriter", "StreamRecoder", +70 | | "getencoder", "getdecoder", "getincrementalencoder", +71 | | "getincrementaldecoder", "getreader", "getwriter", +72 | | "encode", "decode", "iterencode", "iterdecode", +73 | | "strict_errors", "ignore_errors", "replace_errors", +74 | | "xmlcharrefreplace_errors", +75 | | "backslashreplace_errors", "namereplace_errors", +76 | | "register_error", "lookup_error"] | |____________________________________________^ RUF022 -74 | -75 | __all__: tuple[str, ...] = ( # a comment about the opening paren +77 | +78 | __all__: tuple[str, ...] = ( # a comment about the opening paren | = help: Sort `__all__` according to a natural sort ℹ Safe fix -57 57 | # comment6 -58 58 | ] # comment7 -59 59 | -60 |-__all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", -61 |- "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", -62 |- "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", -63 |- "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", -64 |- "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", -65 |- "StreamReader", "StreamWriter", -66 |- "StreamReaderWriter", "StreamRecoder", -67 |- "getencoder", "getdecoder", "getincrementalencoder", -68 |- "getincrementaldecoder", "getreader", "getwriter", -69 |- "encode", "decode", "iterencode", "iterdecode", -70 |- "strict_errors", "ignore_errors", "replace_errors", -71 |- "xmlcharrefreplace_errors", -72 |- "backslashreplace_errors", "namereplace_errors", -73 |- "register_error", "lookup_error"] - 60 |+__all__ = [ - 61 |+ "BOM", - 62 |+ "BOM32_BE", - 63 |+ "BOM32_LE", - 64 |+ "BOM64_BE", - 65 |+ "BOM64_LE", - 66 |+ "BOM_BE", - 67 |+ "BOM_LE", - 68 |+ "BOM_UTF8", - 69 |+ "BOM_UTF16", - 70 |+ "BOM_UTF16_BE", - 71 |+ "BOM_UTF16_LE", - 72 |+ "BOM_UTF32", - 73 |+ "BOM_UTF32_BE", - 74 |+ "BOM_UTF32_LE", - 75 |+ "Codec", - 76 |+ "CodecInfo", - 77 |+ "EncodedFile", - 78 |+ "IncrementalDecoder", - 79 |+ "IncrementalEncoder", - 80 |+ "StreamReader", - 81 |+ "StreamReaderWriter", - 82 |+ "StreamRecoder", - 83 |+ "StreamWriter", - 84 |+ "backslashreplace_errors", - 85 |+ "decode", - 86 |+ "encode", - 87 |+ "getdecoder", - 88 |+ "getencoder", - 89 |+ "getincrementaldecoder", - 90 |+ "getincrementalencoder", - 91 |+ "getreader", - 92 |+ "getwriter", - 93 |+ "ignore_errors", - 94 |+ "iterdecode", - 95 |+ "iterencode", - 96 |+ "lookup", - 97 |+ "lookup_error", - 98 |+ "namereplace_errors", - 99 |+ "open", - 100 |+ "register", - 101 |+ "register_error", - 102 |+ "replace_errors", - 103 |+ "strict_errors", - 104 |+ "xmlcharrefreplace_errors"] -74 105 | -75 106 | __all__: tuple[str, ...] = ( # a comment about the opening paren -76 107 | # multiline comment about "bbb" part 1 +60 60 | # comment6 +61 61 | ] # comment7 +62 62 | +63 |-__all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", +64 |- "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", +65 |- "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", +66 |- "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", +67 |- "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", +68 |- "StreamReader", "StreamWriter", +69 |- "StreamReaderWriter", "StreamRecoder", +70 |- "getencoder", "getdecoder", "getincrementalencoder", +71 |- "getincrementaldecoder", "getreader", "getwriter", +72 |- "encode", "decode", "iterencode", "iterdecode", +73 |- "strict_errors", "ignore_errors", "replace_errors", +74 |- "xmlcharrefreplace_errors", +75 |- "backslashreplace_errors", "namereplace_errors", +76 |- "register_error", "lookup_error"] + 63 |+__all__ = [ + 64 |+ "BOM", + 65 |+ "BOM32_BE", + 66 |+ "BOM32_LE", + 67 |+ "BOM64_BE", + 68 |+ "BOM64_LE", + 69 |+ "BOM_BE", + 70 |+ "BOM_LE", + 71 |+ "BOM_UTF8", + 72 |+ "BOM_UTF16", + 73 |+ "BOM_UTF16_BE", + 74 |+ "BOM_UTF16_LE", + 75 |+ "BOM_UTF32", + 76 |+ "BOM_UTF32_BE", + 77 |+ "BOM_UTF32_LE", + 78 |+ "Codec", + 79 |+ "CodecInfo", + 80 |+ "EncodedFile", + 81 |+ "IncrementalDecoder", + 82 |+ "IncrementalEncoder", + 83 |+ "StreamReader", + 84 |+ "StreamReaderWriter", + 85 |+ "StreamRecoder", + 86 |+ "StreamWriter", + 87 |+ "backslashreplace_errors", + 88 |+ "decode", + 89 |+ "encode", + 90 |+ "getdecoder", + 91 |+ "getencoder", + 92 |+ "getincrementaldecoder", + 93 |+ "getincrementalencoder", + 94 |+ "getreader", + 95 |+ "getwriter", + 96 |+ "ignore_errors", + 97 |+ "iterdecode", + 98 |+ "iterencode", + 99 |+ "lookup", + 100 |+ "lookup_error", + 101 |+ "namereplace_errors", + 102 |+ "open", + 103 |+ "register", + 104 |+ "register_error", + 105 |+ "replace_errors", + 106 |+ "strict_errors", + 107 |+ "xmlcharrefreplace_errors"] +77 108 | +78 109 | __all__: tuple[str, ...] = ( # a comment about the opening paren +79 110 | # multiline comment about "bbb" part 1 -RUF022.py:75:28: RUF022 [*] `__all__` is not sorted +RUF022.py:78:28: RUF022 [*] `__all__` is not sorted | -73 | "register_error", "lookup_error"] -74 | -75 | __all__: tuple[str, ...] = ( # a comment about the opening paren +76 | "register_error", "lookup_error"] +77 | +78 | __all__: tuple[str, ...] = ( # a comment about the opening paren | ____________________________^ -76 | | # multiline comment about "bbb" part 1 -77 | | # multiline comment about "bbb" part 2 -78 | | "bbb", -79 | | # multiline comment about "aaa" part 1 -80 | | # multiline comment about "aaa" part 2 -81 | | "aaa", -82 | | ) +79 | | # multiline comment about "bbb" part 1 +80 | | # multiline comment about "bbb" part 2 +81 | | "bbb", +82 | | # multiline comment about "aaa" part 1 +83 | | # multiline comment about "aaa" part 2 +84 | | "aaa", +85 | | ) | |_^ RUF022 -83 | -84 | # we use natural sort for `__all__`, +86 | +87 | # we use natural sort for `__all__`, | = help: Sort `__all__` according to a natural sort ℹ Unsafe fix -73 73 | "register_error", "lookup_error"] -74 74 | -75 75 | __all__: tuple[str, ...] = ( # a comment about the opening paren - 76 |+ # multiline comment about "aaa" part 1 - 77 |+ # multiline comment about "aaa" part 2 - 78 |+ "aaa", -76 79 | # multiline comment about "bbb" part 1 -77 80 | # multiline comment about "bbb" part 2 -78 81 | "bbb", -79 |- # multiline comment about "aaa" part 1 -80 |- # multiline comment about "aaa" part 2 -81 |- "aaa", -82 82 | ) -83 83 | -84 84 | # we use natural sort for `__all__`, +76 76 | "register_error", "lookup_error"] +77 77 | +78 78 | __all__: tuple[str, ...] = ( # a comment about the opening paren + 79 |+ # multiline comment about "aaa" part 1 + 80 |+ # multiline comment about "aaa" part 2 + 81 |+ "aaa", +79 82 | # multiline comment about "bbb" part 1 +80 83 | # multiline comment about "bbb" part 2 +81 84 | "bbb", +82 |- # multiline comment about "aaa" part 1 +83 |- # multiline comment about "aaa" part 2 +84 |- "aaa", +85 85 | ) +86 86 | +87 87 | # we use natural sort for `__all__`, -RUF022.py:86:11: RUF022 [*] `__all__` is not sorted +RUF022.py:91:11: RUF022 [*] `__all__` is not sorted | -84 | # we use natural sort for `__all__`, -85 | # not alphabetical sort: -86 | __all__ = ( +89 | # Also, this doesn't end with a trailing comma, +90 | # so the autofix shouldn't introduce one: +91 | __all__ = ( | ___________^ -87 | | "aadvark237", -88 | | "aadvark10092", -89 | | "aadvark174", -90 | | "aadvark532", -91 | | ) +92 | | "aadvark237", +93 | | "aadvark10092", +94 | | "aadvark174", +95 | | "aadvark532" +96 | | ) | |_^ RUF022 -92 | -93 | ################################### +97 | +98 | ################################### | = help: Sort `__all__` according to a natural sort ℹ Safe fix -84 84 | # we use natural sort for `__all__`, -85 85 | # not alphabetical sort: -86 86 | __all__ = ( - 87 |+ "aadvark174", -87 88 | "aadvark237", - 89 |+ "aadvark532", -88 90 | "aadvark10092", -89 |- "aadvark174", -90 |- "aadvark532", -91 91 | ) -92 92 | -93 93 | ################################### +89 89 | # Also, this doesn't end with a trailing comma, +90 90 | # so the autofix shouldn't introduce one: +91 91 | __all__ = ( + 92 |+ "aadvark174", +92 93 | "aadvark237", +93 |- "aadvark10092", +94 |- "aadvark174", +95 |- "aadvark532" + 94 |+ "aadvark532", + 95 |+ "aadvark10092" +96 96 | ) +97 97 | +98 98 | ################################### From 655bf67ae3459da6b0bec34ab959d56cd4d26bfe Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 16:52:57 +0000 Subject: [PATCH 29/77] Don't insist on two spaces before an inline comment --- .../resources/test/fixtures/ruff/RUF022.py | 4 ++-- .../src/rules/ruff/rules/sort_dunder_all.rs | 20 ++++++++++++++++--- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 14 ++++++------- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index 60e53891c5be4..a98c87b0b90d1 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -91,8 +91,8 @@ __all__ = ( "aadvark237", "aadvark10092", - "aadvark174", - "aadvark532" + "aadvark174", # the very long whitespace span before this comment is retained + "aadvark532" # the even longer whitespace span before this comment is retained ) ################################### diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index ff5b254db6f29..0e98aa9813266 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -357,6 +357,7 @@ fn collect_dunder_all_lines( let mut parentheses_open = false; let mut lines = vec![]; let mut items_in_line = vec![]; + let mut comment_range_start = None; let mut comment_in_line = None; let mut ends_with_trailing_comma = false; // lex_starts_at gives us absolute ranges rather than relative ranges, @@ -389,6 +390,7 @@ fn collect_dunder_all_lines( if let Some(comment) = comment_in_line { lines.push(DunderAllLine::JustAComment(LineWithJustAComment(comment))); comment_in_line = None; + comment_range_start = None; } } else { lines.push(DunderAllLine::OneOrMoreItems(LineWithItems::new( @@ -396,14 +398,27 @@ fn collect_dunder_all_lines( comment_in_line, ))); comment_in_line = None; + comment_range_start = None; + } + } + Tok::Comment(_) => { + comment_in_line = { + if let Some(range_start) = comment_range_start { + Some(TextRange::new(range_start, subrange.end())) + } else { + Some(subrange) + } } } - Tok::Comment(_) => comment_in_line = Some(subrange), Tok::String { value, .. } => { items_in_line.push((value, subrange)); ends_with_trailing_comma = false; + comment_range_start = Some(subrange.end()); + } + Tok::Comma => { + comment_range_start = Some(subrange.end()); + ends_with_trailing_comma = true; } - Tok::Comma => ends_with_trailing_comma = true, _ => return None, } } @@ -561,7 +576,6 @@ fn join_multiline_dunder_all_items( new_dunder_all.push(','); } if let Some(comment) = item.additional_comments { - new_dunder_all.push_str(" "); new_dunder_all.push_str(locator.slice(comment)); } if !is_final_item { diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index b97ad036fa26b..5f798c8f3981c 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -443,8 +443,8 @@ RUF022.py:91:11: RUF022 [*] `__all__` is not sorted | ___________^ 92 | | "aadvark237", 93 | | "aadvark10092", -94 | | "aadvark174", -95 | | "aadvark532" +94 | | "aadvark174", # the very long whitespace span before this comment is retained +95 | | "aadvark532" # the even longer whitespace span before this comment is retained 96 | | ) | |_^ RUF022 97 | @@ -452,16 +452,16 @@ RUF022.py:91:11: RUF022 [*] `__all__` is not sorted | = help: Sort `__all__` according to a natural sort -ℹ Safe fix +ℹ Unsafe fix 89 89 | # Also, this doesn't end with a trailing comma, 90 90 | # so the autofix shouldn't introduce one: 91 91 | __all__ = ( - 92 |+ "aadvark174", + 92 |+ "aadvark174", # the very long whitespace span before this comment is retained 92 93 | "aadvark237", 93 |- "aadvark10092", -94 |- "aadvark174", -95 |- "aadvark532" - 94 |+ "aadvark532", +94 |- "aadvark174", # the very long whitespace span before this comment is retained +95 |- "aadvark532" # the even longer whitespace span before this comment is retained + 94 |+ "aadvark532", # the even longer whitespace span before this comment is retained 95 |+ "aadvark10092" 96 96 | ) 97 97 | From 9e2bae25679168aa15178f622366dee2481e960c Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 16:57:40 +0000 Subject: [PATCH 30/77] rename `additional_comments` -> `end_of_line_comments` --- .../src/rules/ruff/rules/sort_dunder_all.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 0e98aa9813266..a8571b773aa8a 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -496,7 +496,7 @@ fn collect_dunder_all_items( value: first_val, original_index: all_items.len(), range, - additional_comments: comment_range, + end_of_line_comments: comment_range, }); this_range = None; for (value, range) in owned_items { @@ -504,7 +504,7 @@ fn collect_dunder_all_items( value, original_index: all_items.len(), range, - additional_comments: None, + end_of_line_comments: None, }); } } @@ -520,7 +520,7 @@ struct DunderAllItem { original_index: usize, // Note that this range might include comments, etc. range: TextRange, - additional_comments: Option, + end_of_line_comments: Option, } impl Ranged for DunderAllItem { @@ -575,8 +575,8 @@ fn join_multiline_dunder_all_items( if !is_final_item || needs_trailing_comma { new_dunder_all.push(','); } - if let Some(comment) = item.additional_comments { - new_dunder_all.push_str(locator.slice(comment)); + if let Some(trailing_comments) = item.end_of_line_comments { + new_dunder_all.push_str(locator.slice(trailing_comments)); } if !is_final_item { new_dunder_all.push_str(newline); From af6ca0556fe8c3906bd5e52b85f0129a3e41fef0 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 17:01:58 +0000 Subject: [PATCH 31/77] add comment explaining why comments are grouped with elements into items --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index a8571b773aa8a..634f817717aee 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -471,10 +471,16 @@ fn collect_dunder_all_items( for line in lines { match line { DunderAllLine::JustAComment(LineWithJustAComment(comment_range)) => { + // Comments on the same line as the opening paren and before any elements + // count as part of the "prelude"; these are not grouped into any item... if first_item_encountered || locator.line_start(comment_range.start()) != locator.line_start(dunder_all_range.start()) { + // ...but for all other comments that precede an element, + // group them with that element into an "item", + // so that those comments move as one with the element + // when the `__all__` list/tuple is sorted this_range = Some(this_range.map_or(comment_range, |range| { TextRange::new(range.start(), comment_range.end()) })); From e8481b32e937295a42fa0dc0c79aed6d95d0556d Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 17:50:48 +0000 Subject: [PATCH 32/77] rename `this_range` -> `preceding_comment_range` --- .../src/rules/ruff/rules/sort_dunder_all.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 634f817717aee..ef72f9c824487 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -467,7 +467,7 @@ fn collect_dunder_all_items( _ => lines.len(), }); let mut first_item_encountered = false; - let mut this_range: Option = None; + let mut preceding_comment_range: Option = None; for line in lines { match line { DunderAllLine::JustAComment(LineWithJustAComment(comment_range)) => { @@ -481,9 +481,10 @@ fn collect_dunder_all_items( // group them with that element into an "item", // so that those comments move as one with the element // when the `__all__` list/tuple is sorted - this_range = Some(this_range.map_or(comment_range, |range| { - TextRange::new(range.start(), comment_range.end()) - })); + preceding_comment_range = + Some(preceding_comment_range.map_or(comment_range, |range| { + TextRange::new(range.start(), comment_range.end()) + })); } } DunderAllLine::OneOrMoreItems(LineWithItems { @@ -495,7 +496,7 @@ fn collect_dunder_all_items( let (first_val, first_range) = owned_items .next() .expect("LineWithItems::new() should uphold the invariant that this list is always non-empty"); - let range = this_range.map_or(first_range, |r| { + let range = preceding_comment_range.map_or(first_range, |r| { TextRange::new(r.start(), first_range.end()) }); all_items.push(DunderAllItem { @@ -504,7 +505,7 @@ fn collect_dunder_all_items( range, end_of_line_comments: comment_range, }); - this_range = None; + preceding_comment_range = None; for (value, range) in owned_items { all_items.push(DunderAllItem { value, From 572bb66546b9d0307b0de8e1e576041890410396 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 18:33:23 +0000 Subject: [PATCH 33/77] Hugely overhaul docs and comments --- .../src/rules/ruff/rules/sort_dunder_all.rs | 127 ++++++++++++++++-- 1 file changed, 119 insertions(+), 8 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index ef72f9c824487..1390cd967d650 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -72,6 +72,8 @@ impl Violation for UnsortedDunderAll { } } +/// Sort an `__all__` definition represented by a `StmtAssign` node. +/// For example: `__all__ = ["b", "c", "a"]`. pub(crate) fn sort_dunder_all_assign( checker: &mut Checker, ast::StmtAssign { value, targets, .. }: &ast::StmtAssign, @@ -83,6 +85,8 @@ pub(crate) fn sort_dunder_all_assign( sort_dunder_all(checker, id, value, parent); } +/// Sort an `__all__` mutation represented by a `StmtAugAssign` node. +/// For example: `__all__ += ["b", "c", "a"]`. pub(crate) fn sort_dunder_all_aug_assign( checker: &mut Checker, node: &ast::StmtAugAssign, @@ -103,6 +107,8 @@ pub(crate) fn sort_dunder_all_aug_assign( sort_dunder_all(checker, id, value, parent); } +/// Sort an `__all__` mutation represented by a `StmtAnnAssign` node. +/// For example: `__all__: list[str] = ["b", "c", "a"]`. pub(crate) fn sort_dunder_all_ann_assign( checker: &mut Checker, node: &ast::StmtAnnAssign, @@ -168,6 +174,8 @@ fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr, parent checker.diagnostics.push(diagnostic); } +/// Struct encapsulating an analysis of a Python tuple/list +/// that represents an `__all__` definition or augmentation. struct DunderAllValue { items: Vec, range: TextRange, @@ -176,6 +184,10 @@ struct DunderAllValue { } impl DunderAllValue { + /// Analyses an AST node for a Python tuple/list that represents an `__all__` + /// definition or augmentation. Returns `None` if the analysis fails + /// for whatever reason, or if it looks like we're not actually looking at a + /// tuple/list after all. fn from_expr(value: &ast::Expr, locator: &Locator) -> Option { // Step (1): inspect the AST to check that we're looking at something vaguely sane: let is_multiline = locator.contains_line_break(value.range()); @@ -186,7 +198,15 @@ impl DunderAllValue { }; // An `__all__` definition with < 2 elements can't be unsorted; - // no point in proceeding any further here + // no point in proceeding any further here. + // + // N.B. Here, this is just an optimisation + // (and to avoid us rewriting code when we don't have to). + // + // While other parts of this file *do* depend on there being a + // minimum of 2 elements in `__all__`, that invariant + // is maintained elsewhere. (For example, see comments at the + // start of `into_sorted_source_code()`.) if elts.len() < 2 { return None; } @@ -236,6 +256,11 @@ impl DunderAllValue { true } + /// Determine whether `__all__` is already sorted. + /// If it is not already sorted, attempt to sort `__all__`, + /// and return a string with the sorted `__all__ definition/augmentation` + /// that can be inserted into the source + /// code as a range replacement. fn into_sorted_source_code( self, locator: &Locator, @@ -341,19 +366,42 @@ impl Ranged for DunderAllValue { } } +/// Variants of this enum are returned by `into_sorted_source_code()`. +/// +/// There are three possible states represented here: +/// (1) `__all__` was already sorted +/// (2) `__all__` was not already sorted, +/// but we couldn't figure out how to sort it safely +/// (3) `__all__` was not already sorted; +/// here's the source code to replace it with, +/// so that it becomes sorted! #[derive(Debug)] enum SortedDunderAll { AlreadySorted, Sorted(Option), } +/// Collect data on each line of `__all__`. +/// Return `None` if `__all__` appears to be invalid, +/// or if it's an edge case we don't care about. +/// +/// Why do we need to do this using the raw tokens, +/// when we already have the AST? The AST strips out +/// crucial information that we need to track here, such as: +/// - The value of comments +/// - The amount of whitespace between the end of a line +/// and an inline comment +/// - Whether or not the final item in the tuple/list has a +/// trailing comma +/// +/// All of this information is necessary to have at a later +/// stage if we're to sort items without doing unnecessary +/// brutality to the comments and pre-existing style choices +/// in the original source code. fn collect_dunder_all_lines( range: TextRange, locator: &Locator, ) -> Option<(Vec, bool)> { - // Collect data on each line of `__all__`. - // Return `None` if `__all__` appears to be invalid, - // or if it's an edge case we don't care about. let mut parentheses_open = false; let mut lines = vec![]; let mut items_in_line = vec![]; @@ -366,6 +414,19 @@ fn collect_dunder_all_lines( for pair in lexer::lex_starts_at(locator.slice(range), Mode::Expression, range.start()) { let (tok, subrange) = pair.ok()?; match tok { + // If exactly one `Lpar`` or `Lsqb`` is encountered, that's fine + // -- a valid __all__ definition has to be a list or tuple, + // and most (though not all) lists/tuples start with either a `(` or a `[`. + // + // Any more than one `(` or `[` in an `__all__` definition, however, + // indicates that we've got something here that's just too complex + // for us to handle. Maybe a string element in `__all__` is parenthesized; + // maybe the `__all__` definition is in fact invalid syntax; + // maybe there's some other thing going on that we haven't anticipated. + // + // Whatever the case -- if we encounter more than one `(` or `[`, + // we evidently don't know what to do here. So just return `None` to + // signal failure. Tok::Lpar | Tok::Lsqb => { if parentheses_open { return None; @@ -425,12 +486,23 @@ fn collect_dunder_all_lines( Some((lines, ends_with_trailing_comma)) } +/// Instances of this struct represent source-code lines in the middle +/// of multiline `__all__` tuples/lists where the line contains +/// 0 elements of the tuple/list, but does have a comment in it. #[derive(Debug)] struct LineWithJustAComment(TextRange); +/// Instances of this struct represent source-code lines in single-line +/// or multiline `__all__` tuples/lists where the line contains at least +/// 1 element of the tuple/list. The line may contain > 1 element of the +/// tuple/list, and may also have a trailing comment after the element(s). #[derive(Debug)] struct LineWithItems { + // For elements in the list, we keep track of the value of the + // value of the element as well as the source-code range of the element. + // (We need to know the actual value so that we can sort the items.) items: Vec<(String, TextRange)>, + // For comments, we only need to keep track of the source-code range. comment_range: Option, } @@ -453,15 +525,19 @@ enum DunderAllLine { OneOrMoreItems(LineWithItems), } +/// Given data on each line in `__all__`, group lines together into "items". +/// Each item contains exactly one string element, +/// but might contain multiple comments attached to that element +/// that must move with the element when `__all__` is sorted. +/// +/// Note that any comments following the last item are discarded here, +/// but that doesn't matter: we add them back in `into_sorted_source_code()` +/// as part of the `postlude` (see comments in that function) fn collect_dunder_all_items( lines: Vec, dunder_all_range: TextRange, locator: &Locator, ) -> Vec { - // Given data on each line in `__all__`, group lines together into "items". - // Each item contains exactly one element, - // but might contain multiple comments attached to that element - // that must move with the element when `__all__` is sorted. let mut all_items = Vec::with_capacity(match lines.as_slice() { [DunderAllLine::OneOrMoreItems(single)] => single.items.len(), _ => lines.len(), @@ -520,6 +596,41 @@ fn collect_dunder_all_items( all_items } +/// An instance of this struct represents a single element +/// from the original tuple/list, *and* any comments that +/// are "attached" to it. The comments "attached" to the element +/// will move with the element when the `__all__` tuple/list is sorted. +/// +/// Comments on their own line immediately preceding the element will +/// always form a contiguous range with the range of the element itself; +/// however, inline comments won't necessary form a contiguous range. +/// Consider the following scenario, where both `# comment0` and `# comment1` +/// will move with the "a" element when the list is sorted: +/// +/// ```python +/// __all__ = [ +/// "b", +/// # comment0 +/// "a", "c", # comment1 +/// ] +/// ``` +/// +/// The desired outcome here is: +/// +/// ```python +/// __all__ = [ +/// # comment0 +/// "a", # comment1 +/// "b", +/// "c", +/// ] +/// ``` +/// +/// To achieve this, both `# comment0` and `# comment1` +/// are grouped into the `DunderAllItem` instance +/// where the value is `"a"`, even though the source-code range +/// of `# comment1` does not form a contiguous range with the +/// source-code range of `"a"`. #[derive(Clone, Debug)] struct DunderAllItem { value: String, From 768a3648636e6abbff2ed3373e686c2678b38dc1 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 18:50:04 +0000 Subject: [PATCH 34/77] Postpone an allocation until we actually need it --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 1390cd967d650..d42249f38aed5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -190,7 +190,6 @@ impl DunderAllValue { /// tuple/list after all. fn from_expr(value: &ast::Expr, locator: &Locator) -> Option { // Step (1): inspect the AST to check that we're looking at something vaguely sane: - let is_multiline = locator.contains_line_break(value.range()); let (elts, range) = match value { ast::Expr::List(ast::ExprList { elts, range, .. }) => (elts, range), ast::Expr::Tuple(ast::ExprTuple { elts, range, .. }) => (elts, range), @@ -238,7 +237,7 @@ impl DunderAllValue { Some(DunderAllValue { items, range: *range, - multiline: is_multiline, + multiline: locator.contains_line_break(value.range()), ends_with_trailing_comma, }) } From ea5270cab268c8aedea11774d241e4ec501266b1 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 19:19:04 +0000 Subject: [PATCH 35/77] more clarifications and typo fixes in comments/docs --- .../src/rules/ruff/rules/sort_dunder_all.rs | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index d42249f38aed5..a7c6541107a1f 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -72,7 +72,7 @@ impl Violation for UnsortedDunderAll { } } -/// Sort an `__all__` definition represented by a `StmtAssign` node. +/// Sort an `__all__` definition represented by a `StmtAssign` AST node. /// For example: `__all__ = ["b", "c", "a"]`. pub(crate) fn sort_dunder_all_assign( checker: &mut Checker, @@ -85,7 +85,7 @@ pub(crate) fn sort_dunder_all_assign( sort_dunder_all(checker, id, value, parent); } -/// Sort an `__all__` mutation represented by a `StmtAugAssign` node. +/// Sort an `__all__` mutation represented by a `StmtAugAssign` AST node. /// For example: `__all__ += ["b", "c", "a"]`. pub(crate) fn sort_dunder_all_aug_assign( checker: &mut Checker, @@ -107,7 +107,7 @@ pub(crate) fn sort_dunder_all_aug_assign( sort_dunder_all(checker, id, value, parent); } -/// Sort an `__all__` mutation represented by a `StmtAnnAssign` node. +/// Sort an `__all__` definition represented by a `StmtAnnAssign` AST node. /// For example: `__all__: list[str] = ["b", "c", "a"]`. pub(crate) fn sort_dunder_all_ann_assign( checker: &mut Checker, @@ -174,8 +174,9 @@ fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr, parent checker.diagnostics.push(diagnostic); } -/// Struct encapsulating an analysis of a Python tuple/list -/// that represents an `__all__` definition or augmentation. +/// An instance of this struct encapsulates an analysis +/// of a Python tuple/list that represents an `__all__` +/// definition or augmentation. struct DunderAllValue { items: Vec, range: TextRange, @@ -184,8 +185,8 @@ struct DunderAllValue { } impl DunderAllValue { - /// Analyses an AST node for a Python tuple/list that represents an `__all__` - /// definition or augmentation. Returns `None` if the analysis fails + /// Analyse an AST node for a Python tuple/list that represents an `__all__` + /// definition or augmentation. Return `None` if the analysis fails /// for whatever reason, or if it looks like we're not actually looking at a /// tuple/list after all. fn from_expr(value: &ast::Expr, locator: &Locator) -> Option { @@ -220,6 +221,8 @@ impl DunderAllValue { } // Step (2): parse the `__all__` definition using the raw tokens. + // See the docs for `collect_dunder_all_lines()` for why we have to + // use the raw tokens, rather than just the AST, to do this parsing. // // (2a). Start by collecting information on each line individually: let (lines, ends_with_trailing_comma) = collect_dunder_all_lines(*range, locator)?; @@ -258,8 +261,7 @@ impl DunderAllValue { /// Determine whether `__all__` is already sorted. /// If it is not already sorted, attempt to sort `__all__`, /// and return a string with the sorted `__all__ definition/augmentation` - /// that can be inserted into the source - /// code as a range replacement. + /// that can be inserted into the source code as a range replacement. fn into_sorted_source_code( self, locator: &Locator, @@ -314,6 +316,14 @@ impl DunderAllValue { // but `# comment3` becomes part of the postlude because there are no items // below it. // + // "Prelude" and "postlude" could both possibly be empty strings, for example + // in a situation like this, where there is neither an opening parenthesis + // nor a closing parenthesis: + // + // ```python + // __all__ = "foo", "bar", "baz" + // ``` + // let prelude_end = { let first_item_line_offset = locator.line_start(first_item.start()); if first_item_line_offset == locator.line_start(self.start()) { @@ -407,13 +417,13 @@ fn collect_dunder_all_lines( let mut comment_range_start = None; let mut comment_in_line = None; let mut ends_with_trailing_comma = false; - // lex_starts_at gives us absolute ranges rather than relative ranges, + // `lex_starts_at()` gives us absolute ranges rather than relative ranges, // but (surprisingly) we still need to pass in the slice of code we want it to lex, - // rather than the whole source file + // rather than the whole source file: for pair in lexer::lex_starts_at(locator.slice(range), Mode::Expression, range.start()) { let (tok, subrange) = pair.ok()?; match tok { - // If exactly one `Lpar`` or `Lsqb`` is encountered, that's fine + // If exactly one `Lpar` or `Lsqb` is encountered, that's fine // -- a valid __all__ definition has to be a list or tuple, // and most (though not all) lists/tuples start with either a `(` or a `[`. // @@ -487,7 +497,7 @@ fn collect_dunder_all_lines( /// Instances of this struct represent source-code lines in the middle /// of multiline `__all__` tuples/lists where the line contains -/// 0 elements of the tuple/list, but does have a comment in it. +/// 0 elements of the tuple/list, but the line does have a comment in it. #[derive(Debug)] struct LineWithJustAComment(TextRange); @@ -553,8 +563,8 @@ fn collect_dunder_all_items( != locator.line_start(dunder_all_range.start()) { // ...but for all other comments that precede an element, - // group them with that element into an "item", - // so that those comments move as one with the element + // group the comment with the element following that comment + // into an "item", so that the comment moves as one with the element // when the `__all__` list/tuple is sorted preceding_comment_range = Some(preceding_comment_range.map_or(comment_range, |range| { From 229fec17edf6aa1b1ed2c2a365b9e81e740908db Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 12 Jan 2024 19:32:53 +0000 Subject: [PATCH 36/77] Get rid of `.as_ref()` Co-authored-by: Andrew Gallant --- .../src/rules/ruff/rules/sort_dunder_all.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index a7c6541107a1f..f1abdeee0184d 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -93,15 +93,15 @@ pub(crate) fn sort_dunder_all_aug_assign( parent: &ast::Stmt, ) { let ast::StmtAugAssign { - value, - target, + ref value, + ref target, op: ast::Operator::Add, .. - } = node + } = *node else { return; }; - let ast::Expr::Name(ast::ExprName { id, .. }) = target.as_ref() else { + let ast::Expr::Name(ast::ExprName { ref id, .. }) = **target else { return; }; sort_dunder_all(checker, id, value, parent); @@ -115,14 +115,14 @@ pub(crate) fn sort_dunder_all_ann_assign( parent: &ast::Stmt, ) { let ast::StmtAnnAssign { - target, - value: Some(val), + ref target, + value: Some(ref val), .. } = node else { return; }; - let ast::Expr::Name(ast::ExprName { id, .. }) = target.as_ref() else { + let ast::Expr::Name(ast::ExprName { ref id, .. }) = **target else { return; }; sort_dunder_all(checker, id, val, parent); From f116550261748a967dc7708707ddc77cd652b228 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 13 Jan 2024 13:41:50 +0000 Subject: [PATCH 37/77] Also sort tuples/lists passed to `__all__.extend()` --- .../resources/test/fixtures/ruff/RUF022.py | 54 ++++++ .../src/checkers/ast/analyze/expression.rs | 3 + .../src/rules/ruff/rules/sort_dunder_all.rs | 58 +++++- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 175 +++++++++++++++++- 4 files changed, 279 insertions(+), 11 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index a98c87b0b90d1..043b3815a2835 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -95,6 +95,46 @@ "aadvark532" # the even longer whitespace span before this comment is retained ) +__all__.extend(["foo", "bar"]) +__all__.extend(("foo", "bar")) +__all__.extend(( # comment0 + # comment about foo + "foo", # comment about foo + # comment about bar + "bar" # comment about bar + # comment1 +)) # comment2 + +__all__.extend( # comment0 + # comment1 + ( # comment2 + # comment about foo + "foo", # comment about foo + # comment about bar + "bar" # comment about bar + # comment3 + ) # comment4 +) # comment2 + +__all__.extend([ # comment0 + # comment about foo + "foo", # comment about foo + # comment about bar + "bar" # comment about bar + # comment1 +]) # comment2 + +__all__.extend( # comment0 + # comment1 + [ # comment2 + # comment about foo + "foo", # comment about foo + # comment about bar + "bar" # comment about bar + # comment3 + ] # comment4 +) # comment2 + ################################### # These should all not get flagged: ################################### @@ -143,3 +183,17 @@ class IntroducesNonModuleScope: # it's very well ) # documented +__all__.append("foo") +__all__.extend(["bar", "foo"]) +__all__.extend((((["bar", "foo"])))) +__all__.extend([ + "bar", # comment0 + "foo" # comment1 +]) +__all__.extend(("bar", "foo")) +__all__.extend( + ( + "bar", + "foo" + ) +) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 680fc91b491c9..530138f55b4c0 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -968,6 +968,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { if checker.enabled(Rule::SslInsecureVersion) { flake8_bandit::rules::ssl_insecure_version(checker, call); } + if checker.enabled(Rule::UnsortedDunderAll) { + ruff::rules::sort_dunder_all_extend_call(checker, call); + } } Expr::Dict(dict) => { if checker.any_enabled(&[ diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index f1abdeee0184d..627caa5052b86 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -82,7 +82,7 @@ pub(crate) fn sort_dunder_all_assign( let [ast::Expr::Name(ast::ExprName { id, .. })] = targets.as_slice() else { return; }; - sort_dunder_all(checker, id, value, parent); + sort_dunder_all(checker, id, value, parent.range()); } /// Sort an `__all__` mutation represented by a `StmtAugAssign` AST node. @@ -104,7 +104,47 @@ pub(crate) fn sort_dunder_all_aug_assign( let ast::Expr::Name(ast::ExprName { ref id, .. }) = **target else { return; }; - sort_dunder_all(checker, id, value, parent); + sort_dunder_all(checker, id, value, parent.range()); +} + +/// Sort an `__all__` mutation from a call to `.extend()`. +pub(crate) fn sort_dunder_all_extend_call( + checker: &mut Checker, + call @ ast::ExprCall { + ref func, + arguments: ast::Arguments { args, keywords, .. }, + .. + }: &ast::ExprCall, +) { + let ([value_passed], []) = (args.as_slice(), keywords.as_slice()) else { + return; + }; + if let Some(name) = extract_name_dot_extend_was_called_on(func) { + let locator = checker.locator(); + let call_line = locator.line_start(call.start()); + let arg_line = locator.line_start(value_passed.start()); + let parent_range = if arg_line > call_line { + value_passed.range() + } else { + call.range() + }; + sort_dunder_all(checker, name, value_passed, parent_range); + } +} + +/// Given a Python call `x.extend()`, return `Some("x")`. +/// Return `None` if this wasn't actually a `.extend()` call after all. +fn extract_name_dot_extend_was_called_on(node: &ast::Expr) -> Option<&str> { + let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = node else { + return None; + }; + if attr.as_str() != "extend" { + return None; + } + let ast::Expr::Name(ast::ExprName { ref id, .. }) = **value else { + return None; + }; + Some(id) } /// Sort an `__all__` definition represented by a `StmtAnnAssign` AST node. @@ -125,10 +165,10 @@ pub(crate) fn sort_dunder_all_ann_assign( let ast::Expr::Name(ast::ExprName { ref id, .. }) = **target else { return; }; - sort_dunder_all(checker, id, val, parent); + sort_dunder_all(checker, id, val, parent.range()); } -fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr, parent: &ast::Stmt) { +fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr, parent_range: TextRange) { if target != "__all__" { return; } @@ -150,7 +190,7 @@ fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr, parent }; let new_dunder_all = - match dunder_all_val.into_sorted_source_code(locator, parent, checker.stylist()) { + match dunder_all_val.into_sorted_source_code(locator, parent_range, checker.stylist()) { SortedDunderAll::AlreadySorted => return, SortedDunderAll::Sorted(value) => value, }; @@ -265,7 +305,7 @@ impl DunderAllValue { fn into_sorted_source_code( self, locator: &Locator, - parent: &ast::Stmt, + parent_range: TextRange, stylist: &Stylist, ) -> SortedDunderAll { // As well as saving us unnecessary work, @@ -355,7 +395,7 @@ impl DunderAllValue { join_multiline_dunder_all_items( &sorted_items, locator, - parent, + parent_range, indentation, newline, self.ends_with_trailing_comma, @@ -687,12 +727,12 @@ fn join_singleline_dunder_all_items(sorted_items: &[DunderAllItem], locator: &Lo fn join_multiline_dunder_all_items( sorted_items: &[DunderAllItem], locator: &Locator, - parent: &ast::Stmt, + parent_range: TextRange, additional_indent: &str, newline: &str, needs_trailing_comma: bool, ) -> Option { - let indent = indentation(locator, parent)?; + let indent = indentation(locator, &parent_range)?; let mut new_dunder_all = String::new(); for (i, item) in sorted_items.iter().enumerate() { new_dunder_all.push_str(indent); diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index 5f798c8f3981c..5d174ede94331 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -448,7 +448,7 @@ RUF022.py:91:11: RUF022 [*] `__all__` is not sorted 96 | | ) | |_^ RUF022 97 | -98 | ################################### +98 | __all__.extend(["foo", "bar"]) | = help: Sort `__all__` according to a natural sort @@ -465,6 +465,177 @@ RUF022.py:91:11: RUF022 [*] `__all__` is not sorted 95 |+ "aadvark10092" 96 96 | ) 97 97 | -98 98 | ################################### +98 98 | __all__.extend(["foo", "bar"]) + +RUF022.py:98:16: RUF022 [*] `__all__` is not sorted + | + 96 | ) + 97 | + 98 | __all__.extend(["foo", "bar"]) + | ^^^^^^^^^^^^^^ RUF022 + 99 | __all__.extend(("foo", "bar")) +100 | __all__.extend(( # comment0 + | + = help: Sort `__all__` according to a natural sort + +ℹ Safe fix +95 95 | "aadvark532" # the even longer whitespace span before this comment is retained +96 96 | ) +97 97 | +98 |-__all__.extend(["foo", "bar"]) + 98 |+__all__.extend(["bar", "foo"]) +99 99 | __all__.extend(("foo", "bar")) +100 100 | __all__.extend(( # comment0 +101 101 | # comment about foo + +RUF022.py:99:16: RUF022 [*] `__all__` is not sorted + | + 98 | __all__.extend(["foo", "bar"]) + 99 | __all__.extend(("foo", "bar")) + | ^^^^^^^^^^^^^^ RUF022 +100 | __all__.extend(( # comment0 +101 | # comment about foo + | + = help: Sort `__all__` according to a natural sort + +ℹ Safe fix +96 96 | ) +97 97 | +98 98 | __all__.extend(["foo", "bar"]) +99 |-__all__.extend(("foo", "bar")) + 99 |+__all__.extend(("bar", "foo")) +100 100 | __all__.extend(( # comment0 +101 101 | # comment about foo +102 102 | "foo", # comment about foo + +RUF022.py:100:16: RUF022 [*] `__all__` is not sorted + | + 98 | __all__.extend(["foo", "bar"]) + 99 | __all__.extend(("foo", "bar")) +100 | __all__.extend(( # comment0 + | ________________^ +101 | | # comment about foo +102 | | "foo", # comment about foo +103 | | # comment about bar +104 | | "bar" # comment about bar +105 | | # comment1 +106 | | )) # comment2 + | |_^ RUF022 +107 | +108 | __all__.extend( # comment0 + | + = help: Sort `__all__` according to a natural sort + +ℹ Unsafe fix +98 98 | __all__.extend(["foo", "bar"]) +99 99 | __all__.extend(("foo", "bar")) +100 100 | __all__.extend(( # comment0 + 101 |+ # comment about bar + 102 |+ "bar", # comment about bar +101 103 | # comment about foo +102 |- "foo", # comment about foo +103 |- # comment about bar +104 |- "bar" # comment about bar + 104 |+ "foo" # comment about foo +105 105 | # comment1 +106 106 | )) # comment2 +107 107 | + +RUF022.py:110:5: RUF022 [*] `__all__` is not sorted + | +108 | __all__.extend( # comment0 +109 | # comment1 +110 | ( # comment2 + | _____^ +111 | | # comment about foo +112 | | "foo", # comment about foo +113 | | # comment about bar +114 | | "bar" # comment about bar +115 | | # comment3 +116 | | ) # comment4 + | |_____^ RUF022 +117 | ) # comment2 + | + = help: Sort `__all__` according to a natural sort + +ℹ Unsafe fix +108 108 | __all__.extend( # comment0 +109 109 | # comment1 +110 110 | ( # comment2 + 111 |+ # comment about bar + 112 |+ "bar", # comment about bar +111 113 | # comment about foo +112 |- "foo", # comment about foo +113 |- # comment about bar +114 |- "bar" # comment about bar + 114 |+ "foo" # comment about foo +115 115 | # comment3 +116 116 | ) # comment4 +117 117 | ) # comment2 + +RUF022.py:119:16: RUF022 [*] `__all__` is not sorted + | +117 | ) # comment2 +118 | +119 | __all__.extend([ # comment0 + | ________________^ +120 | | # comment about foo +121 | | "foo", # comment about foo +122 | | # comment about bar +123 | | "bar" # comment about bar +124 | | # comment1 +125 | | ]) # comment2 + | |_^ RUF022 +126 | +127 | __all__.extend( # comment0 + | + = help: Sort `__all__` according to a natural sort + +ℹ Unsafe fix +117 117 | ) # comment2 +118 118 | +119 119 | __all__.extend([ # comment0 + 120 |+ # comment about bar + 121 |+ "bar", # comment about bar +120 122 | # comment about foo +121 |- "foo", # comment about foo +122 |- # comment about bar +123 |- "bar" # comment about bar + 123 |+ "foo" # comment about foo +124 124 | # comment1 +125 125 | ]) # comment2 +126 126 | + +RUF022.py:129:5: RUF022 [*] `__all__` is not sorted + | +127 | __all__.extend( # comment0 +128 | # comment1 +129 | [ # comment2 + | _____^ +130 | | # comment about foo +131 | | "foo", # comment about foo +132 | | # comment about bar +133 | | "bar" # comment about bar +134 | | # comment3 +135 | | ] # comment4 + | |_____^ RUF022 +136 | ) # comment2 + | + = help: Sort `__all__` according to a natural sort + +ℹ Unsafe fix +127 127 | __all__.extend( # comment0 +128 128 | # comment1 +129 129 | [ # comment2 + 130 |+ # comment about bar + 131 |+ "bar", # comment about bar +130 132 | # comment about foo +131 |- "foo", # comment about foo +132 |- # comment about bar +133 |- "bar" # comment about bar + 133 |+ "foo" # comment about foo +134 134 | # comment3 +135 135 | ] # comment4 +136 136 | ) # comment2 From bec4a1b714f53fce67b98acaae7b922934537cc2 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 13 Jan 2024 14:18:51 +0000 Subject: [PATCH 38/77] =?UTF-8?q?use=20a=20=F0=9F=90=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 627caa5052b86..a4193b4d77696 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::cmp::Ordering; use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; @@ -380,9 +381,7 @@ impl DunderAllValue { last_item_line_offset } }; - let mut prelude = locator - .slice(TextRange::new(self.start(), prelude_end)) - .to_string(); + let mut prelude = Cow::Borrowed(locator.slice(TextRange::new(self.start(), prelude_end))); let postlude = locator.slice(TextRange::new(postlude_start, self.end())); let mut sorted_items = self.items; @@ -391,7 +390,7 @@ impl DunderAllValue { let joined_items = if self.multiline { let indentation = stylist.indentation(); let newline = stylist.line_ending().as_str(); - prelude = format!("{}{}", prelude.trim_end(), newline); + prelude = Cow::Owned(format!("{}{}", prelude.trim_end(), newline)); join_multiline_dunder_all_items( &sorted_items, locator, From 301bcd5e237644e62ccf9d89b7935baa13282432 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 13 Jan 2024 14:42:20 +0000 Subject: [PATCH 39/77] Huge cleanup by changing the way we calculate leading indentation --- .../src/checkers/ast/analyze/statement.rs | 6 +- .../src/rules/ruff/rules/sort_dunder_all.rs | 122 +++++++----------- 2 files changed, 49 insertions(+), 79 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index e57dab652d26a..cc72eb1eb6d93 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -1066,7 +1066,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } if checker.enabled(Rule::UnsortedDunderAll) { - ruff::rules::sort_dunder_all_aug_assign(checker, aug_assign, stmt); + ruff::rules::sort_dunder_all_aug_assign(checker, aug_assign); } } Stmt::If( @@ -1455,7 +1455,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { pylint::rules::type_bivariance(checker, value); } if checker.settings.rules.enabled(Rule::UnsortedDunderAll) { - ruff::rules::sort_dunder_all_assign(checker, assign, stmt); + ruff::rules::sort_dunder_all_assign(checker, assign); } if checker.source_type.is_stub() { if checker.any_enabled(&[ @@ -1528,7 +1528,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { pyupgrade::rules::non_pep695_type_alias(checker, assign_stmt); } if checker.settings.rules.enabled(Rule::UnsortedDunderAll) { - ruff::rules::sort_dunder_all_ann_assign(checker, assign_stmt, stmt); + ruff::rules::sort_dunder_all_ann_assign(checker, assign_stmt); } if checker.source_type.is_stub() { if let Some(value) = value { diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index a4193b4d77696..89be672806eec 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -1,14 +1,14 @@ use std::borrow::Cow; use std::cmp::Ordering; -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast as ast; -use ruff_python_ast::whitespace::indentation; use ruff_python_codegen::Stylist; use ruff_python_parser::{lexer, Mode, Tok}; +use ruff_python_trivia::leading_indentation; use ruff_source_file::Locator; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; @@ -60,16 +60,14 @@ use natord; #[violation] pub struct UnsortedDunderAll; -impl Violation for UnsortedDunderAll { - const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; - +impl AlwaysFixableViolation for UnsortedDunderAll { #[derive_message_formats] fn message(&self) -> String { format!("`__all__` is not sorted") } - fn fix_title(&self) -> Option { - Some("Sort `__all__` according to a natural sort".to_string()) + fn fix_title(&self) -> String { + "Sort `__all__` according to a natural sort".to_string() } } @@ -78,21 +76,16 @@ impl Violation for UnsortedDunderAll { pub(crate) fn sort_dunder_all_assign( checker: &mut Checker, ast::StmtAssign { value, targets, .. }: &ast::StmtAssign, - parent: &ast::Stmt, ) { let [ast::Expr::Name(ast::ExprName { id, .. })] = targets.as_slice() else { return; }; - sort_dunder_all(checker, id, value, parent.range()); + sort_dunder_all(checker, id, value); } /// Sort an `__all__` mutation represented by a `StmtAugAssign` AST node. /// For example: `__all__ += ["b", "c", "a"]`. -pub(crate) fn sort_dunder_all_aug_assign( - checker: &mut Checker, - node: &ast::StmtAugAssign, - parent: &ast::Stmt, -) { +pub(crate) fn sort_dunder_all_aug_assign(checker: &mut Checker, node: &ast::StmtAugAssign) { let ast::StmtAugAssign { ref value, ref target, @@ -105,13 +98,13 @@ pub(crate) fn sort_dunder_all_aug_assign( let ast::Expr::Name(ast::ExprName { ref id, .. }) = **target else { return; }; - sort_dunder_all(checker, id, value, parent.range()); + sort_dunder_all(checker, id, value); } /// Sort an `__all__` mutation from a call to `.extend()`. pub(crate) fn sort_dunder_all_extend_call( checker: &mut Checker, - call @ ast::ExprCall { + ast::ExprCall { ref func, arguments: ast::Arguments { args, keywords, .. }, .. @@ -121,15 +114,7 @@ pub(crate) fn sort_dunder_all_extend_call( return; }; if let Some(name) = extract_name_dot_extend_was_called_on(func) { - let locator = checker.locator(); - let call_line = locator.line_start(call.start()); - let arg_line = locator.line_start(value_passed.start()); - let parent_range = if arg_line > call_line { - value_passed.range() - } else { - call.range() - }; - sort_dunder_all(checker, name, value_passed, parent_range); + sort_dunder_all(checker, name, value_passed); } } @@ -150,11 +135,7 @@ fn extract_name_dot_extend_was_called_on(node: &ast::Expr) -> Option<&str> { /// Sort an `__all__` definition represented by a `StmtAnnAssign` AST node. /// For example: `__all__: list[str] = ["b", "c", "a"]`. -pub(crate) fn sort_dunder_all_ann_assign( - checker: &mut Checker, - node: &ast::StmtAnnAssign, - parent: &ast::Stmt, -) { +pub(crate) fn sort_dunder_all_ann_assign(checker: &mut Checker, node: &ast::StmtAnnAssign) { let ast::StmtAnnAssign { ref target, value: Some(ref val), @@ -166,10 +147,10 @@ pub(crate) fn sort_dunder_all_ann_assign( let ast::Expr::Name(ast::ExprName { ref id, .. }) = **target else { return; }; - sort_dunder_all(checker, id, val, parent.range()); + sort_dunder_all(checker, id, val); } -fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr, parent_range: TextRange) { +fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr) { if target != "__all__" { return; } @@ -190,29 +171,24 @@ fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr, parent return; }; - let new_dunder_all = - match dunder_all_val.into_sorted_source_code(locator, parent_range, checker.stylist()) { - SortedDunderAll::AlreadySorted => return, - SortedDunderAll::Sorted(value) => value, - }; - - let mut diagnostic = Diagnostic::new(UnsortedDunderAll, range); + let new_dunder_all = match dunder_all_val.into_sorted_source_code(locator, checker.stylist()) { + SortedDunderAll::AlreadySorted => return, + SortedDunderAll::Sorted(value) => value, + }; - if let Some(new_dunder_all) = new_dunder_all { - let applicability = { - if multiline && checker.indexer().comment_ranges().intersects(node.range()) { - Applicability::Unsafe - } else { - Applicability::Safe - } - }; - diagnostic.set_fix(Fix::applicable_edit( - Edit::range_replacement(new_dunder_all, range), - applicability, - )); - } + let applicability = { + if multiline && checker.indexer().comment_ranges().intersects(node.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + } + }; - checker.diagnostics.push(diagnostic); + let edit = Edit::range_replacement(new_dunder_all, range); + checker.diagnostics.push( + Diagnostic::new(UnsortedDunderAll, range) + .with_fix(Fix::applicable_edit(edit, applicability)), + ); } /// An instance of this struct encapsulates an analysis @@ -303,12 +279,7 @@ impl DunderAllValue { /// If it is not already sorted, attempt to sort `__all__`, /// and return a string with the sorted `__all__ definition/augmentation` /// that can be inserted into the source code as a range replacement. - fn into_sorted_source_code( - self, - locator: &Locator, - parent_range: TextRange, - stylist: &Stylist, - ) -> SortedDunderAll { + fn into_sorted_source_code(self, locator: &Locator, stylist: &Stylist) -> SortedDunderAll { // As well as saving us unnecessary work, // returning early here also means that we can rely on the invariant // throughout the rest of this function that both `items` and `sorted_items` @@ -384,6 +355,7 @@ impl DunderAllValue { let mut prelude = Cow::Borrowed(locator.slice(TextRange::new(self.start(), prelude_end))); let postlude = locator.slice(TextRange::new(postlude_start, self.end())); + let start_offset = self.start(); let mut sorted_items = self.items; sorted_items.sort(); @@ -394,17 +366,16 @@ impl DunderAllValue { join_multiline_dunder_all_items( &sorted_items, locator, - parent_range, + start_offset, indentation, newline, self.ends_with_trailing_comma, ) } else { - Some(join_singleline_dunder_all_items(&sorted_items, locator)) + join_singleline_dunder_all_items(&sorted_items, locator) }; - let new_dunder_all = joined_items.map(|items| format!("{prelude}{items}{postlude}")); - SortedDunderAll::Sorted(new_dunder_all) + SortedDunderAll::Sorted(format!("{prelude}{joined_items}{postlude}")) } } @@ -416,17 +387,16 @@ impl Ranged for DunderAllValue { /// Variants of this enum are returned by `into_sorted_source_code()`. /// -/// There are three possible states represented here: -/// (1) `__all__` was already sorted -/// (2) `__all__` was not already sorted, -/// but we couldn't figure out how to sort it safely -/// (3) `__all__` was not already sorted; -/// here's the source code to replace it with, -/// so that it becomes sorted! +/// - `SortedDunderAll::AlreadySorted` is returned if `__all__` was +/// already sorted; this means no code rewriting is required. +/// - `SortedDunderAll::Sorted` is returned if `__all__` was not already +/// sorted. The string data attached to this variant is the source +/// code of the sorted `__all__`, that can be inserted into the source +/// code as a `range_replacement` autofix. #[derive(Debug)] enum SortedDunderAll { AlreadySorted, - Sorted(Option), + Sorted(String), } /// Collect data on each line of `__all__`. @@ -726,12 +696,12 @@ fn join_singleline_dunder_all_items(sorted_items: &[DunderAllItem], locator: &Lo fn join_multiline_dunder_all_items( sorted_items: &[DunderAllItem], locator: &Locator, - parent_range: TextRange, + start_offset: TextSize, additional_indent: &str, newline: &str, needs_trailing_comma: bool, -) -> Option { - let indent = indentation(locator, &parent_range)?; +) -> String { + let indent = leading_indentation(locator.full_line(start_offset)); let mut new_dunder_all = String::new(); for (i, item) in sorted_items.iter().enumerate() { new_dunder_all.push_str(indent); @@ -748,5 +718,5 @@ fn join_multiline_dunder_all_items( new_dunder_all.push_str(newline); } } - Some(new_dunder_all) + new_dunder_all } From de871a488c255075b21fae34a444aa5986a0ea90 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 13 Jan 2024 14:59:43 +0000 Subject: [PATCH 40/77] nits --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 89be672806eec..03d2d919b68fe 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -124,7 +124,7 @@ fn extract_name_dot_extend_was_called_on(node: &ast::Expr) -> Option<&str> { let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = node else { return None; }; - if attr.as_str() != "extend" { + if attr != "extend" { return None; } let ast::Expr::Name(ast::ExprName { ref id, .. }) = **value else { @@ -185,6 +185,7 @@ fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr) { }; let edit = Edit::range_replacement(new_dunder_all, range); + checker.diagnostics.push( Diagnostic::new(UnsortedDunderAll, range) .with_fix(Fix::applicable_edit(edit, applicability)), @@ -289,7 +290,7 @@ impl DunderAllValue { return SortedDunderAll::AlreadySorted; } let [first_item, .., last_item] = self.items.as_slice() else { - panic!("Expected to have already returned if the list had < 2 items") + unreachable!("Expected to have already returned if the list had < 2 items") }; // As well as the "items" in the `__all__` definition, From 306cfd55676136347e01e549823d33095355d674 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 13 Jan 2024 15:44:25 +0000 Subject: [PATCH 41/77] Fix an edge case that caused weird indentation for a string element following a comment on its own line --- .../resources/test/fixtures/ruff/RUF022.py | 5 +++ .../src/rules/ruff/rules/sort_dunder_all.rs | 34 ++++++++++++++++--- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 34 ++++++++++++++++++- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index 043b3815a2835..739f1e1abece0 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -135,6 +135,11 @@ ] # comment4 ) # comment2 +__all__ = ["Style", "Treeview", + # Extensions + "LabeledScale", "OptionMenu", +] + ################################### # These should all not get flagged: ################################### diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 03d2d919b68fe..731f068a448cf 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -702,13 +702,37 @@ fn join_multiline_dunder_all_items( newline: &str, needs_trailing_comma: bool, ) -> String { - let indent = leading_indentation(locator.full_line(start_offset)); + let indent = format!( + "{}{}", + leading_indentation(locator.full_line(start_offset)), + additional_indent + ); + let max_index = sorted_items.len() - 1; + let mut new_dunder_all = String::new(); for (i, item) in sorted_items.iter().enumerate() { - new_dunder_all.push_str(indent); - new_dunder_all.push_str(additional_indent); - new_dunder_all.push_str(locator.slice(item)); - let is_final_item = i == (sorted_items.len() - 1); + let is_final_item = i == max_index; + + // Separate out the item into source lines again. + // + // The final line of any item must have exactly 1 element in it, + // but there could be any number of comments on their own line + // preceding that element that also count as part of this item. + // Separating them out again means we can ensure that all elements in + // `__all__` have consistent indentation. + let original_source = locator.slice(item); + let lines = original_source.split(newline).map(str::trim).collect_vec(); + let [preceding_comments @ .., element] = lines.as_slice() else { + panic!("Cannot pass an empty list as `sorted_items` to this function") + }; + + for comment_line in preceding_comments { + new_dunder_all.push_str(&indent); + new_dunder_all.push_str(comment_line); + new_dunder_all.push_str(newline); + } + new_dunder_all.push_str(&indent); + new_dunder_all.push_str(element); if !is_final_item || needs_trailing_comma { new_dunder_all.push(','); } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index 5d174ede94331..493f1ad15f18a 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -265,7 +265,7 @@ RUF022.py:44:11: RUF022 [*] `__all__` is not sorted 46 |+ "b", 47 |+ "d", # comment1 48 |+ # comment2 - 49 |+ "f", + 49 |+ "f", 50 |+ # comment4 49 51 | "formatted", 52 |+ "strangely", # comment3 @@ -638,4 +638,36 @@ RUF022.py:129:5: RUF022 [*] `__all__` is not sorted 135 135 | ] # comment4 136 136 | ) # comment2 +RUF022.py:138:11: RUF022 [*] `__all__` is not sorted + | +136 | ) # comment2 +137 | +138 | __all__ = ["Style", "Treeview", + | ___________^ +139 | | # Extensions +140 | | "LabeledScale", "OptionMenu", +141 | | ] + | |_^ RUF022 +142 | +143 | ################################### + | + = help: Sort `__all__` according to a natural sort + +ℹ Unsafe fix +135 135 | ] # comment4 +136 136 | ) # comment2 +137 137 | +138 |-__all__ = ["Style", "Treeview", +139 |- # Extensions +140 |- "LabeledScale", "OptionMenu", + 138 |+__all__ = [ + 139 |+ # Extensions + 140 |+ "LabeledScale", + 141 |+ "OptionMenu", + 142 |+ "Style", + 143 |+ "Treeview", +141 144 | ] +142 145 | +143 146 | ################################### + From 35f2ee53be058fb14942ebf0efcf7c78264c3fb3 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 13 Jan 2024 16:20:08 +0000 Subject: [PATCH 42/77] fix an edge case that caused weird indentation for the closing bracket --- .../resources/test/fixtures/ruff/RUF022.py | 4 +++ .../src/rules/ruff/rules/sort_dunder_all.rs | 30 +++++++++---------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index 739f1e1abece0..87c5afd80a7cf 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -140,6 +140,10 @@ "LabeledScale", "OptionMenu", ] +__all__ = ["Awaitable", "Coroutine", + "AsyncIterable", "AsyncIterator", "AsyncGenerator", + ] + ################################### # These should all not get flagged: ################################### diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 731f068a448cf..4a8a633da0d54 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -8,7 +8,7 @@ use ruff_python_codegen::Stylist; use ruff_python_parser::{lexer, Mode, Tok}; use ruff_python_trivia::leading_indentation; use ruff_source_file::Locator; -use ruff_text_size::{Ranged, TextRange, TextSize}; +use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -323,7 +323,7 @@ impl DunderAllValue { // (an "item" being a region of source code that all moves as one unit // when `__all__` is sorted). // - The postlude in the above example is the source code region starting - // just after `# comment2` and ending just before the lgoical newline + // just after `# comment2` and ending just before the logical newline // that follows the closing paren. `# comment2` is part of the last item, // as it's an inline comment on the same line as an element, // but `# comment3` becomes part of the postlude because there are no items @@ -354,21 +354,27 @@ impl DunderAllValue { } }; let mut prelude = Cow::Borrowed(locator.slice(TextRange::new(self.start(), prelude_end))); - let postlude = locator.slice(TextRange::new(postlude_start, self.end())); + let mut postlude = Cow::Borrowed(locator.slice(TextRange::new(postlude_start, self.end()))); let start_offset = self.start(); let mut sorted_items = self.items; sorted_items.sort(); let joined_items = if self.multiline { - let indentation = stylist.indentation(); + let leading_indent = leading_indentation(locator.full_line(start_offset)); + let item_indent = format!("{}{}", leading_indent, stylist.indentation().as_str()); let newline = stylist.line_ending().as_str(); prelude = Cow::Owned(format!("{}{}", prelude.trim_end(), newline)); + if postlude.starts_with(newline) { + let trimmed_postlude = postlude.trim_start(); + if trimmed_postlude.starts_with(']') || trimmed_postlude.starts_with(')') { + postlude = Cow::Owned(format!("{newline}{leading_indent}{trimmed_postlude}")); + } + } join_multiline_dunder_all_items( &sorted_items, locator, - start_offset, - indentation, + &item_indent, newline, self.ends_with_trailing_comma, ) @@ -697,16 +703,10 @@ fn join_singleline_dunder_all_items(sorted_items: &[DunderAllItem], locator: &Lo fn join_multiline_dunder_all_items( sorted_items: &[DunderAllItem], locator: &Locator, - start_offset: TextSize, - additional_indent: &str, + item_indent: &str, newline: &str, needs_trailing_comma: bool, ) -> String { - let indent = format!( - "{}{}", - leading_indentation(locator.full_line(start_offset)), - additional_indent - ); let max_index = sorted_items.len() - 1; let mut new_dunder_all = String::new(); @@ -727,11 +727,11 @@ fn join_multiline_dunder_all_items( }; for comment_line in preceding_comments { - new_dunder_all.push_str(&indent); + new_dunder_all.push_str(item_indent); new_dunder_all.push_str(comment_line); new_dunder_all.push_str(newline); } - new_dunder_all.push_str(&indent); + new_dunder_all.push_str(item_indent); new_dunder_all.push_str(element); if !is_final_item || needs_trailing_comma { new_dunder_all.push(','); From f5cdcf2c9d2d400edaea2dbfa2489aaac37daa01 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 13 Jan 2024 16:43:55 +0000 Subject: [PATCH 43/77] more edge cases --- .../resources/test/fixtures/ruff/RUF022.py | 6 ++ .../src/rules/ruff/rules/sort_dunder_all.rs | 35 ++++++++-- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 64 ++++++++++++++++++- 3 files changed, 96 insertions(+), 9 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index 87c5afd80a7cf..fbfdda3188a0c 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -144,6 +144,12 @@ "AsyncIterable", "AsyncIterator", "AsyncGenerator", ] +__all__ = [ + "foo", + "bar", + "baz", + ] + ################################### # These should all not get flagged: ################################### diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 4a8a633da0d54..e4d8a002239a4 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -8,7 +8,7 @@ use ruff_python_codegen::Stylist; use ruff_python_parser::{lexer, Mode, Tok}; use ruff_python_trivia::leading_indentation; use ruff_source_file::Locator; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; @@ -365,12 +365,7 @@ impl DunderAllValue { let item_indent = format!("{}{}", leading_indent, stylist.indentation().as_str()); let newline = stylist.line_ending().as_str(); prelude = Cow::Owned(format!("{}{}", prelude.trim_end(), newline)); - if postlude.starts_with(newline) { - let trimmed_postlude = postlude.trim_start(); - if trimmed_postlude.starts_with(']') || trimmed_postlude.starts_with(')') { - postlude = Cow::Owned(format!("{newline}{leading_indent}{trimmed_postlude}")); - } - } + postlude = fixup_postlude(postlude, newline, leading_indent, &item_indent); join_multiline_dunder_all_items( &sorted_items, locator, @@ -386,6 +381,32 @@ impl DunderAllValue { } } +/// Fixup the postlude for a multiline `__all__` definition. +/// +/// Without the fixup, closing `)` or `]` characters +/// at the end of sorted `__all__` definitions can sometimes +/// have strange indentations. +fn fixup_postlude<'a>( + postlude: Cow<'a, str>, + newline: &str, + leading_indent: &str, + item_indent: &str, +) -> Cow<'a, str> { + if !postlude.starts_with(newline) { + return postlude; + } + if TextSize::of(leading_indentation(postlude.trim_start_matches(newline))) + <= TextSize::of(item_indent) + { + return postlude; + } + let trimmed_postlude = postlude.trim_start(); + if trimmed_postlude.starts_with(']') || trimmed_postlude.starts_with(')') { + return Cow::Owned(format!("{newline}{leading_indent}{trimmed_postlude}")); + } + postlude +} + impl Ranged for DunderAllValue { fn range(&self) -> TextRange { self.range diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index 493f1ad15f18a..1975474394ebd 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -649,7 +649,7 @@ RUF022.py:138:11: RUF022 [*] `__all__` is not sorted 141 | | ] | |_^ RUF022 142 | -143 | ################################### +143 | __all__ = ["Awaitable", "Coroutine", | = help: Sort `__all__` according to a natural sort @@ -668,6 +668,66 @@ RUF022.py:138:11: RUF022 [*] `__all__` is not sorted 143 |+ "Treeview", 141 144 | ] 142 145 | -143 146 | ################################### +143 146 | __all__ = ["Awaitable", "Coroutine", + +RUF022.py:143:11: RUF022 [*] `__all__` is not sorted + | +141 | ] +142 | +143 | __all__ = ["Awaitable", "Coroutine", + | ___________^ +144 | | "AsyncIterable", "AsyncIterator", "AsyncGenerator", +145 | | ] + | |____________^ RUF022 +146 | +147 | __all__ = [ + | + = help: Sort `__all__` according to a natural sort + +ℹ Safe fix +140 140 | "LabeledScale", "OptionMenu", +141 141 | ] +142 142 | +143 |-__all__ = ["Awaitable", "Coroutine", +144 |- "AsyncIterable", "AsyncIterator", "AsyncGenerator", +145 |- ] + 143 |+__all__ = [ + 144 |+ "AsyncGenerator", + 145 |+ "AsyncIterable", + 146 |+ "AsyncIterator", + 147 |+ "Awaitable", + 148 |+ "Coroutine", + 149 |+] +146 150 | +147 151 | __all__ = [ +148 152 | "foo", + +RUF022.py:147:11: RUF022 [*] `__all__` is not sorted + | +145 | ] +146 | +147 | __all__ = [ + | ___________^ +148 | | "foo", +149 | | "bar", +150 | | "baz", +151 | | ] + | |_____^ RUF022 +152 | +153 | ################################### + | + = help: Sort `__all__` according to a natural sort + +ℹ Safe fix +145 145 | ] +146 146 | +147 147 | __all__ = [ +148 |- "foo", +149 148 | "bar", +150 149 | "baz", + 150 |+ "foo", +151 151 | ] +152 152 | +153 153 | ################################### From cdf7845b74a9784345fe24256238146488e5d5eb Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 13 Jan 2024 17:58:32 +0000 Subject: [PATCH 44/77] Use an 'isort-style' sort, not a natural sort --- .../resources/test/fixtures/ruff/RUF022.py | 38 +++++ .../src/rules/ruff/rules/sort_dunder_all.rs | 83 +++++++++-- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 133 ++++++++++++++---- 3 files changed, 214 insertions(+), 40 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index fbfdda3188a0c..6fe5eef380b5a 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -150,6 +150,44 @@ "baz", ] +# we implement an "isort-style sort": +# SCEAMING_CASE constants first, +# then CamelCase classes, +# then anything thats lowercase_snake_case. +# This (which is currently alphabetically sorted) +# should get reordered accordingly: +__all__ = [ + "APRIL", + "AUGUST", + "Calendar", + "DECEMBER", + "Day", + "FEBRUARY", + "FRIDAY", + "HTMLCalendar", + "IllegalMonthError", + "JANUARY", + "JULY", + "JUNE", + "LocaleHTMLCalendar", + "MARCH", + "MAY", + "MONDAY", + "Month", + "NOVEMBER", + "OCTOBER", + "SATURDAY", + "SEPTEMBER", + "SUNDAY", + "THURSDAY", + "TUESDAY", + "TextCalendar", + "WEDNESDAY", + "calendar", + "timegm", + "weekday", + "weekheader"] + ################################### # These should all not get flagged: ################################### diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index e4d8a002239a4..98b3491b71228 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -6,6 +6,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast as ast; use ruff_python_codegen::Stylist; use ruff_python_parser::{lexer, Mode, Tok}; +use ruff_python_stdlib::str::is_cased_uppercase; use ruff_python_trivia::leading_indentation; use ruff_source_file::Locator; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -17,7 +18,14 @@ use natord; /// ## What it does /// Checks for `__all__` definitions that are not ordered -/// according to a [natural sort](https://en.wikipedia.org/wiki/Natural_sort_order). +/// according to an "isort-style" sort. +/// +/// An isort-style sort sorts items first according to their casing: +/// SCREAMING_SNAKE_CASE names (conventionally used for global constants) +/// come first, followed by CamelCase names (conventionally) used for +/// classes), followed by anything else. Within each category, +/// a [natural sort](https://en.wikipedia.org/wiki/Natural_sort_order) +/// is used to order the elements. /// /// ## Why is this bad? /// Consistency is good. Use a common convention for `__all__` to make your @@ -67,7 +75,7 @@ impl AlwaysFixableViolation for UnsortedDunderAll { } fn fix_title(&self) -> String { - "Sort `__all__` according to a natural sort".to_string() + "Sort `__all__` according to an isort-style sort".to_string() } } @@ -621,20 +629,15 @@ fn collect_dunder_all_items( let range = preceding_comment_range.map_or(first_range, |r| { TextRange::new(r.start(), first_range.end()) }); - all_items.push(DunderAllItem { - value: first_val, - original_index: all_items.len(), + all_items.push(DunderAllItem::new( + first_val, + all_items.len(), range, - end_of_line_comments: comment_range, - }); + comment_range, + )); preceding_comment_range = None; for (value, range) in owned_items { - all_items.push(DunderAllItem { - value, - original_index: all_items.len(), - range, - end_of_line_comments: None, - }); + all_items.push(DunderAllItem::new(value, all_items.len(), range, None)); } } } @@ -642,6 +645,37 @@ fn collect_dunder_all_items( all_items } +/// Classification for an element in `__all__`. +/// +/// This is necessary to achieve an "isort-style" sort, +/// where elements are sorted first by category, +/// then, within categories, are sorted according +/// to a natural sort. +/// +/// You'll notice that a very similar enum exists +/// in ruff's reimplementation of isort. +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone)] +enum InferredMemberType { + Constant, + Class, + Other, +} + +impl InferredMemberType { + fn of(value: &str) -> Self { + // E.g. `CONSTANT` + if value.len() > 1 && is_cased_uppercase(value) { + Self::Constant + // E.g. `Class` + } else if value.chars().next().is_some_and(char::is_uppercase) { + Self::Class + // E.g. `some_variable` or `some_function` + } else { + Self::Other + } + } +} + /// An instance of this struct represents a single element /// from the original tuple/list, *and* any comments that /// are "attached" to it. The comments "attached" to the element @@ -680,6 +714,7 @@ fn collect_dunder_all_items( #[derive(Clone, Debug)] struct DunderAllItem { value: String, + category: InferredMemberType, // Each `AllItem` in any given list should have a unique `original_index`: original_index: usize, // Note that this range might include comments, etc. @@ -687,6 +722,24 @@ struct DunderAllItem { end_of_line_comments: Option, } +impl DunderAllItem { + fn new( + value: String, + original_index: usize, + range: TextRange, + end_of_line_comments: Option, + ) -> Self { + let category = InferredMemberType::of(value.as_str()); + Self { + value, + category, + original_index, + range, + end_of_line_comments, + } + } +} + impl Ranged for DunderAllItem { fn range(&self) -> TextRange { self.range @@ -703,7 +756,9 @@ impl Eq for DunderAllItem {} impl Ord for DunderAllItem { fn cmp(&self, other: &Self) -> Ordering { - natord::compare(&self.value, &other.value) + self.category + .cmp(&other.category) + .then_with(|| natord::compare(&self.value, &other.value)) .then_with(|| self.original_index.cmp(&other.original_index)) } } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index 1975474394ebd..9efcf4de099ad 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -10,7 +10,7 @@ RUF022.py:5:11: RUF022 [*] `__all__` is not sorted 6 | __all__ += ["foo", "bar", "antipasti"] 7 | __all__ = ("d", "c", "b", "a") | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Safe fix 2 2 | # Single-line __all__ definitions (nice 'n' easy!) @@ -29,7 +29,7 @@ RUF022.py:6:12: RUF022 [*] `__all__` is not sorted | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF022 7 | __all__ = ("d", "c", "b", "a") | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Safe fix 3 3 | ################################################## @@ -50,7 +50,7 @@ RUF022.py:7:11: RUF022 [*] `__all__` is not sorted 8 | 9 | __all__: list = ["b", "c", "a",] # note the trailing comma, which is retained | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Safe fix 4 4 | @@ -70,7 +70,7 @@ RUF022.py:9:17: RUF022 [*] `__all__` is not sorted | ^^^^^^^^^^^^^^^^ RUF022 10 | __all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Safe fix 6 6 | __all__ += ["foo", "bar", "antipasti"] @@ -90,7 +90,7 @@ RUF022.py:10:18: RUF022 [*] `__all__` is not sorted 11 | 12 | if bool(): | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Safe fix 7 7 | __all__ = ("d", "c", "b", "a") @@ -110,7 +110,7 @@ RUF022.py:13:16: RUF022 [*] `__all__` is not sorted 14 | else: 15 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Safe fix 10 10 | __all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained @@ -131,7 +131,7 @@ RUF022.py:15:16: RUF022 [*] `__all__` is not sorted 16 | 17 | __all__: list[str] = ["the", "three", "little", "pigs"] | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Safe fix 12 12 | if bool(): @@ -152,7 +152,7 @@ RUF022.py:17:22: RUF022 [*] `__all__` is not sorted 18 | 19 | #################################### | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Safe fix 14 14 | else: @@ -180,7 +180,7 @@ RUF022.py:23:11: RUF022 [*] `__all__` is not sorted 30 | 31 | __all__ = [ | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Unsafe fix 21 21 | #################################### @@ -215,7 +215,7 @@ RUF022.py:31:11: RUF022 [*] `__all__` is not sorted 38 | 39 | ########################################## | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Unsafe fix 29 29 | ) @@ -249,7 +249,7 @@ RUF022.py:44:11: RUF022 [*] `__all__` is not sorted | |_^ RUF022 52 | # comment7 | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Unsafe fix 41 41 | ########################################## @@ -290,7 +290,7 @@ RUF022.py:54:11: RUF022 [*] `__all__` is not sorted 62 | 63 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Unsafe fix 52 52 | # comment7 @@ -330,7 +330,7 @@ RUF022.py:63:11: RUF022 [*] `__all__` is not sorted 77 | 78 | __all__: tuple[str, ...] = ( # a comment about the opening paren | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Safe fix 60 60 | # comment6 @@ -416,7 +416,7 @@ RUF022.py:78:28: RUF022 [*] `__all__` is not sorted 86 | 87 | # we use natural sort for `__all__`, | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Unsafe fix 76 76 | "register_error", "lookup_error"] @@ -450,7 +450,7 @@ RUF022.py:91:11: RUF022 [*] `__all__` is not sorted 97 | 98 | __all__.extend(["foo", "bar"]) | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Unsafe fix 89 89 | # Also, this doesn't end with a trailing comma, @@ -476,7 +476,7 @@ RUF022.py:98:16: RUF022 [*] `__all__` is not sorted 99 | __all__.extend(("foo", "bar")) 100 | __all__.extend(( # comment0 | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Safe fix 95 95 | "aadvark532" # the even longer whitespace span before this comment is retained @@ -496,7 +496,7 @@ RUF022.py:99:16: RUF022 [*] `__all__` is not sorted 100 | __all__.extend(( # comment0 101 | # comment about foo | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Safe fix 96 96 | ) @@ -524,7 +524,7 @@ RUF022.py:100:16: RUF022 [*] `__all__` is not sorted 107 | 108 | __all__.extend( # comment0 | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Unsafe fix 98 98 | __all__.extend(["foo", "bar"]) @@ -556,7 +556,7 @@ RUF022.py:110:5: RUF022 [*] `__all__` is not sorted | |_____^ RUF022 117 | ) # comment2 | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Unsafe fix 108 108 | __all__.extend( # comment0 @@ -589,7 +589,7 @@ RUF022.py:119:16: RUF022 [*] `__all__` is not sorted 126 | 127 | __all__.extend( # comment0 | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Unsafe fix 117 117 | ) # comment2 @@ -621,7 +621,7 @@ RUF022.py:129:5: RUF022 [*] `__all__` is not sorted | |_____^ RUF022 136 | ) # comment2 | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Unsafe fix 127 127 | __all__.extend( # comment0 @@ -651,7 +651,7 @@ RUF022.py:138:11: RUF022 [*] `__all__` is not sorted 142 | 143 | __all__ = ["Awaitable", "Coroutine", | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Unsafe fix 135 135 | ] # comment4 @@ -682,7 +682,7 @@ RUF022.py:143:11: RUF022 [*] `__all__` is not sorted 146 | 147 | __all__ = [ | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Safe fix 140 140 | "LabeledScale", "OptionMenu", @@ -714,9 +714,9 @@ RUF022.py:147:11: RUF022 [*] `__all__` is not sorted 151 | | ] | |_____^ RUF022 152 | -153 | ################################### +153 | # we implement an "isort-style sort": | - = help: Sort `__all__` according to a natural sort + = help: Sort `__all__` according to an isort-style sort ℹ Safe fix 145 145 | ] @@ -728,6 +728,87 @@ RUF022.py:147:11: RUF022 [*] `__all__` is not sorted 150 |+ "foo", 151 151 | ] 152 152 | -153 153 | ################################### +153 153 | # we implement an "isort-style sort": + +RUF022.py:159:11: RUF022 [*] `__all__` is not sorted + | +157 | # This (which is currently alphabetically sorted) +158 | # should get reordered accordingly: +159 | __all__ = [ + | ___________^ +160 | | "APRIL", +161 | | "AUGUST", +162 | | "Calendar", +163 | | "DECEMBER", +164 | | "Day", +165 | | "FEBRUARY", +166 | | "FRIDAY", +167 | | "HTMLCalendar", +168 | | "IllegalMonthError", +169 | | "JANUARY", +170 | | "JULY", +171 | | "JUNE", +172 | | "LocaleHTMLCalendar", +173 | | "MARCH", +174 | | "MAY", +175 | | "MONDAY", +176 | | "Month", +177 | | "NOVEMBER", +178 | | "OCTOBER", +179 | | "SATURDAY", +180 | | "SEPTEMBER", +181 | | "SUNDAY", +182 | | "THURSDAY", +183 | | "TUESDAY", +184 | | "TextCalendar", +185 | | "WEDNESDAY", +186 | | "calendar", +187 | | "timegm", +188 | | "weekday", +189 | | "weekheader"] + | |_________________^ RUF022 +190 | +191 | ################################### + | + = help: Sort `__all__` according to an isort-style sort + +ℹ Safe fix +159 159 | __all__ = [ +160 160 | "APRIL", +161 161 | "AUGUST", +162 |- "Calendar", +163 162 | "DECEMBER", +164 |- "Day", +165 163 | "FEBRUARY", +166 164 | "FRIDAY", +167 |- "HTMLCalendar", +168 |- "IllegalMonthError", +169 165 | "JANUARY", +170 166 | "JULY", +171 167 | "JUNE", +172 |- "LocaleHTMLCalendar", +173 168 | "MARCH", +174 169 | "MAY", +175 170 | "MONDAY", +176 |- "Month", +177 171 | "NOVEMBER", +178 172 | "OCTOBER", +179 173 | "SATURDAY", +-------------------------------------------------------------------------------- +181 175 | "SUNDAY", +182 176 | "THURSDAY", +183 177 | "TUESDAY", + 178 |+ "WEDNESDAY", + 179 |+ "Calendar", + 180 |+ "Day", + 181 |+ "HTMLCalendar", + 182 |+ "IllegalMonthError", + 183 |+ "LocaleHTMLCalendar", + 184 |+ "Month", +184 185 | "TextCalendar", +185 |- "WEDNESDAY", +186 186 | "calendar", +187 187 | "timegm", +188 188 | "weekday", From 559ce7a5a3cc815c2012ad49533f9476bb12ea49 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 13 Jan 2024 18:07:27 +0000 Subject: [PATCH 45/77] `DunderAllItem` should not be clonable --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 98b3491b71228..aaae0a5b6fb1b 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -711,7 +711,7 @@ impl InferredMemberType { /// where the value is `"a"`, even though the source-code range /// of `# comment1` does not form a contiguous range with the /// source-code range of `"a"`. -#[derive(Clone, Debug)] +#[derive(Debug)] struct DunderAllItem { value: String, category: InferredMemberType, From e75253188ca1a7517e30c58313084540a955457f Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 13 Jan 2024 18:12:33 +0000 Subject: [PATCH 46/77] `panic()` -> `unreachable()` --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index aaae0a5b6fb1b..0edb7db9e12d6 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -799,7 +799,10 @@ fn join_multiline_dunder_all_items( let original_source = locator.slice(item); let lines = original_source.split(newline).map(str::trim).collect_vec(); let [preceding_comments @ .., element] = lines.as_slice() else { - panic!("Cannot pass an empty list as `sorted_items` to this function") + unreachable!( + "It should be impossible for an item in `__all__` + to have the empty string as its source code" + ) }; for comment_line in preceding_comments { From 29de09b8e2d47b1944535dde9a0ccaec298ccab2 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 13 Jan 2024 22:45:07 +0000 Subject: [PATCH 47/77] Remove the need for an `unreachable!()` call --- .../src/rules/ruff/rules/sort_dunder_all.rs | 81 +++++++++---------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 0edb7db9e12d6..b34c93175ce39 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -22,7 +22,7 @@ use natord; /// /// An isort-style sort sorts items first according to their casing: /// SCREAMING_SNAKE_CASE names (conventionally used for global constants) -/// come first, followed by CamelCase names (conventionally) used for +/// come first, followed by CamelCase names (conventionally used for /// classes), followed by anything else. Within each category, /// a [natural sort](https://en.wikipedia.org/wiki/Natural_sort_order) /// is used to order the elements. @@ -557,18 +557,18 @@ struct LineWithItems { // (We need to know the actual value so that we can sort the items.) items: Vec<(String, TextRange)>, // For comments, we only need to keep track of the source-code range. - comment_range: Option, + trailing_comment_range: Option, } impl LineWithItems { - fn new(items: Vec<(String, TextRange)>, comment_range: Option) -> Self { + fn new(items: Vec<(String, TextRange)>, trailing_comment_range: Option) -> Self { assert!( !items.is_empty(), "Use the 'JustAComment' variant to represent lines with 0 items" ); Self { items, - comment_range, + trailing_comment_range, } } } @@ -597,7 +597,7 @@ fn collect_dunder_all_items( _ => lines.len(), }); let mut first_item_encountered = false; - let mut preceding_comment_range: Option = None; + let mut preceding_comment_ranges = vec![]; for line in lines { match line { DunderAllLine::JustAComment(LineWithJustAComment(comment_range)) => { @@ -611,33 +611,31 @@ fn collect_dunder_all_items( // group the comment with the element following that comment // into an "item", so that the comment moves as one with the element // when the `__all__` list/tuple is sorted - preceding_comment_range = - Some(preceding_comment_range.map_or(comment_range, |range| { - TextRange::new(range.start(), comment_range.end()) - })); + preceding_comment_ranges.push(comment_range); } } DunderAllLine::OneOrMoreItems(LineWithItems { items, - comment_range, + trailing_comment_range: comment_range, }) => { first_item_encountered = true; let mut owned_items = items.into_iter(); let (first_val, first_range) = owned_items .next() .expect("LineWithItems::new() should uphold the invariant that this list is always non-empty"); - let range = preceding_comment_range.map_or(first_range, |r| { - TextRange::new(r.start(), first_range.end()) - }); all_items.push(DunderAllItem::new( first_val, all_items.len(), - range, + std::mem::take(&mut preceding_comment_ranges), + first_range, comment_range, )); - preceding_comment_range = None; for (value, range) in owned_items { - all_items.push(DunderAllItem::new(value, all_items.len(), range, None)); + all_items.push(DunderAllItem::with_no_comments( + value, + all_items.len(), + range, + )); } } } @@ -717,8 +715,12 @@ struct DunderAllItem { category: InferredMemberType, // Each `AllItem` in any given list should have a unique `original_index`: original_index: usize, - // Note that this range might include comments, etc. - range: TextRange, + preceding_comment_ranges: Vec, + element_range: TextRange, + // total_range incorporates the ranges of preceding comments + // (which must be contiguous with the element), + // but doesn't incorporate any trailing comments + total_range: TextRange, end_of_line_comments: Option, } @@ -726,23 +728,37 @@ impl DunderAllItem { fn new( value: String, original_index: usize, - range: TextRange, + preceding_comment_ranges: Vec, + element_range: TextRange, end_of_line_comments: Option, ) -> Self { let category = InferredMemberType::of(value.as_str()); + let total_range = { + if let Some(first_comment_range) = preceding_comment_ranges.first() { + TextRange::new(first_comment_range.start(), element_range.end()) + } else { + element_range + } + }; Self { value, category, original_index, - range, + preceding_comment_ranges, + element_range, + total_range, end_of_line_comments, } } + + fn with_no_comments(value: String, original_index: usize, element_range: TextRange) -> Self { + Self::new(value, original_index, vec![], element_range, None) + } } impl Ranged for DunderAllItem { fn range(&self) -> TextRange { - self.range + self.total_range } } @@ -788,30 +804,13 @@ fn join_multiline_dunder_all_items( let mut new_dunder_all = String::new(); for (i, item) in sorted_items.iter().enumerate() { let is_final_item = i == max_index; - - // Separate out the item into source lines again. - // - // The final line of any item must have exactly 1 element in it, - // but there could be any number of comments on their own line - // preceding that element that also count as part of this item. - // Separating them out again means we can ensure that all elements in - // `__all__` have consistent indentation. - let original_source = locator.slice(item); - let lines = original_source.split(newline).map(str::trim).collect_vec(); - let [preceding_comments @ .., element] = lines.as_slice() else { - unreachable!( - "It should be impossible for an item in `__all__` - to have the empty string as its source code" - ) - }; - - for comment_line in preceding_comments { + for comment_range in &item.preceding_comment_ranges { new_dunder_all.push_str(item_indent); - new_dunder_all.push_str(comment_line); + new_dunder_all.push_str(locator.slice(comment_range)); new_dunder_all.push_str(newline); } new_dunder_all.push_str(item_indent); - new_dunder_all.push_str(element); + new_dunder_all.push_str(locator.slice(item.element_range)); if !is_final_item || needs_trailing_comma { new_dunder_all.push(','); } From f10bfc4de2e942b9834f3d440633f99d5437db64 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 13 Jan 2024 22:48:38 +0000 Subject: [PATCH 48/77] nits --- .../resources/test/fixtures/ruff/RUF022.py | 76 +- .../src/rules/ruff/rules/sort_dunder_all.rs | 2 +- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 1036 ++++++++--------- 3 files changed, 557 insertions(+), 557 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index 6fe5eef380b5a..44024111e185f 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -36,6 +36,44 @@ "a" ] +# we implement an "isort-style sort": +# SCEAMING_CASE constants first, +# then CamelCase classes, +# then anything thats lowercase_snake_case. +# This (which is currently alphabetically sorted) +# should get reordered accordingly: +__all__ = [ + "APRIL", + "AUGUST", + "Calendar", + "DECEMBER", + "Day", + "FEBRUARY", + "FRIDAY", + "HTMLCalendar", + "IllegalMonthError", + "JANUARY", + "JULY", + "JUNE", + "LocaleHTMLCalendar", + "MARCH", + "MAY", + "MONDAY", + "Month", + "NOVEMBER", + "OCTOBER", + "SATURDAY", + "SEPTEMBER", + "SUNDAY", + "THURSDAY", + "TUESDAY", + "TextCalendar", + "WEDNESDAY", + "calendar", + "timegm", + "weekday", + "weekheader"] + ########################################## # Messier multiline __all__ definitions... ########################################## @@ -150,44 +188,6 @@ "baz", ] -# we implement an "isort-style sort": -# SCEAMING_CASE constants first, -# then CamelCase classes, -# then anything thats lowercase_snake_case. -# This (which is currently alphabetically sorted) -# should get reordered accordingly: -__all__ = [ - "APRIL", - "AUGUST", - "Calendar", - "DECEMBER", - "Day", - "FEBRUARY", - "FRIDAY", - "HTMLCalendar", - "IllegalMonthError", - "JANUARY", - "JULY", - "JUNE", - "LocaleHTMLCalendar", - "MARCH", - "MAY", - "MONDAY", - "Month", - "NOVEMBER", - "OCTOBER", - "SATURDAY", - "SEPTEMBER", - "SUNDAY", - "THURSDAY", - "TUESDAY", - "TextCalendar", - "WEDNESDAY", - "calendar", - "timegm", - "weekday", - "weekheader"] - ################################### # These should all not get flagged: ################################### diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index b34c93175ce39..6470c27eb6dbb 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -75,7 +75,7 @@ impl AlwaysFixableViolation for UnsortedDunderAll { } fn fix_title(&self) -> String { - "Sort `__all__` according to an isort-style sort".to_string() + "Apply an isort-style sorting to `__all__`".to_string() } } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index 9efcf4de099ad..c7b11fce41c3a 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -10,7 +10,7 @@ RUF022.py:5:11: RUF022 [*] `__all__` is not sorted 6 | __all__ += ["foo", "bar", "antipasti"] 7 | __all__ = ("d", "c", "b", "a") | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Safe fix 2 2 | # Single-line __all__ definitions (nice 'n' easy!) @@ -29,7 +29,7 @@ RUF022.py:6:12: RUF022 [*] `__all__` is not sorted | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF022 7 | __all__ = ("d", "c", "b", "a") | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Safe fix 3 3 | ################################################## @@ -50,7 +50,7 @@ RUF022.py:7:11: RUF022 [*] `__all__` is not sorted 8 | 9 | __all__: list = ["b", "c", "a",] # note the trailing comma, which is retained | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Safe fix 4 4 | @@ -70,7 +70,7 @@ RUF022.py:9:17: RUF022 [*] `__all__` is not sorted | ^^^^^^^^^^^^^^^^ RUF022 10 | __all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Safe fix 6 6 | __all__ += ["foo", "bar", "antipasti"] @@ -90,7 +90,7 @@ RUF022.py:10:18: RUF022 [*] `__all__` is not sorted 11 | 12 | if bool(): | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Safe fix 7 7 | __all__ = ("d", "c", "b", "a") @@ -110,7 +110,7 @@ RUF022.py:13:16: RUF022 [*] `__all__` is not sorted 14 | else: 15 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Safe fix 10 10 | __all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained @@ -131,7 +131,7 @@ RUF022.py:15:16: RUF022 [*] `__all__` is not sorted 16 | 17 | __all__: list[str] = ["the", "three", "little", "pigs"] | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Safe fix 12 12 | if bool(): @@ -152,7 +152,7 @@ RUF022.py:17:22: RUF022 [*] `__all__` is not sorted 18 | 19 | #################################### | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Safe fix 14 14 | else: @@ -180,7 +180,7 @@ RUF022.py:23:11: RUF022 [*] `__all__` is not sorted 30 | 31 | __all__ = [ | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix 21 21 | #################################### @@ -213,9 +213,9 @@ RUF022.py:31:11: RUF022 [*] `__all__` is not sorted 37 | | ] | |_^ RUF022 38 | -39 | ########################################## +39 | # we implement an "isort-style sort": | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix 29 29 | ) @@ -232,583 +232,583 @@ RUF022.py:31:11: RUF022 [*] `__all__` is not sorted 36 |+ "d" 37 37 | ] 38 38 | -39 39 | ########################################## +39 39 | # we implement an "isort-style sort": -RUF022.py:44:11: RUF022 [*] `__all__` is not sorted +RUF022.py:45:11: RUF022 [*] `__all__` is not sorted | -43 | # comment0 -44 | __all__ = ("d", "a", # comment1 +43 | # This (which is currently alphabetically sorted) +44 | # should get reordered accordingly: +45 | __all__ = [ | ___________^ -45 | | # comment2 -46 | | "f", "b", -47 | | "strangely", # comment3 -48 | | # comment4 -49 | | "formatted", -50 | | # comment5 -51 | | ) # comment6 - | |_^ RUF022 -52 | # comment7 +46 | | "APRIL", +47 | | "AUGUST", +48 | | "Calendar", +49 | | "DECEMBER", +50 | | "Day", +51 | | "FEBRUARY", +52 | | "FRIDAY", +53 | | "HTMLCalendar", +54 | | "IllegalMonthError", +55 | | "JANUARY", +56 | | "JULY", +57 | | "JUNE", +58 | | "LocaleHTMLCalendar", +59 | | "MARCH", +60 | | "MAY", +61 | | "MONDAY", +62 | | "Month", +63 | | "NOVEMBER", +64 | | "OCTOBER", +65 | | "SATURDAY", +66 | | "SEPTEMBER", +67 | | "SUNDAY", +68 | | "THURSDAY", +69 | | "TUESDAY", +70 | | "TextCalendar", +71 | | "WEDNESDAY", +72 | | "calendar", +73 | | "timegm", +74 | | "weekday", +75 | | "weekheader"] + | |_________________^ RUF022 +76 | +77 | ########################################## | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` -ℹ Unsafe fix -41 41 | ########################################## -42 42 | -43 43 | # comment0 -44 |-__all__ = ("d", "a", # comment1 -45 |- # comment2 -46 |- "f", "b", -47 |- "strangely", # comment3 -48 |- # comment4 - 44 |+__all__ = ( - 45 |+ "a", - 46 |+ "b", - 47 |+ "d", # comment1 - 48 |+ # comment2 - 49 |+ "f", - 50 |+ # comment4 -49 51 | "formatted", - 52 |+ "strangely", # comment3 -50 53 | # comment5 -51 54 | ) # comment6 -52 55 | # comment7 - -RUF022.py:54:11: RUF022 [*] `__all__` is not sorted +ℹ Safe fix +45 45 | __all__ = [ +46 46 | "APRIL", +47 47 | "AUGUST", +48 |- "Calendar", +49 48 | "DECEMBER", +50 |- "Day", +51 49 | "FEBRUARY", +52 50 | "FRIDAY", +53 |- "HTMLCalendar", +54 |- "IllegalMonthError", +55 51 | "JANUARY", +56 52 | "JULY", +57 53 | "JUNE", +58 |- "LocaleHTMLCalendar", +59 54 | "MARCH", +60 55 | "MAY", +61 56 | "MONDAY", +62 |- "Month", +63 57 | "NOVEMBER", +64 58 | "OCTOBER", +65 59 | "SATURDAY", +-------------------------------------------------------------------------------- +67 61 | "SUNDAY", +68 62 | "THURSDAY", +69 63 | "TUESDAY", + 64 |+ "WEDNESDAY", + 65 |+ "Calendar", + 66 |+ "Day", + 67 |+ "HTMLCalendar", + 68 |+ "IllegalMonthError", + 69 |+ "LocaleHTMLCalendar", + 70 |+ "Month", +70 71 | "TextCalendar", +71 |- "WEDNESDAY", +72 72 | "calendar", +73 73 | "timegm", +74 74 | "weekday", + +RUF022.py:82:11: RUF022 [*] `__all__` is not sorted | -52 | # comment7 -53 | -54 | __all__ = [ # comment0 +81 | # comment0 +82 | __all__ = ("d", "a", # comment1 | ___________^ -55 | | # comment1 -56 | | # comment2 -57 | | "dx", "cx", "bx", "ax" # comment3 -58 | | # comment4 -59 | | # comment5 -60 | | # comment6 -61 | | ] # comment7 +83 | | # comment2 +84 | | "f", "b", +85 | | "strangely", # comment3 +86 | | # comment4 +87 | | "formatted", +88 | | # comment5 +89 | | ) # comment6 | |_^ RUF022 -62 | -63 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", +90 | # comment7 | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -52 52 | # comment7 -53 53 | -54 54 | __all__ = [ # comment0 - 55 |+ "ax", - 56 |+ "bx", - 57 |+ "cx", -55 58 | # comment1 -56 59 | # comment2 -57 |- "dx", "cx", "bx", "ax" # comment3 - 60 |+ "dx" # comment3 -58 61 | # comment4 -59 62 | # comment5 -60 63 | # comment6 - -RUF022.py:63:11: RUF022 [*] `__all__` is not sorted - | -61 | ] # comment7 -62 | -63 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", - | ___________^ -64 | | "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", -65 | | "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", -66 | | "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", -67 | | "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", -68 | | "StreamReader", "StreamWriter", -69 | | "StreamReaderWriter", "StreamRecoder", -70 | | "getencoder", "getdecoder", "getincrementalencoder", -71 | | "getincrementaldecoder", "getreader", "getwriter", -72 | | "encode", "decode", "iterencode", "iterdecode", -73 | | "strict_errors", "ignore_errors", "replace_errors", -74 | | "xmlcharrefreplace_errors", -75 | | "backslashreplace_errors", "namereplace_errors", -76 | | "register_error", "lookup_error"] - | |____________________________________________^ RUF022 -77 | -78 | __all__: tuple[str, ...] = ( # a comment about the opening paren - | - = help: Sort `__all__` according to an isort-style sort +79 79 | ########################################## +80 80 | +81 81 | # comment0 +82 |-__all__ = ("d", "a", # comment1 +83 |- # comment2 +84 |- "f", "b", +85 |- "strangely", # comment3 +86 |- # comment4 + 82 |+__all__ = ( + 83 |+ "a", + 84 |+ "b", + 85 |+ "d", # comment1 + 86 |+ # comment2 + 87 |+ "f", + 88 |+ # comment4 +87 89 | "formatted", + 90 |+ "strangely", # comment3 +88 91 | # comment5 +89 92 | ) # comment6 +90 93 | # comment7 + +RUF022.py:92:11: RUF022 [*] `__all__` is not sorted + | + 90 | # comment7 + 91 | + 92 | __all__ = [ # comment0 + | ___________^ + 93 | | # comment1 + 94 | | # comment2 + 95 | | "dx", "cx", "bx", "ax" # comment3 + 96 | | # comment4 + 97 | | # comment5 + 98 | | # comment6 + 99 | | ] # comment7 + | |_^ RUF022 +100 | +101 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Unsafe fix +90 90 | # comment7 +91 91 | +92 92 | __all__ = [ # comment0 + 93 |+ "ax", + 94 |+ "bx", + 95 |+ "cx", +93 96 | # comment1 +94 97 | # comment2 +95 |- "dx", "cx", "bx", "ax" # comment3 + 98 |+ "dx" # comment3 +96 99 | # comment4 +97 100 | # comment5 +98 101 | # comment6 + +RUF022.py:101:11: RUF022 [*] `__all__` is not sorted + | + 99 | ] # comment7 +100 | +101 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", + | ___________^ +102 | | "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", +103 | | "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", +104 | | "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", +105 | | "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", +106 | | "StreamReader", "StreamWriter", +107 | | "StreamReaderWriter", "StreamRecoder", +108 | | "getencoder", "getdecoder", "getincrementalencoder", +109 | | "getincrementaldecoder", "getreader", "getwriter", +110 | | "encode", "decode", "iterencode", "iterdecode", +111 | | "strict_errors", "ignore_errors", "replace_errors", +112 | | "xmlcharrefreplace_errors", +113 | | "backslashreplace_errors", "namereplace_errors", +114 | | "register_error", "lookup_error"] + | |____________________________________________^ RUF022 +115 | +116 | __all__: tuple[str, ...] = ( # a comment about the opening paren + | + = help: Apply an isort-style sorting to `__all__` ℹ Safe fix -60 60 | # comment6 -61 61 | ] # comment7 -62 62 | -63 |-__all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", -64 |- "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", -65 |- "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", -66 |- "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", -67 |- "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", -68 |- "StreamReader", "StreamWriter", -69 |- "StreamReaderWriter", "StreamRecoder", -70 |- "getencoder", "getdecoder", "getincrementalencoder", -71 |- "getincrementaldecoder", "getreader", "getwriter", -72 |- "encode", "decode", "iterencode", "iterdecode", -73 |- "strict_errors", "ignore_errors", "replace_errors", -74 |- "xmlcharrefreplace_errors", -75 |- "backslashreplace_errors", "namereplace_errors", -76 |- "register_error", "lookup_error"] - 63 |+__all__ = [ - 64 |+ "BOM", - 65 |+ "BOM32_BE", - 66 |+ "BOM32_LE", - 67 |+ "BOM64_BE", - 68 |+ "BOM64_LE", - 69 |+ "BOM_BE", - 70 |+ "BOM_LE", - 71 |+ "BOM_UTF8", - 72 |+ "BOM_UTF16", - 73 |+ "BOM_UTF16_BE", - 74 |+ "BOM_UTF16_LE", - 75 |+ "BOM_UTF32", - 76 |+ "BOM_UTF32_BE", - 77 |+ "BOM_UTF32_LE", - 78 |+ "Codec", - 79 |+ "CodecInfo", - 80 |+ "EncodedFile", - 81 |+ "IncrementalDecoder", - 82 |+ "IncrementalEncoder", - 83 |+ "StreamReader", - 84 |+ "StreamReaderWriter", - 85 |+ "StreamRecoder", - 86 |+ "StreamWriter", - 87 |+ "backslashreplace_errors", - 88 |+ "decode", - 89 |+ "encode", - 90 |+ "getdecoder", - 91 |+ "getencoder", - 92 |+ "getincrementaldecoder", - 93 |+ "getincrementalencoder", - 94 |+ "getreader", - 95 |+ "getwriter", - 96 |+ "ignore_errors", - 97 |+ "iterdecode", - 98 |+ "iterencode", - 99 |+ "lookup", - 100 |+ "lookup_error", - 101 |+ "namereplace_errors", - 102 |+ "open", - 103 |+ "register", - 104 |+ "register_error", - 105 |+ "replace_errors", - 106 |+ "strict_errors", - 107 |+ "xmlcharrefreplace_errors"] -77 108 | -78 109 | __all__: tuple[str, ...] = ( # a comment about the opening paren -79 110 | # multiline comment about "bbb" part 1 - -RUF022.py:78:28: RUF022 [*] `__all__` is not sorted - | -76 | "register_error", "lookup_error"] -77 | -78 | __all__: tuple[str, ...] = ( # a comment about the opening paren - | ____________________________^ -79 | | # multiline comment about "bbb" part 1 -80 | | # multiline comment about "bbb" part 2 -81 | | "bbb", -82 | | # multiline comment about "aaa" part 1 -83 | | # multiline comment about "aaa" part 2 -84 | | "aaa", -85 | | ) - | |_^ RUF022 -86 | -87 | # we use natural sort for `__all__`, - | - = help: Sort `__all__` according to an isort-style sort +98 98 | # comment6 +99 99 | ] # comment7 +100 100 | +101 |-__all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", +102 |- "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", +103 |- "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", +104 |- "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", +105 |- "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", +106 |- "StreamReader", "StreamWriter", +107 |- "StreamReaderWriter", "StreamRecoder", +108 |- "getencoder", "getdecoder", "getincrementalencoder", +109 |- "getincrementaldecoder", "getreader", "getwriter", +110 |- "encode", "decode", "iterencode", "iterdecode", +111 |- "strict_errors", "ignore_errors", "replace_errors", +112 |- "xmlcharrefreplace_errors", +113 |- "backslashreplace_errors", "namereplace_errors", +114 |- "register_error", "lookup_error"] + 101 |+__all__ = [ + 102 |+ "BOM", + 103 |+ "BOM32_BE", + 104 |+ "BOM32_LE", + 105 |+ "BOM64_BE", + 106 |+ "BOM64_LE", + 107 |+ "BOM_BE", + 108 |+ "BOM_LE", + 109 |+ "BOM_UTF8", + 110 |+ "BOM_UTF16", + 111 |+ "BOM_UTF16_BE", + 112 |+ "BOM_UTF16_LE", + 113 |+ "BOM_UTF32", + 114 |+ "BOM_UTF32_BE", + 115 |+ "BOM_UTF32_LE", + 116 |+ "Codec", + 117 |+ "CodecInfo", + 118 |+ "EncodedFile", + 119 |+ "IncrementalDecoder", + 120 |+ "IncrementalEncoder", + 121 |+ "StreamReader", + 122 |+ "StreamReaderWriter", + 123 |+ "StreamRecoder", + 124 |+ "StreamWriter", + 125 |+ "backslashreplace_errors", + 126 |+ "decode", + 127 |+ "encode", + 128 |+ "getdecoder", + 129 |+ "getencoder", + 130 |+ "getincrementaldecoder", + 131 |+ "getincrementalencoder", + 132 |+ "getreader", + 133 |+ "getwriter", + 134 |+ "ignore_errors", + 135 |+ "iterdecode", + 136 |+ "iterencode", + 137 |+ "lookup", + 138 |+ "lookup_error", + 139 |+ "namereplace_errors", + 140 |+ "open", + 141 |+ "register", + 142 |+ "register_error", + 143 |+ "replace_errors", + 144 |+ "strict_errors", + 145 |+ "xmlcharrefreplace_errors"] +115 146 | +116 147 | __all__: tuple[str, ...] = ( # a comment about the opening paren +117 148 | # multiline comment about "bbb" part 1 + +RUF022.py:116:28: RUF022 [*] `__all__` is not sorted + | +114 | "register_error", "lookup_error"] +115 | +116 | __all__: tuple[str, ...] = ( # a comment about the opening paren + | ____________________________^ +117 | | # multiline comment about "bbb" part 1 +118 | | # multiline comment about "bbb" part 2 +119 | | "bbb", +120 | | # multiline comment about "aaa" part 1 +121 | | # multiline comment about "aaa" part 2 +122 | | "aaa", +123 | | ) + | |_^ RUF022 +124 | +125 | # we use natural sort for `__all__`, + | + = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -76 76 | "register_error", "lookup_error"] -77 77 | -78 78 | __all__: tuple[str, ...] = ( # a comment about the opening paren - 79 |+ # multiline comment about "aaa" part 1 - 80 |+ # multiline comment about "aaa" part 2 - 81 |+ "aaa", -79 82 | # multiline comment about "bbb" part 1 -80 83 | # multiline comment about "bbb" part 2 -81 84 | "bbb", -82 |- # multiline comment about "aaa" part 1 -83 |- # multiline comment about "aaa" part 2 -84 |- "aaa", -85 85 | ) -86 86 | -87 87 | # we use natural sort for `__all__`, - -RUF022.py:91:11: RUF022 [*] `__all__` is not sorted - | -89 | # Also, this doesn't end with a trailing comma, -90 | # so the autofix shouldn't introduce one: -91 | __all__ = ( - | ___________^ -92 | | "aadvark237", -93 | | "aadvark10092", -94 | | "aadvark174", # the very long whitespace span before this comment is retained -95 | | "aadvark532" # the even longer whitespace span before this comment is retained -96 | | ) - | |_^ RUF022 -97 | -98 | __all__.extend(["foo", "bar"]) - | - = help: Sort `__all__` according to an isort-style sort +114 114 | "register_error", "lookup_error"] +115 115 | +116 116 | __all__: tuple[str, ...] = ( # a comment about the opening paren + 117 |+ # multiline comment about "aaa" part 1 + 118 |+ # multiline comment about "aaa" part 2 + 119 |+ "aaa", +117 120 | # multiline comment about "bbb" part 1 +118 121 | # multiline comment about "bbb" part 2 +119 122 | "bbb", +120 |- # multiline comment about "aaa" part 1 +121 |- # multiline comment about "aaa" part 2 +122 |- "aaa", +123 123 | ) +124 124 | +125 125 | # we use natural sort for `__all__`, + +RUF022.py:129:11: RUF022 [*] `__all__` is not sorted + | +127 | # Also, this doesn't end with a trailing comma, +128 | # so the autofix shouldn't introduce one: +129 | __all__ = ( + | ___________^ +130 | | "aadvark237", +131 | | "aadvark10092", +132 | | "aadvark174", # the very long whitespace span before this comment is retained +133 | | "aadvark532" # the even longer whitespace span before this comment is retained +134 | | ) + | |_^ RUF022 +135 | +136 | __all__.extend(["foo", "bar"]) + | + = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -89 89 | # Also, this doesn't end with a trailing comma, -90 90 | # so the autofix shouldn't introduce one: -91 91 | __all__ = ( - 92 |+ "aadvark174", # the very long whitespace span before this comment is retained -92 93 | "aadvark237", -93 |- "aadvark10092", -94 |- "aadvark174", # the very long whitespace span before this comment is retained -95 |- "aadvark532" # the even longer whitespace span before this comment is retained - 94 |+ "aadvark532", # the even longer whitespace span before this comment is retained - 95 |+ "aadvark10092" -96 96 | ) -97 97 | -98 98 | __all__.extend(["foo", "bar"]) - -RUF022.py:98:16: RUF022 [*] `__all__` is not sorted +127 127 | # Also, this doesn't end with a trailing comma, +128 128 | # so the autofix shouldn't introduce one: +129 129 | __all__ = ( + 130 |+ "aadvark174", # the very long whitespace span before this comment is retained +130 131 | "aadvark237", +131 |- "aadvark10092", +132 |- "aadvark174", # the very long whitespace span before this comment is retained +133 |- "aadvark532" # the even longer whitespace span before this comment is retained + 132 |+ "aadvark532", # the even longer whitespace span before this comment is retained + 133 |+ "aadvark10092" +134 134 | ) +135 135 | +136 136 | __all__.extend(["foo", "bar"]) + +RUF022.py:136:16: RUF022 [*] `__all__` is not sorted | - 96 | ) - 97 | - 98 | __all__.extend(["foo", "bar"]) +134 | ) +135 | +136 | __all__.extend(["foo", "bar"]) | ^^^^^^^^^^^^^^ RUF022 - 99 | __all__.extend(("foo", "bar")) -100 | __all__.extend(( # comment0 +137 | __all__.extend(("foo", "bar")) +138 | __all__.extend(( # comment0 | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Safe fix -95 95 | "aadvark532" # the even longer whitespace span before this comment is retained -96 96 | ) -97 97 | -98 |-__all__.extend(["foo", "bar"]) - 98 |+__all__.extend(["bar", "foo"]) -99 99 | __all__.extend(("foo", "bar")) -100 100 | __all__.extend(( # comment0 -101 101 | # comment about foo - -RUF022.py:99:16: RUF022 [*] `__all__` is not sorted +133 133 | "aadvark532" # the even longer whitespace span before this comment is retained +134 134 | ) +135 135 | +136 |-__all__.extend(["foo", "bar"]) + 136 |+__all__.extend(["bar", "foo"]) +137 137 | __all__.extend(("foo", "bar")) +138 138 | __all__.extend(( # comment0 +139 139 | # comment about foo + +RUF022.py:137:16: RUF022 [*] `__all__` is not sorted | - 98 | __all__.extend(["foo", "bar"]) - 99 | __all__.extend(("foo", "bar")) +136 | __all__.extend(["foo", "bar"]) +137 | __all__.extend(("foo", "bar")) | ^^^^^^^^^^^^^^ RUF022 -100 | __all__.extend(( # comment0 -101 | # comment about foo +138 | __all__.extend(( # comment0 +139 | # comment about foo | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Safe fix -96 96 | ) -97 97 | -98 98 | __all__.extend(["foo", "bar"]) -99 |-__all__.extend(("foo", "bar")) - 99 |+__all__.extend(("bar", "foo")) -100 100 | __all__.extend(( # comment0 -101 101 | # comment about foo -102 102 | "foo", # comment about foo - -RUF022.py:100:16: RUF022 [*] `__all__` is not sorted +134 134 | ) +135 135 | +136 136 | __all__.extend(["foo", "bar"]) +137 |-__all__.extend(("foo", "bar")) + 137 |+__all__.extend(("bar", "foo")) +138 138 | __all__.extend(( # comment0 +139 139 | # comment about foo +140 140 | "foo", # comment about foo + +RUF022.py:138:16: RUF022 [*] `__all__` is not sorted | - 98 | __all__.extend(["foo", "bar"]) - 99 | __all__.extend(("foo", "bar")) -100 | __all__.extend(( # comment0 +136 | __all__.extend(["foo", "bar"]) +137 | __all__.extend(("foo", "bar")) +138 | __all__.extend(( # comment0 | ________________^ -101 | | # comment about foo -102 | | "foo", # comment about foo -103 | | # comment about bar -104 | | "bar" # comment about bar -105 | | # comment1 -106 | | )) # comment2 +139 | | # comment about foo +140 | | "foo", # comment about foo +141 | | # comment about bar +142 | | "bar" # comment about bar +143 | | # comment1 +144 | | )) # comment2 | |_^ RUF022 -107 | -108 | __all__.extend( # comment0 +145 | +146 | __all__.extend( # comment0 | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -98 98 | __all__.extend(["foo", "bar"]) -99 99 | __all__.extend(("foo", "bar")) -100 100 | __all__.extend(( # comment0 - 101 |+ # comment about bar - 102 |+ "bar", # comment about bar -101 103 | # comment about foo -102 |- "foo", # comment about foo -103 |- # comment about bar -104 |- "bar" # comment about bar - 104 |+ "foo" # comment about foo -105 105 | # comment1 -106 106 | )) # comment2 -107 107 | - -RUF022.py:110:5: RUF022 [*] `__all__` is not sorted +136 136 | __all__.extend(["foo", "bar"]) +137 137 | __all__.extend(("foo", "bar")) +138 138 | __all__.extend(( # comment0 + 139 |+ # comment about bar + 140 |+ "bar", # comment about bar +139 141 | # comment about foo +140 |- "foo", # comment about foo +141 |- # comment about bar +142 |- "bar" # comment about bar + 142 |+ "foo" # comment about foo +143 143 | # comment1 +144 144 | )) # comment2 +145 145 | + +RUF022.py:148:5: RUF022 [*] `__all__` is not sorted | -108 | __all__.extend( # comment0 -109 | # comment1 -110 | ( # comment2 +146 | __all__.extend( # comment0 +147 | # comment1 +148 | ( # comment2 | _____^ -111 | | # comment about foo -112 | | "foo", # comment about foo -113 | | # comment about bar -114 | | "bar" # comment about bar -115 | | # comment3 -116 | | ) # comment4 +149 | | # comment about foo +150 | | "foo", # comment about foo +151 | | # comment about bar +152 | | "bar" # comment about bar +153 | | # comment3 +154 | | ) # comment4 | |_____^ RUF022 -117 | ) # comment2 +155 | ) # comment2 | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -108 108 | __all__.extend( # comment0 -109 109 | # comment1 -110 110 | ( # comment2 - 111 |+ # comment about bar - 112 |+ "bar", # comment about bar -111 113 | # comment about foo -112 |- "foo", # comment about foo -113 |- # comment about bar -114 |- "bar" # comment about bar - 114 |+ "foo" # comment about foo -115 115 | # comment3 -116 116 | ) # comment4 -117 117 | ) # comment2 - -RUF022.py:119:16: RUF022 [*] `__all__` is not sorted +146 146 | __all__.extend( # comment0 +147 147 | # comment1 +148 148 | ( # comment2 + 149 |+ # comment about bar + 150 |+ "bar", # comment about bar +149 151 | # comment about foo +150 |- "foo", # comment about foo +151 |- # comment about bar +152 |- "bar" # comment about bar + 152 |+ "foo" # comment about foo +153 153 | # comment3 +154 154 | ) # comment4 +155 155 | ) # comment2 + +RUF022.py:157:16: RUF022 [*] `__all__` is not sorted | -117 | ) # comment2 -118 | -119 | __all__.extend([ # comment0 +155 | ) # comment2 +156 | +157 | __all__.extend([ # comment0 | ________________^ -120 | | # comment about foo -121 | | "foo", # comment about foo -122 | | # comment about bar -123 | | "bar" # comment about bar -124 | | # comment1 -125 | | ]) # comment2 +158 | | # comment about foo +159 | | "foo", # comment about foo +160 | | # comment about bar +161 | | "bar" # comment about bar +162 | | # comment1 +163 | | ]) # comment2 | |_^ RUF022 -126 | -127 | __all__.extend( # comment0 +164 | +165 | __all__.extend( # comment0 | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -117 117 | ) # comment2 -118 118 | -119 119 | __all__.extend([ # comment0 - 120 |+ # comment about bar - 121 |+ "bar", # comment about bar -120 122 | # comment about foo -121 |- "foo", # comment about foo -122 |- # comment about bar -123 |- "bar" # comment about bar - 123 |+ "foo" # comment about foo -124 124 | # comment1 -125 125 | ]) # comment2 -126 126 | - -RUF022.py:129:5: RUF022 [*] `__all__` is not sorted +155 155 | ) # comment2 +156 156 | +157 157 | __all__.extend([ # comment0 + 158 |+ # comment about bar + 159 |+ "bar", # comment about bar +158 160 | # comment about foo +159 |- "foo", # comment about foo +160 |- # comment about bar +161 |- "bar" # comment about bar + 161 |+ "foo" # comment about foo +162 162 | # comment1 +163 163 | ]) # comment2 +164 164 | + +RUF022.py:167:5: RUF022 [*] `__all__` is not sorted | -127 | __all__.extend( # comment0 -128 | # comment1 -129 | [ # comment2 +165 | __all__.extend( # comment0 +166 | # comment1 +167 | [ # comment2 | _____^ -130 | | # comment about foo -131 | | "foo", # comment about foo -132 | | # comment about bar -133 | | "bar" # comment about bar -134 | | # comment3 -135 | | ] # comment4 +168 | | # comment about foo +169 | | "foo", # comment about foo +170 | | # comment about bar +171 | | "bar" # comment about bar +172 | | # comment3 +173 | | ] # comment4 | |_____^ RUF022 -136 | ) # comment2 +174 | ) # comment2 | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -127 127 | __all__.extend( # comment0 -128 128 | # comment1 -129 129 | [ # comment2 - 130 |+ # comment about bar - 131 |+ "bar", # comment about bar -130 132 | # comment about foo -131 |- "foo", # comment about foo -132 |- # comment about bar -133 |- "bar" # comment about bar - 133 |+ "foo" # comment about foo -134 134 | # comment3 -135 135 | ] # comment4 -136 136 | ) # comment2 - -RUF022.py:138:11: RUF022 [*] `__all__` is not sorted +165 165 | __all__.extend( # comment0 +166 166 | # comment1 +167 167 | [ # comment2 + 168 |+ # comment about bar + 169 |+ "bar", # comment about bar +168 170 | # comment about foo +169 |- "foo", # comment about foo +170 |- # comment about bar +171 |- "bar" # comment about bar + 171 |+ "foo" # comment about foo +172 172 | # comment3 +173 173 | ] # comment4 +174 174 | ) # comment2 + +RUF022.py:176:11: RUF022 [*] `__all__` is not sorted | -136 | ) # comment2 -137 | -138 | __all__ = ["Style", "Treeview", +174 | ) # comment2 +175 | +176 | __all__ = ["Style", "Treeview", | ___________^ -139 | | # Extensions -140 | | "LabeledScale", "OptionMenu", -141 | | ] +177 | | # Extensions +178 | | "LabeledScale", "OptionMenu", +179 | | ] | |_^ RUF022 -142 | -143 | __all__ = ["Awaitable", "Coroutine", +180 | +181 | __all__ = ["Awaitable", "Coroutine", | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -135 135 | ] # comment4 -136 136 | ) # comment2 -137 137 | -138 |-__all__ = ["Style", "Treeview", -139 |- # Extensions -140 |- "LabeledScale", "OptionMenu", - 138 |+__all__ = [ - 139 |+ # Extensions - 140 |+ "LabeledScale", - 141 |+ "OptionMenu", - 142 |+ "Style", - 143 |+ "Treeview", -141 144 | ] -142 145 | -143 146 | __all__ = ["Awaitable", "Coroutine", - -RUF022.py:143:11: RUF022 [*] `__all__` is not sorted +173 173 | ] # comment4 +174 174 | ) # comment2 +175 175 | +176 |-__all__ = ["Style", "Treeview", +177 |- # Extensions +178 |- "LabeledScale", "OptionMenu", + 176 |+__all__ = [ + 177 |+ # Extensions + 178 |+ "LabeledScale", + 179 |+ "OptionMenu", + 180 |+ "Style", + 181 |+ "Treeview", +179 182 | ] +180 183 | +181 184 | __all__ = ["Awaitable", "Coroutine", + +RUF022.py:181:11: RUF022 [*] `__all__` is not sorted | -141 | ] -142 | -143 | __all__ = ["Awaitable", "Coroutine", +179 | ] +180 | +181 | __all__ = ["Awaitable", "Coroutine", | ___________^ -144 | | "AsyncIterable", "AsyncIterator", "AsyncGenerator", -145 | | ] +182 | | "AsyncIterable", "AsyncIterator", "AsyncGenerator", +183 | | ] | |____________^ RUF022 -146 | -147 | __all__ = [ +184 | +185 | __all__ = [ | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Safe fix -140 140 | "LabeledScale", "OptionMenu", -141 141 | ] -142 142 | -143 |-__all__ = ["Awaitable", "Coroutine", -144 |- "AsyncIterable", "AsyncIterator", "AsyncGenerator", -145 |- ] - 143 |+__all__ = [ - 144 |+ "AsyncGenerator", - 145 |+ "AsyncIterable", - 146 |+ "AsyncIterator", - 147 |+ "Awaitable", - 148 |+ "Coroutine", - 149 |+] -146 150 | -147 151 | __all__ = [ -148 152 | "foo", - -RUF022.py:147:11: RUF022 [*] `__all__` is not sorted +178 178 | "LabeledScale", "OptionMenu", +179 179 | ] +180 180 | +181 |-__all__ = ["Awaitable", "Coroutine", +182 |- "AsyncIterable", "AsyncIterator", "AsyncGenerator", +183 |- ] + 181 |+__all__ = [ + 182 |+ "AsyncGenerator", + 183 |+ "AsyncIterable", + 184 |+ "AsyncIterator", + 185 |+ "Awaitable", + 186 |+ "Coroutine", + 187 |+] +184 188 | +185 189 | __all__ = [ +186 190 | "foo", + +RUF022.py:185:11: RUF022 [*] `__all__` is not sorted | -145 | ] -146 | -147 | __all__ = [ +183 | ] +184 | +185 | __all__ = [ | ___________^ -148 | | "foo", -149 | | "bar", -150 | | "baz", -151 | | ] +186 | | "foo", +187 | | "bar", +188 | | "baz", +189 | | ] | |_____^ RUF022 -152 | -153 | # we implement an "isort-style sort": - | - = help: Sort `__all__` according to an isort-style sort - -ℹ Safe fix -145 145 | ] -146 146 | -147 147 | __all__ = [ -148 |- "foo", -149 148 | "bar", -150 149 | "baz", - 150 |+ "foo", -151 151 | ] -152 152 | -153 153 | # we implement an "isort-style sort": - -RUF022.py:159:11: RUF022 [*] `__all__` is not sorted - | -157 | # This (which is currently alphabetically sorted) -158 | # should get reordered accordingly: -159 | __all__ = [ - | ___________^ -160 | | "APRIL", -161 | | "AUGUST", -162 | | "Calendar", -163 | | "DECEMBER", -164 | | "Day", -165 | | "FEBRUARY", -166 | | "FRIDAY", -167 | | "HTMLCalendar", -168 | | "IllegalMonthError", -169 | | "JANUARY", -170 | | "JULY", -171 | | "JUNE", -172 | | "LocaleHTMLCalendar", -173 | | "MARCH", -174 | | "MAY", -175 | | "MONDAY", -176 | | "Month", -177 | | "NOVEMBER", -178 | | "OCTOBER", -179 | | "SATURDAY", -180 | | "SEPTEMBER", -181 | | "SUNDAY", -182 | | "THURSDAY", -183 | | "TUESDAY", -184 | | "TextCalendar", -185 | | "WEDNESDAY", -186 | | "calendar", -187 | | "timegm", -188 | | "weekday", -189 | | "weekheader"] - | |_________________^ RUF022 190 | 191 | ################################### | - = help: Sort `__all__` according to an isort-style sort + = help: Apply an isort-style sorting to `__all__` ℹ Safe fix -159 159 | __all__ = [ -160 160 | "APRIL", -161 161 | "AUGUST", -162 |- "Calendar", -163 162 | "DECEMBER", -164 |- "Day", -165 163 | "FEBRUARY", -166 164 | "FRIDAY", -167 |- "HTMLCalendar", -168 |- "IllegalMonthError", -169 165 | "JANUARY", -170 166 | "JULY", -171 167 | "JUNE", -172 |- "LocaleHTMLCalendar", -173 168 | "MARCH", -174 169 | "MAY", -175 170 | "MONDAY", -176 |- "Month", -177 171 | "NOVEMBER", -178 172 | "OCTOBER", -179 173 | "SATURDAY", --------------------------------------------------------------------------------- -181 175 | "SUNDAY", -182 176 | "THURSDAY", -183 177 | "TUESDAY", - 178 |+ "WEDNESDAY", - 179 |+ "Calendar", - 180 |+ "Day", - 181 |+ "HTMLCalendar", - 182 |+ "IllegalMonthError", - 183 |+ "LocaleHTMLCalendar", - 184 |+ "Month", -184 185 | "TextCalendar", -185 |- "WEDNESDAY", -186 186 | "calendar", -187 187 | "timegm", -188 188 | "weekday", +183 183 | ] +184 184 | +185 185 | __all__ = [ +186 |- "foo", +187 186 | "bar", +188 187 | "baz", + 188 |+ "foo", +189 189 | ] +190 190 | +191 191 | ################################### From c6701b4b98888216a4a27b0a9b192de1bf826451 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sun, 14 Jan 2024 12:27:17 +0000 Subject: [PATCH 49/77] Remove remaining `unreachable!()` and `.expect()` calls --- .../resources/test/fixtures/ruff/RUF022.py | 1 + .../src/rules/ruff/rules/sort_dunder_all.rs | 78 +++++++++---------- 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index 44024111e185f..690111ba3af6b 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -217,6 +217,7 @@ class IntroducesNonModuleScope: __all__ = ("b", "a", "e", "d") __all__ = ["b", "a", "e", "d"] __all__ += ["foo", "bar", "antipasti"] + __all__.extend(["zebra", "giraffe", "antelope"]) __all__ = {"look", "a", "set"} __all__ = {"very": "strange", "not": "sorted", "we don't": "care"} diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 6470c27eb6dbb..8ec029fd5df97 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -292,14 +292,13 @@ impl DunderAllValue { // As well as saving us unnecessary work, // returning early here also means that we can rely on the invariant // throughout the rest of this function that both `items` and `sorted_items` - // have length of at least two. If there are fewer than two items in `__all__`, - // it is impossible for them *not* to compare equal here: + // have length of at least two. + let [first_item, .., last_item] = self.items.as_slice() else { + return SortedDunderAll::AlreadySorted; + }; if self.is_already_sorted() { return SortedDunderAll::AlreadySorted; } - let [first_item, .., last_item] = self.items.as_slice() else { - unreachable!("Expected to have already returned if the list had < 2 items") - }; // As well as the "items" in the `__all__` definition, // there is also a "prelude" and a "postlude": @@ -458,7 +457,8 @@ fn collect_dunder_all_lines( ) -> Option<(Vec, bool)> { let mut parentheses_open = false; let mut lines = vec![]; - let mut items_in_line = vec![]; + let mut first_item_in_line = None; + let mut following_items_in_line = vec![]; let mut comment_range_start = None; let mut comment_in_line = None; let mut ends_with_trailing_comma = false; @@ -488,32 +488,35 @@ fn collect_dunder_all_lines( parentheses_open = true; } Tok::Rpar | Tok::Rsqb | Tok::Newline => { - if items_in_line.is_empty() { + if let Some(first_item) = first_item_in_line { + lines.push(DunderAllLine::OneOrMoreItems(LineWithItems { + first_item, + following_items: std::mem::take(&mut following_items_in_line), + trailing_comment_range: comment_in_line, + })); + } else { if let Some(comment) = comment_in_line { lines.push(DunderAllLine::JustAComment(LineWithJustAComment(comment))); } - } else { - lines.push(DunderAllLine::OneOrMoreItems(LineWithItems::new( - items_in_line, - comment_in_line, - ))); } break; } Tok::NonLogicalNewline => { - if items_in_line.is_empty() { + if let Some(first_item) = first_item_in_line { + lines.push(DunderAllLine::OneOrMoreItems(LineWithItems { + first_item, + following_items: std::mem::take(&mut following_items_in_line), + trailing_comment_range: comment_in_line, + })); + first_item_in_line = None; + comment_in_line = None; + comment_range_start = None; + } else { if let Some(comment) = comment_in_line { lines.push(DunderAllLine::JustAComment(LineWithJustAComment(comment))); comment_in_line = None; comment_range_start = None; } - } else { - lines.push(DunderAllLine::OneOrMoreItems(LineWithItems::new( - std::mem::take(&mut items_in_line), - comment_in_line, - ))); - comment_in_line = None; - comment_range_start = None; } } Tok::Comment(_) => { @@ -526,7 +529,11 @@ fn collect_dunder_all_lines( } } Tok::String { value, .. } => { - items_in_line.push((value, subrange)); + if first_item_in_line.is_none() { + first_item_in_line = Some((value, subrange)); + } else { + following_items_in_line.push((value, subrange)); + } ends_with_trailing_comma = false; comment_range_start = Some(subrange.end()); } @@ -555,24 +562,12 @@ struct LineWithItems { // For elements in the list, we keep track of the value of the // value of the element as well as the source-code range of the element. // (We need to know the actual value so that we can sort the items.) - items: Vec<(String, TextRange)>, + first_item: (String, TextRange), + following_items: Vec<(String, TextRange)>, // For comments, we only need to keep track of the source-code range. trailing_comment_range: Option, } -impl LineWithItems { - fn new(items: Vec<(String, TextRange)>, trailing_comment_range: Option) -> Self { - assert!( - !items.is_empty(), - "Use the 'JustAComment' variant to represent lines with 0 items" - ); - Self { - items, - trailing_comment_range, - } - } -} - #[derive(Debug)] enum DunderAllLine { JustAComment(LineWithJustAComment), @@ -593,7 +588,9 @@ fn collect_dunder_all_items( locator: &Locator, ) -> Vec { let mut all_items = Vec::with_capacity(match lines.as_slice() { - [DunderAllLine::OneOrMoreItems(single)] => single.items.len(), + [DunderAllLine::OneOrMoreItems(LineWithItems { + following_items, .. + })] => following_items.len() + 1, _ => lines.len(), }); let mut first_item_encountered = false; @@ -615,14 +612,11 @@ fn collect_dunder_all_items( } } DunderAllLine::OneOrMoreItems(LineWithItems { - items, + first_item: (first_val, first_range), + following_items, trailing_comment_range: comment_range, }) => { first_item_encountered = true; - let mut owned_items = items.into_iter(); - let (first_val, first_range) = owned_items - .next() - .expect("LineWithItems::new() should uphold the invariant that this list is always non-empty"); all_items.push(DunderAllItem::new( first_val, all_items.len(), @@ -630,7 +624,7 @@ fn collect_dunder_all_items( first_range, comment_range, )); - for (value, range) in owned_items { + for (value, range) in following_items { all_items.push(DunderAllItem::with_no_comments( value, all_items.len(), From c74cb482779a88ecb7a9eaafeee69ee814b47cb7 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sun, 14 Jan 2024 12:33:13 +0000 Subject: [PATCH 50/77] nit --- .../src/rules/ruff/rules/sort_dunder_all.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 8ec029fd5df97..0756151a98f73 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -388,6 +388,12 @@ impl DunderAllValue { } } +impl Ranged for DunderAllValue { + fn range(&self) -> TextRange { + self.range + } +} + /// Fixup the postlude for a multiline `__all__` definition. /// /// Without the fixup, closing `)` or `]` characters @@ -414,12 +420,6 @@ fn fixup_postlude<'a>( postlude } -impl Ranged for DunderAllValue { - fn range(&self) -> TextRange { - self.range - } -} - /// Variants of this enum are returned by `into_sorted_source_code()`. /// /// - `SortedDunderAll::AlreadySorted` is returned if `__all__` was From e298e1d74c418a87845880f1fe33676b36f88deb Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sun, 14 Jan 2024 14:30:29 +0000 Subject: [PATCH 51/77] Improve clarity of `collect_dunder_all_lines()` --- .../src/rules/ruff/rules/sort_dunder_all.rs | 111 +++++++++++------- 1 file changed, 67 insertions(+), 44 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 0756151a98f73..587070718df68 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -455,13 +455,15 @@ fn collect_dunder_all_lines( range: TextRange, locator: &Locator, ) -> Option<(Vec, bool)> { + // These first three variables are used for keeping track of state + // regarding the entirety of the `__all__` definition... let mut parentheses_open = false; - let mut lines = vec![]; - let mut first_item_in_line = None; - let mut following_items_in_line = vec![]; - let mut comment_range_start = None; - let mut comment_in_line = None; let mut ends_with_trailing_comma = false; + let mut lines = vec![]; + // ... all state regarding a single line of an `__all__` definition + // is encapsulated in this variable + let mut line_state = LineState::default(); + // `lex_starts_at()` gives us absolute ranges rather than relative ranges, // but (surprisingly) we still need to pass in the slice of code we want it to lex, // rather than the whole source file: @@ -488,57 +490,26 @@ fn collect_dunder_all_lines( parentheses_open = true; } Tok::Rpar | Tok::Rsqb | Tok::Newline => { - if let Some(first_item) = first_item_in_line { - lines.push(DunderAllLine::OneOrMoreItems(LineWithItems { - first_item, - following_items: std::mem::take(&mut following_items_in_line), - trailing_comment_range: comment_in_line, - })); - } else { - if let Some(comment) = comment_in_line { - lines.push(DunderAllLine::JustAComment(LineWithJustAComment(comment))); - } + if let Some(line) = line_state.into_dunder_all_line() { + lines.push(line); } break; } Tok::NonLogicalNewline => { - if let Some(first_item) = first_item_in_line { - lines.push(DunderAllLine::OneOrMoreItems(LineWithItems { - first_item, - following_items: std::mem::take(&mut following_items_in_line), - trailing_comment_range: comment_in_line, - })); - first_item_in_line = None; - comment_in_line = None; - comment_range_start = None; - } else { - if let Some(comment) = comment_in_line { - lines.push(DunderAllLine::JustAComment(LineWithJustAComment(comment))); - comment_in_line = None; - comment_range_start = None; - } + if let Some(line) = line_state.into_dunder_all_line() { + lines.push(line); } + line_state = LineState::default(); } Tok::Comment(_) => { - comment_in_line = { - if let Some(range_start) = comment_range_start { - Some(TextRange::new(range_start, subrange.end())) - } else { - Some(subrange) - } - } + line_state.receive_comment_token(subrange); } Tok::String { value, .. } => { - if first_item_in_line.is_none() { - first_item_in_line = Some((value, subrange)); - } else { - following_items_in_line.push((value, subrange)); - } + line_state.receive_string_token(value, subrange); ends_with_trailing_comma = false; - comment_range_start = Some(subrange.end()); } Tok::Comma => { - comment_range_start = Some(subrange.end()); + line_state.receive_comma_token(subrange); ends_with_trailing_comma = true; } _ => return None, @@ -547,6 +518,58 @@ fn collect_dunder_all_lines( Some((lines, ends_with_trailing_comma)) } +/// This struct is for keeping track of state +/// regarding a single line in an `__all__` definition. +/// It is purely internal to `collect_dunder_all_lines()`, +/// and should not be used outside that function. +#[derive(Debug, Default)] +struct LineState { + first_item_in_line: Option<(String, TextRange)>, + following_items_in_line: Vec<(String, TextRange)>, + comment_range_start: Option, + comment_in_line: Option, +} + +impl LineState { + fn receive_string_token(&mut self, token_value: String, token_range: TextRange) { + if self.first_item_in_line.is_none() { + self.first_item_in_line = Some((token_value, token_range)); + } else { + self.following_items_in_line + .push((token_value, token_range)); + } + self.comment_range_start = Some(token_range.end()); + } + + fn receive_comma_token(&mut self, token_range: TextRange) { + self.comment_range_start = Some(token_range.end()); + } + + fn receive_comment_token(&mut self, token_range: TextRange) { + self.comment_in_line = { + if let Some(range_start) = self.comment_range_start { + Some(TextRange::new(range_start, token_range.end())) + } else { + Some(token_range) + } + } + } + + fn into_dunder_all_line(self) -> Option { + if let Some(first_item) = self.first_item_in_line { + Some(DunderAllLine::OneOrMoreItems(LineWithItems { + first_item, + following_items: self.following_items_in_line, + trailing_comment_range: self.comment_in_line, + })) + } else { + self.comment_in_line.map(|comment_range| { + DunderAllLine::JustAComment(LineWithJustAComment(comment_range)) + }) + } + } +} + /// Instances of this struct represent source-code lines in the middle /// of multiline `__all__` tuples/lists where the line contains /// 0 elements of the tuple/list, but the line does have a comment in it. From 256c37e5d71599f5790dde118e88b66806d1b1af Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sun, 14 Jan 2024 17:47:43 +0000 Subject: [PATCH 52/77] Use more ruff-specific idioms --- .../ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 587070718df68..cf23929a94a07 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -129,15 +129,11 @@ pub(crate) fn sort_dunder_all_extend_call( /// Given a Python call `x.extend()`, return `Some("x")`. /// Return `None` if this wasn't actually a `.extend()` call after all. fn extract_name_dot_extend_was_called_on(node: &ast::Expr) -> Option<&str> { - let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = node else { - return None; - }; + let ast::ExprAttribute { value, attr, .. } = node.as_attribute_expr()?; if attr != "extend" { return None; } - let ast::Expr::Name(ast::ExprName { ref id, .. }) = **value else { - return None; - }; + let ast::ExprName { id, .. } = value.as_name_expr()?; Some(id) } From 5290f9318edf87c8f0f6bdf0cecf8e60af348bbc Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sun, 14 Jan 2024 21:50:56 +0000 Subject: [PATCH 53/77] deduplicate the different hook functions --- .../src/rules/ruff/rules/sort_dunder_all.rs | 60 ++++++++----------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index cf23929a94a07..bed88eb697028 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -85,28 +85,23 @@ pub(crate) fn sort_dunder_all_assign( checker: &mut Checker, ast::StmtAssign { value, targets, .. }: &ast::StmtAssign, ) { - let [ast::Expr::Name(ast::ExprName { id, .. })] = targets.as_slice() else { - return; - }; - sort_dunder_all(checker, id, value); + if let [expr] = targets.as_slice() { + sort_dunder_all(checker, expr, value); + } } /// Sort an `__all__` mutation represented by a `StmtAugAssign` AST node. /// For example: `__all__ += ["b", "c", "a"]`. pub(crate) fn sort_dunder_all_aug_assign(checker: &mut Checker, node: &ast::StmtAugAssign) { - let ast::StmtAugAssign { + if let ast::StmtAugAssign { ref value, ref target, op: ast::Operator::Add, .. } = *node - else { - return; - }; - let ast::Expr::Name(ast::ExprName { ref id, .. }) = **target else { - return; - }; - sort_dunder_all(checker, id, value); + { + sort_dunder_all(checker, target, value); + } } /// Sort an `__all__` mutation from a call to `.extend()`. @@ -121,41 +116,38 @@ pub(crate) fn sort_dunder_all_extend_call( let ([value_passed], []) = (args.as_slice(), keywords.as_slice()) else { return; }; - if let Some(name) = extract_name_dot_extend_was_called_on(func) { - sort_dunder_all(checker, name, value_passed); - } -} - -/// Given a Python call `x.extend()`, return `Some("x")`. -/// Return `None` if this wasn't actually a `.extend()` call after all. -fn extract_name_dot_extend_was_called_on(node: &ast::Expr) -> Option<&str> { - let ast::ExprAttribute { value, attr, .. } = node.as_attribute_expr()?; - if attr != "extend" { - return None; + let ast::Expr::Attribute(ast::ExprAttribute { + ref value, + ref attr, + .. + }) = **func + else { + return; + }; + if attr == "extend" { + sort_dunder_all(checker, value, value_passed); } - let ast::ExprName { id, .. } = value.as_name_expr()?; - Some(id) } /// Sort an `__all__` definition represented by a `StmtAnnAssign` AST node. /// For example: `__all__: list[str] = ["b", "c", "a"]`. pub(crate) fn sort_dunder_all_ann_assign(checker: &mut Checker, node: &ast::StmtAnnAssign) { - let ast::StmtAnnAssign { + if let ast::StmtAnnAssign { ref target, value: Some(ref val), .. } = node - else { - return; - }; - let ast::Expr::Name(ast::ExprName { ref id, .. }) = **target else { + { + sort_dunder_all(checker, target, val); + } +} + +fn sort_dunder_all(checker: &mut Checker, target: &ast::Expr, node: &ast::Expr) { + let ast::Expr::Name(ast::ExprName { id, .. }) = target else { return; }; - sort_dunder_all(checker, id, val); -} -fn sort_dunder_all(checker: &mut Checker, target: &str, node: &ast::Expr) { - if target != "__all__" { + if id != "__all__" { return; } From 4677cb29e8416322d35b3ae227ab49158790781f Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sun, 14 Jan 2024 23:54:33 +0000 Subject: [PATCH 54/77] remove some unnecessary uses of `ref` --- .../src/rules/ruff/rules/sort_dunder_all.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index bed88eb697028..9cbe3127598f5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -94,11 +94,11 @@ pub(crate) fn sort_dunder_all_assign( /// For example: `__all__ += ["b", "c", "a"]`. pub(crate) fn sort_dunder_all_aug_assign(checker: &mut Checker, node: &ast::StmtAugAssign) { if let ast::StmtAugAssign { - ref value, - ref target, + value, + target, op: ast::Operator::Add, .. - } = *node + } = node { sort_dunder_all(checker, target, value); } @@ -108,7 +108,7 @@ pub(crate) fn sort_dunder_all_aug_assign(checker: &mut Checker, node: &ast::Stmt pub(crate) fn sort_dunder_all_extend_call( checker: &mut Checker, ast::ExprCall { - ref func, + func, arguments: ast::Arguments { args, keywords, .. }, .. }: &ast::ExprCall, @@ -133,8 +133,8 @@ pub(crate) fn sort_dunder_all_extend_call( /// For example: `__all__: list[str] = ["b", "c", "a"]`. pub(crate) fn sort_dunder_all_ann_assign(checker: &mut Checker, node: &ast::StmtAnnAssign) { if let ast::StmtAnnAssign { - ref target, - value: Some(ref val), + target, + value: Some(val), .. } = node { From bcfeae90175f6cb2dbabde642fef55f148673e11 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 15 Jan 2024 07:33:01 +0000 Subject: [PATCH 55/77] simplify slightly --- .../src/rules/ruff/rules/sort_dunder_all.rs | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 9cbe3127598f5..e2811e34bdfeb 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -93,14 +93,8 @@ pub(crate) fn sort_dunder_all_assign( /// Sort an `__all__` mutation represented by a `StmtAugAssign` AST node. /// For example: `__all__ += ["b", "c", "a"]`. pub(crate) fn sort_dunder_all_aug_assign(checker: &mut Checker, node: &ast::StmtAugAssign) { - if let ast::StmtAugAssign { - value, - target, - op: ast::Operator::Add, - .. - } = node - { - sort_dunder_all(checker, target, value); + if node.op.is_add() { + sort_dunder_all(checker, &node.target, &node.value); } } @@ -132,13 +126,8 @@ pub(crate) fn sort_dunder_all_extend_call( /// Sort an `__all__` definition represented by a `StmtAnnAssign` AST node. /// For example: `__all__: list[str] = ["b", "c", "a"]`. pub(crate) fn sort_dunder_all_ann_assign(checker: &mut Checker, node: &ast::StmtAnnAssign) { - if let ast::StmtAnnAssign { - target, - value: Some(val), - .. - } = node - { - sort_dunder_all(checker, target, val); + if let Some(value) = &node.value { + sort_dunder_all(checker, &node.target, value); } } @@ -579,6 +568,12 @@ struct LineWithItems { trailing_comment_range: Option, } +impl LineWithItems { + fn num_items(&self) -> usize { + self.following_items.len() + 1 + } +} + #[derive(Debug)] enum DunderAllLine { JustAComment(LineWithJustAComment), @@ -599,9 +594,7 @@ fn collect_dunder_all_items( locator: &Locator, ) -> Vec { let mut all_items = Vec::with_capacity(match lines.as_slice() { - [DunderAllLine::OneOrMoreItems(LineWithItems { - following_items, .. - })] => following_items.len() + 1, + [DunderAllLine::OneOrMoreItems(single)] => single.num_items(), _ => lines.len(), }); let mut first_item_encountered = false; From 5dced4aa5f8ba73eca25335cf965dfcfd725607d Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 15 Jan 2024 09:27:52 +0000 Subject: [PATCH 56/77] don't be so judgemental Co-authored-by: Micha Reiser --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index e2811e34bdfeb..528ab2dbde2e7 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -413,7 +413,7 @@ enum SortedDunderAll { /// Collect data on each line of `__all__`. /// Return `None` if `__all__` appears to be invalid, -/// or if it's an edge case we don't care about. +/// or if it's an edge case we don't support. /// /// Why do we need to do this using the raw tokens, /// when we already have the AST? The AST strips out From dd9370ec4e63c3f35572eaf757de135b852d6cc5 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 15 Jan 2024 10:15:12 +0000 Subject: [PATCH 57/77] Rename `receive_*` methods for clarity --- .../src/rules/ruff/rules/sort_dunder_all.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 528ab2dbde2e7..fd3c045aa0aa9 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -479,14 +479,14 @@ fn collect_dunder_all_lines( line_state = LineState::default(); } Tok::Comment(_) => { - line_state.receive_comment_token(subrange); + line_state.visit_comment_token(subrange); } Tok::String { value, .. } => { - line_state.receive_string_token(value, subrange); + line_state.visit_string_token(value, subrange); ends_with_trailing_comma = false; } Tok::Comma => { - line_state.receive_comma_token(subrange); + line_state.visit_comma_token(subrange); ends_with_trailing_comma = true; } _ => return None, @@ -508,7 +508,7 @@ struct LineState { } impl LineState { - fn receive_string_token(&mut self, token_value: String, token_range: TextRange) { + fn visit_string_token(&mut self, token_value: String, token_range: TextRange) { if self.first_item_in_line.is_none() { self.first_item_in_line = Some((token_value, token_range)); } else { @@ -518,14 +518,14 @@ impl LineState { self.comment_range_start = Some(token_range.end()); } - fn receive_comma_token(&mut self, token_range: TextRange) { + fn visit_comma_token(&mut self, token_range: TextRange) { self.comment_range_start = Some(token_range.end()); } - fn receive_comment_token(&mut self, token_range: TextRange) { + fn visit_comment_token(&mut self, token_range: TextRange) { self.comment_in_line = { - if let Some(range_start) = self.comment_range_start { - Some(TextRange::new(range_start, token_range.end())) + if let Some(comment_range_start) = self.comment_range_start { + Some(TextRange::new(comment_range_start, token_range.end())) } else { Some(token_range) } From 0281f932600e338d8b94a5751f17ad3bb313796a Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 15 Jan 2024 10:22:58 +0000 Subject: [PATCH 58/77] Add a test for `__all__` definitions with duplicate elements --- .../ruff_linter/resources/test/fixtures/ruff/RUF022.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index 690111ba3af6b..c83415815cecb 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -251,3 +251,13 @@ class IntroducesNonModuleScope: "foo" ) ) + +# We don't deduplicate elements (yet); +# this just ensures that duplicate elements aren't unnecessarily +# reordered by an autofix: +__all__ = ( + "duplicate_element", # comment1 + "duplicate_element", # comment3 + "duplicate_element", # comment2 + "duplicate_element", # comment0 +) From 33cab1b4dae12643e55a0f399e9a9dcd2274384a Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 15 Jan 2024 10:30:00 +0000 Subject: [PATCH 59/77] Get rid of `original_index` --- .../src/rules/ruff/rules/sort_dunder_all.rs | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index fd3c045aa0aa9..5f67c68a1a559 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -623,17 +623,12 @@ fn collect_dunder_all_items( first_item_encountered = true; all_items.push(DunderAllItem::new( first_val, - all_items.len(), std::mem::take(&mut preceding_comment_ranges), first_range, comment_range, )); for (value, range) in following_items { - all_items.push(DunderAllItem::with_no_comments( - value, - all_items.len(), - range, - )); + all_items.push(DunderAllItem::with_no_comments(value, range)); } } } @@ -711,8 +706,6 @@ impl InferredMemberType { struct DunderAllItem { value: String, category: InferredMemberType, - // Each `AllItem` in any given list should have a unique `original_index`: - original_index: usize, preceding_comment_ranges: Vec, element_range: TextRange, // total_range incorporates the ranges of preceding comments @@ -725,7 +718,6 @@ struct DunderAllItem { impl DunderAllItem { fn new( value: String, - original_index: usize, preceding_comment_ranges: Vec, element_range: TextRange, end_of_line_comments: Option, @@ -741,7 +733,6 @@ impl DunderAllItem { Self { value, category, - original_index, preceding_comment_ranges, element_range, total_range, @@ -749,8 +740,8 @@ impl DunderAllItem { } } - fn with_no_comments(value: String, original_index: usize, element_range: TextRange) -> Self { - Self::new(value, original_index, vec![], element_range, None) + fn with_no_comments(value: String, element_range: TextRange) -> Self { + Self::new(value, vec![], element_range, None) } } @@ -760,20 +751,11 @@ impl Ranged for DunderAllItem { } } -impl PartialEq for DunderAllItem { - fn eq(&self, other: &Self) -> bool { - self.original_index == other.original_index - } -} - -impl Eq for DunderAllItem {} - impl Ord for DunderAllItem { fn cmp(&self, other: &Self) -> Ordering { self.category .cmp(&other.category) .then_with(|| natord::compare(&self.value, &other.value)) - .then_with(|| self.original_index.cmp(&other.original_index)) } } @@ -783,6 +765,14 @@ impl PartialOrd for DunderAllItem { } } +impl PartialEq for DunderAllItem { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} + +impl Eq for DunderAllItem {} + fn join_singleline_dunder_all_items(sorted_items: &[DunderAllItem], locator: &Locator) -> String { sorted_items .iter() From c8e4b6362c9c8777f0eb78586a4edcc54c2316ee Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 15 Jan 2024 15:53:23 +0000 Subject: [PATCH 60/77] Big overhaul addressing Micha's review --- .../resources/test/fixtures/ruff/RUF022.py | 40 +- .../src/rules/ruff/rules/sort_dunder_all.rs | 461 +++--- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 1297 +++++++++-------- 3 files changed, 975 insertions(+), 823 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index c83415815cecb..af9003963f709 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -6,8 +6,12 @@ __all__ += ["foo", "bar", "antipasti"] __all__ = ("d", "c", "b", "a") -__all__: list = ["b", "c", "a",] # note the trailing comma, which is retained -__all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained +# Quoting style is retained, +# but unnecessary parens are not +__all__: list = ['b', "c", ((('a')))] +# Trailing commas are also not retained in single-line `__all__` definitions +# (but they are in multiline `__all__` definitions) +__all__: tuple = ("b", "c", "a",) if bool(): __all__ += ("x", "m", "a", "s") @@ -16,6 +20,11 @@ __all__: list[str] = ["the", "three", "little", "pigs"] +__all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") +__all__.extend(["foo", "bar"]) +__all__.extend(("foo", "bar")) +__all__.extend((((["foo", "bar"])))) + #################################### # Neat multiline __all__ definitions #################################### @@ -133,8 +142,6 @@ "aadvark532" # the even longer whitespace span before this comment is retained ) -__all__.extend(["foo", "bar"]) -__all__.extend(("foo", "bar")) __all__.extend(( # comment0 # comment about foo "foo", # comment about foo @@ -188,6 +195,21 @@ "baz", ] +######################################################################### +# These should be flagged, but not fixed: +# - Parenthesized items in multiline definitions are out of scope +# - The same goes for any `__all__` definitions with concatenated strings +######################################################################### + +__all__ = ( + "look", + ( + "a_veeeeeeeeeeeeeeeeeeery_long_parenthesized_item" + ), +) + +__all__ = ("don't" "care" "about", "__all__" "with", "concatenated" "strings") + ################################### # These should all not get flagged: ################################### @@ -223,15 +245,6 @@ class IntroducesNonModuleScope: __all__ = {"very": "strange", "not": "sorted", "we don't": "care"} ["not", "an", "assignment", "just", "an", "expression"] __all__ = (9, 8, 7) -__all__ = ("don't" "care" "about", "__all__" "with", "concatenated" "strings") - -__all__ = ( - "look", - ( - "a_veeeeeeeeeeeeeeeeeeery_long_parenthesized_item_we_dont_care_about" - ), -) - __all__ = ( # This is just an empty tuple, # but, # it's very well @@ -239,7 +252,6 @@ class IntroducesNonModuleScope: __all__.append("foo") __all__.extend(["bar", "foo"]) -__all__.extend((((["bar", "foo"])))) __all__.extend([ "bar", # comment0 "foo" # comment1 diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 5f67c68a1a559..d7c017cff230b 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -1,13 +1,13 @@ use std::borrow::Cow; use std::cmp::Ordering; -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; +use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast as ast; use ruff_python_codegen::Stylist; use ruff_python_parser::{lexer, Mode, Tok}; use ruff_python_stdlib::str::is_cased_uppercase; -use ruff_python_trivia::leading_indentation; +use ruff_python_trivia::{leading_indentation, SimpleTokenKind, SimpleTokenizer}; use ruff_source_file::Locator; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -68,14 +68,16 @@ use natord; #[violation] pub struct UnsortedDunderAll; -impl AlwaysFixableViolation for UnsortedDunderAll { +impl Violation for UnsortedDunderAll { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!("`__all__` is not sorted") } - fn fix_title(&self) -> String { - "Apply an isort-style sorting to `__all__`".to_string() + fn fix_title(&self) -> Option { + Some("Apply an isort-style sorting to `__all__`".to_string()) } } @@ -131,6 +133,97 @@ pub(crate) fn sort_dunder_all_ann_assign(checker: &mut Checker, node: &ast::Stmt } } +/// Return `true` if a tuple is parenthesized in the source code. +/// +/// (Yes, this function is shamelessly copied from the formatter.) +fn is_tuple_parenthesized(tuple: &ast::ExprTuple, source: &str) -> bool { + let Some(elt) = tuple.elts.first() else { + return true; + }; + + // Count the number of open parentheses between the start of the tuple and the first element. + let open_parentheses_count = + SimpleTokenizer::new(source, TextRange::new(tuple.start(), elt.start())) + .skip_trivia() + .filter(|token| token.kind() == SimpleTokenKind::LParen) + .count(); + if open_parentheses_count == 0 { + return false; + } + + // Count the number of parentheses between the end of the first element and its trailing comma. + let close_parentheses_count = + SimpleTokenizer::new(source, TextRange::new(elt.end(), tuple.end())) + .skip_trivia() + .take_while(|token| token.kind() != SimpleTokenKind::Comma) + .filter(|token| token.kind() == SimpleTokenKind::RParen) + .count(); + + // If the number of open parentheses is greater than the number of close parentheses, the tuple + // is parenthesized. + open_parentheses_count > close_parentheses_count +} + +fn sort_single_line_dunder_all( + elts: &[ast::Expr], + elements: &[&str], + kind: &DunderAllKind, + locator: &Locator, +) -> String { + let mut element_pairs = elts.iter().zip(elements).collect_vec(); + element_pairs.sort_by_cached_key(|(_, elem)| AllItemSortKey::from(**elem)); + let joined_items = element_pairs + .iter() + .map(|(elt, _)| locator.slice(elt)) + .join(", "); + match kind { + DunderAllKind::List => format!("[{joined_items}]"), + DunderAllKind::Tuple(tuple_node) => { + if is_tuple_parenthesized(tuple_node, locator.contents()) { + format!("({joined_items})") + } else { + joined_items + } + } + } +} + +enum DunderAllKind<'a> { + List, + Tuple(&'a ast::ExprTuple), +} + +fn get_fix( + range: TextRange, + elts: &[ast::Expr], + string_items: &[&str], + kind: &DunderAllKind, + checker: &Checker, +) -> Option { + let locator = checker.locator(); + let is_multiline = locator.contains_line_break(range); + + let sorted_source_code = { + if is_multiline { + MultilineDunderAllValue::from_source_range(range, locator)? + .into_sorted_source_code(locator, checker.stylist()) + } else { + sort_single_line_dunder_all(elts, string_items, kind, locator) + } + }; + + let applicability = { + if is_multiline && checker.indexer().comment_ranges().intersects(range) { + Applicability::Unsafe + } else { + Applicability::Safe + } + }; + + let edit = Edit::range_replacement(sorted_source_code, range); + Some(Fix::applicable_edit(edit, applicability)) +} + fn sort_dunder_all(checker: &mut Checker, target: &ast::Expr, node: &ast::Expr) { let ast::Expr::Name(ast::ExprName { id, .. }) = target else { return; @@ -145,92 +238,65 @@ fn sort_dunder_all(checker: &mut Checker, target: &ast::Expr, node: &ast::Expr) return; } - let locator = checker.locator(); + let (elts, range, kind) = match node { + ast::Expr::List(ast::ExprList { elts, range, .. }) => (elts, *range, DunderAllKind::List), + ast::Expr::Tuple(tuple_node @ ast::ExprTuple { elts, range, .. }) => { + (elts, *range, DunderAllKind::Tuple(tuple_node)) + } + _ => return, + }; - let Some( - dunder_all_val @ DunderAllValue { - range, multiline, .. - }, - ) = DunderAllValue::from_expr(node, locator) - else { + let mut possibly_fixable = true; + let mut string_items = vec![]; + for elt in elts { + // Don't flag `__all__` definitions that contain non-strings + let Some(string_literal) = elt.as_string_literal_expr() else { + return; + }; + // If any strings are implicitly concatenated, don't bother trying to autofix + if possibly_fixable && string_literal.value.is_implicit_concatenated() { + possibly_fixable = false; + } + string_items.push(string_literal.value.to_str()); + } + if dunder_all_is_already_sorted(&string_items) { return; - }; + } - let new_dunder_all = match dunder_all_val.into_sorted_source_code(locator, checker.stylist()) { - SortedDunderAll::AlreadySorted => return, - SortedDunderAll::Sorted(value) => value, - }; + let mut diagnostic = Diagnostic::new(UnsortedDunderAll, range); - let applicability = { - if multiline && checker.indexer().comment_ranges().intersects(node.range()) { - Applicability::Unsafe - } else { - Applicability::Safe + if possibly_fixable { + if let Some(fix) = get_fix(range, elts, &string_items, &kind, checker) { + diagnostic.set_fix(fix); } - }; - - let edit = Edit::range_replacement(new_dunder_all, range); + } - checker.diagnostics.push( - Diagnostic::new(UnsortedDunderAll, range) - .with_fix(Fix::applicable_edit(edit, applicability)), - ); + checker.diagnostics.push(diagnostic); } /// An instance of this struct encapsulates an analysis /// of a Python tuple/list that represents an `__all__` /// definition or augmentation. -struct DunderAllValue { +struct MultilineDunderAllValue { items: Vec, range: TextRange, - multiline: bool, ends_with_trailing_comma: bool, } -impl DunderAllValue { +impl MultilineDunderAllValue { /// Analyse an AST node for a Python tuple/list that represents an `__all__` /// definition or augmentation. Return `None` if the analysis fails /// for whatever reason, or if it looks like we're not actually looking at a /// tuple/list after all. - fn from_expr(value: &ast::Expr, locator: &Locator) -> Option { - // Step (1): inspect the AST to check that we're looking at something vaguely sane: - let (elts, range) = match value { - ast::Expr::List(ast::ExprList { elts, range, .. }) => (elts, range), - ast::Expr::Tuple(ast::ExprTuple { elts, range, .. }) => (elts, range), - _ => return None, - }; - - // An `__all__` definition with < 2 elements can't be unsorted; - // no point in proceeding any further here. - // - // N.B. Here, this is just an optimisation - // (and to avoid us rewriting code when we don't have to). - // - // While other parts of this file *do* depend on there being a - // minimum of 2 elements in `__all__`, that invariant - // is maintained elsewhere. (For example, see comments at the - // start of `into_sorted_source_code()`.) - if elts.len() < 2 { - return None; - } - - for elt in elts { - // Only consider sorting it if __all__ only has strings in it - let string_literal = elt.as_string_literal_expr()?; - // And if any strings are implicitly concatenated, don't bother - if string_literal.value.is_implicit_concatenated() { - return None; - } - } - - // Step (2): parse the `__all__` definition using the raw tokens. + fn from_source_range(range: TextRange, locator: &Locator) -> Option { + // Parse the `__all__` definition using the raw tokens. // See the docs for `collect_dunder_all_lines()` for why we have to // use the raw tokens, rather than just the AST, to do this parsing. // - // (2a). Start by collecting information on each line individually: - let (lines, ends_with_trailing_comma) = collect_dunder_all_lines(*range, locator)?; + // Step (1). Start by collecting information on each line individually: + let (lines, ends_with_trailing_comma) = collect_dunder_all_lines(range, locator)?; - // (2b). Group lines together into sortable "items": + // Step (2). Group lines together into sortable "items": // - Any "item" contains a single element of the `__all__` list/tuple // - "Items" are ordered according to the element they contain // - Assume that any comments on their own line are meant to be grouped @@ -238,44 +304,25 @@ impl DunderAllValue { // the comments above the element move with it. // - The same goes for any comments on the same line as an element: // if the element moves, the comment moves with it. - let items = collect_dunder_all_items(lines, *range, locator); + let items = collect_dunder_all_items(lines, range, locator); - Some(DunderAllValue { + Some(MultilineDunderAllValue { items, - range: *range, - multiline: locator.contains_line_break(value.range()), + range, ends_with_trailing_comma, }) } - /// Implementation of the unstable [`&[T].is_sorted`] function. - /// See - fn is_already_sorted(&self) -> bool { - // tuple_windows() clones, - // but here that's okay: we're only cloning *references*, rather than the items themselves - for (this, next) in self.items.iter().tuple_windows() { - if next < this { - return false; - } - } - true - } - - /// Determine whether `__all__` is already sorted. - /// If it is not already sorted, attempt to sort `__all__`, - /// and return a string with the sorted `__all__ definition/augmentation` - /// that can be inserted into the source code as a range replacement. - fn into_sorted_source_code(self, locator: &Locator, stylist: &Stylist) -> SortedDunderAll { - // As well as saving us unnecessary work, - // returning early here also means that we can rely on the invariant - // throughout the rest of this function that both `items` and `sorted_items` - // have length of at least two. - let [first_item, .., last_item] = self.items.as_slice() else { - return SortedDunderAll::AlreadySorted; + /// Sort a multiline `__all__` definition + /// that is known to be unsorted. + fn into_sorted_source_code(mut self, locator: &Locator, stylist: &Stylist) -> String { + let (first_item_start, last_item_end) = match self.items.as_slice() { + [first_item, .., last_item] => (first_item.start(), last_item.end()), + _ => unreachable!( + "We shouldn't be attempting an autofix if `__all__` has < 2 elements, + as it cannot be unsorted in that situation." + ), }; - if self.is_already_sorted() { - return SortedDunderAll::AlreadySorted; - } // As well as the "items" in the `__all__` definition, // there is also a "prelude" and a "postlude": @@ -321,94 +368,106 @@ impl DunderAllValue { // __all__ = "foo", "bar", "baz" // ``` // - let prelude_end = { - let first_item_line_offset = locator.line_start(first_item.start()); - if first_item_line_offset == locator.line_start(self.start()) { - first_item.start() - } else { - first_item_line_offset - } - }; - let postlude_start = { - let last_item_line_offset = locator.line_end(last_item.end()); - if last_item_line_offset == locator.line_end(self.end()) { - last_item.end() - } else { - last_item_line_offset - } - }; - let mut prelude = Cow::Borrowed(locator.slice(TextRange::new(self.start(), prelude_end))); - let mut postlude = Cow::Borrowed(locator.slice(TextRange::new(postlude_start, self.end()))); - + let newline = stylist.line_ending().as_str(); let start_offset = self.start(); - let mut sorted_items = self.items; - sorted_items.sort(); - - let joined_items = if self.multiline { - let leading_indent = leading_indentation(locator.full_line(start_offset)); - let item_indent = format!("{}{}", leading_indent, stylist.indentation().as_str()); - let newline = stylist.line_ending().as_str(); - prelude = Cow::Owned(format!("{}{}", prelude.trim_end(), newline)); - postlude = fixup_postlude(postlude, newline, leading_indent, &item_indent); - join_multiline_dunder_all_items( - &sorted_items, - locator, - &item_indent, - newline, - self.ends_with_trailing_comma, - ) - } else { - join_singleline_dunder_all_items(&sorted_items, locator) - }; - - SortedDunderAll::Sorted(format!("{prelude}{joined_items}{postlude}")) + let leading_indent = leading_indentation(locator.full_line(start_offset)); + let item_indent = format!("{}{}", leading_indent, stylist.indentation().as_str()); + + let prelude = + multiline_dunder_all_prelude(first_item_start, newline, start_offset, locator); + let postlude = multiline_dunder_all_postlude( + last_item_end, + newline, + leading_indent, + &item_indent, + self.end(), + locator, + ); + + self.items + .sort_by_cached_key(|item| AllItemSortKey::from(item)); + let joined_items = join_multiline_dunder_all_items( + &self.items, + locator, + &item_indent, + newline, + self.ends_with_trailing_comma, + ); + + format!("{prelude}{joined_items}{postlude}") } } -impl Ranged for DunderAllValue { +impl Ranged for MultilineDunderAllValue { fn range(&self) -> TextRange { self.range } } -/// Fixup the postlude for a multiline `__all__` definition. -/// -/// Without the fixup, closing `)` or `]` characters -/// at the end of sorted `__all__` definitions can sometimes -/// have strange indentations. -fn fixup_postlude<'a>( - postlude: Cow<'a, str>, +fn multiline_dunder_all_prelude( + first_item_start_offset: TextSize, + newline: &str, + dunder_all_offset: TextSize, + locator: &Locator, +) -> String { + let prelude_end = { + let first_item_line_offset = locator.line_start(first_item_start_offset); + if first_item_line_offset == locator.line_start(dunder_all_offset) { + first_item_start_offset + } else { + first_item_line_offset + } + }; + let prelude = locator.slice(TextRange::new(dunder_all_offset, prelude_end)); + format!("{}{}", prelude.trim_end(), newline) +} + +fn dunder_all_is_already_sorted(string_elements: &[&str]) -> bool { + let mut element_iter = string_elements.iter(); + let Some(this) = element_iter.next() else { + return true; + }; + let mut this_key = AllItemSortKey::from(*this); + for next in element_iter { + let next_key = AllItemSortKey::from(*next); + if next_key < this_key { + return false; + } + this_key = next_key; + } + true +} + +fn multiline_dunder_all_postlude<'a>( + last_item_end_offset: TextSize, newline: &str, leading_indent: &str, item_indent: &str, + dunder_all_range_end: TextSize, + locator: &'a Locator, ) -> Cow<'a, str> { + let postlude_start = { + let last_item_line_offset = locator.line_end(last_item_end_offset); + if last_item_line_offset == locator.line_end(dunder_all_range_end) { + last_item_end_offset + } else { + last_item_line_offset + } + }; + let postlude = locator.slice(TextRange::new(postlude_start, dunder_all_range_end)); if !postlude.starts_with(newline) { - return postlude; + return Cow::Borrowed(postlude); } if TextSize::of(leading_indentation(postlude.trim_start_matches(newline))) <= TextSize::of(item_indent) { - return postlude; + return Cow::Borrowed(postlude); } let trimmed_postlude = postlude.trim_start(); if trimmed_postlude.starts_with(']') || trimmed_postlude.starts_with(')') { return Cow::Owned(format!("{newline}{leading_indent}{trimmed_postlude}")); } - postlude -} - -/// Variants of this enum are returned by `into_sorted_source_code()`. -/// -/// - `SortedDunderAll::AlreadySorted` is returned if `__all__` was -/// already sorted; this means no code rewriting is required. -/// - `SortedDunderAll::Sorted` is returned if `__all__` was not already -/// sorted. The string data attached to this variant is the source -/// code of the sorted `__all__`, that can be inserted into the source -/// code as a `range_replacement` autofix. -#[derive(Debug)] -enum SortedDunderAll { - AlreadySorted, - Sorted(String), + Cow::Borrowed(postlude) } /// Collect data on each line of `__all__`. @@ -645,7 +704,7 @@ fn collect_dunder_all_items( /// /// You'll notice that a very similar enum exists /// in ruff's reimplementation of isort. -#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone)] +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Copy)] enum InferredMemberType { Constant, Class, @@ -667,6 +726,48 @@ impl InferredMemberType { } } +struct AllItemSortKey { + category: InferredMemberType, + value: String, +} + +impl Ord for AllItemSortKey { + fn cmp(&self, other: &Self) -> Ordering { + self.category + .cmp(&other.category) + .then_with(|| natord::compare(&self.value, &other.value)) + } +} + +impl PartialOrd for AllItemSortKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for AllItemSortKey { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} + +impl Eq for AllItemSortKey {} + +impl From<&str> for AllItemSortKey { + fn from(value: &str) -> Self { + Self { + category: InferredMemberType::of(value), + value: String::from(value), + } + } +} + +impl From<&DunderAllItem> for AllItemSortKey { + fn from(item: &DunderAllItem) -> Self { + Self::from(item.value.as_str()) + } +} + /// An instance of this struct represents a single element /// from the original tuple/list, *and* any comments that /// are "attached" to it. The comments "attached" to the element @@ -705,7 +806,6 @@ impl InferredMemberType { #[derive(Debug)] struct DunderAllItem { value: String, - category: InferredMemberType, preceding_comment_ranges: Vec, element_range: TextRange, // total_range incorporates the ranges of preceding comments @@ -722,7 +822,6 @@ impl DunderAllItem { element_range: TextRange, end_of_line_comments: Option, ) -> Self { - let category = InferredMemberType::of(value.as_str()); let total_range = { if let Some(first_comment_range) = preceding_comment_ranges.first() { TextRange::new(first_comment_range.start(), element_range.end()) @@ -732,7 +831,6 @@ impl DunderAllItem { }; Self { value, - category, preceding_comment_ranges, element_range, total_range, @@ -751,35 +849,6 @@ impl Ranged for DunderAllItem { } } -impl Ord for DunderAllItem { - fn cmp(&self, other: &Self) -> Ordering { - self.category - .cmp(&other.category) - .then_with(|| natord::compare(&self.value, &other.value)) - } -} - -impl PartialOrd for DunderAllItem { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl PartialEq for DunderAllItem { - fn eq(&self, other: &Self) -> bool { - self.cmp(other) == Ordering::Equal - } -} - -impl Eq for DunderAllItem {} - -fn join_singleline_dunder_all_items(sorted_items: &[DunderAllItem], locator: &Locator) -> String { - sorted_items - .iter() - .map(|item| locator.slice(item)) - .join(", ") -} - fn join_multiline_dunder_all_items( sorted_items: &[DunderAllItem], locator: &Locator, diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index c7b11fce41c3a..5f29cc29a8742 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -39,7 +39,7 @@ RUF022.py:6:12: RUF022 [*] `__all__` is not sorted 6 |+__all__ += ["antipasti", "bar", "foo"] 7 7 | __all__ = ("d", "c", "b", "a") 8 8 | -9 9 | __all__: list = ["b", "c", "a",] # note the trailing comma, which is retained +9 9 | # Quoting style is retained, RUF022.py:7:11: RUF022 [*] `__all__` is not sorted | @@ -48,7 +48,7 @@ RUF022.py:7:11: RUF022 [*] `__all__` is not sorted 7 | __all__ = ("d", "c", "b", "a") | ^^^^^^^^^^^^^^^^^^^^ RUF022 8 | -9 | __all__: list = ["b", "c", "a",] # note the trailing comma, which is retained +9 | # Quoting style is retained, | = help: Apply an isort-style sorting to `__all__` @@ -59,756 +59,827 @@ RUF022.py:7:11: RUF022 [*] `__all__` is not sorted 7 |-__all__ = ("d", "c", "b", "a") 7 |+__all__ = ("a", "b", "c", "d") 8 8 | -9 9 | __all__: list = ["b", "c", "a",] # note the trailing comma, which is retained -10 10 | __all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained +9 9 | # Quoting style is retained, +10 10 | # but unnecessary parens are not -RUF022.py:9:17: RUF022 [*] `__all__` is not sorted +RUF022.py:11:17: RUF022 [*] `__all__` is not sorted | - 7 | __all__ = ("d", "c", "b", "a") - 8 | - 9 | __all__: list = ["b", "c", "a",] # note the trailing comma, which is retained - | ^^^^^^^^^^^^^^^^ RUF022 -10 | __all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained + 9 | # Quoting style is retained, +10 | # but unnecessary parens are not +11 | __all__: list = ['b', "c", ((('a')))] + | ^^^^^^^^^^^^^^^^^^^^^ RUF022 +12 | # Trailing commas are also not retained in single-line `__all__` definitions +13 | # (but they are in multiline `__all__` definitions) | = help: Apply an isort-style sorting to `__all__` ℹ Safe fix -6 6 | __all__ += ["foo", "bar", "antipasti"] -7 7 | __all__ = ("d", "c", "b", "a") 8 8 | -9 |-__all__: list = ["b", "c", "a",] # note the trailing comma, which is retained - 9 |+__all__: list = ["a", "b", "c",] # note the trailing comma, which is retained -10 10 | __all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained -11 11 | -12 12 | if bool(): - -RUF022.py:10:18: RUF022 [*] `__all__` is not sorted +9 9 | # Quoting style is retained, +10 10 | # but unnecessary parens are not +11 |-__all__: list = ['b', "c", ((('a')))] + 11 |+__all__: list = ['a', 'b', "c"] +12 12 | # Trailing commas are also not retained in single-line `__all__` definitions +13 13 | # (but they are in multiline `__all__` definitions) +14 14 | __all__: tuple = ("b", "c", "a",) + +RUF022.py:14:18: RUF022 [*] `__all__` is not sorted | - 9 | __all__: list = ["b", "c", "a",] # note the trailing comma, which is retained -10 | __all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained +12 | # Trailing commas are also not retained in single-line `__all__` definitions +13 | # (but they are in multiline `__all__` definitions) +14 | __all__: tuple = ("b", "c", "a",) | ^^^^^^^^^^^^^^^^ RUF022 -11 | -12 | if bool(): +15 | +16 | if bool(): | = help: Apply an isort-style sorting to `__all__` ℹ Safe fix -7 7 | __all__ = ("d", "c", "b", "a") -8 8 | -9 9 | __all__: list = ["b", "c", "a",] # note the trailing comma, which is retained -10 |-__all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained - 10 |+__all__: tuple = ("a", "b", "c",) # note the trailing comma, which is retained -11 11 | -12 12 | if bool(): -13 13 | __all__ += ("x", "m", "a", "s") - -RUF022.py:13:16: RUF022 [*] `__all__` is not sorted +11 11 | __all__: list = ['b', "c", ((('a')))] +12 12 | # Trailing commas are also not retained in single-line `__all__` definitions +13 13 | # (but they are in multiline `__all__` definitions) +14 |-__all__: tuple = ("b", "c", "a",) + 14 |+__all__: tuple = ("a", "b", "c") +15 15 | +16 16 | if bool(): +17 17 | __all__ += ("x", "m", "a", "s") + +RUF022.py:17:16: RUF022 [*] `__all__` is not sorted | -12 | if bool(): -13 | __all__ += ("x", "m", "a", "s") +16 | if bool(): +17 | __all__ += ("x", "m", "a", "s") | ^^^^^^^^^^^^^^^^^^^^ RUF022 -14 | else: -15 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +18 | else: +19 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) | = help: Apply an isort-style sorting to `__all__` ℹ Safe fix -10 10 | __all__: tuple = ("b", "c", "a",) # note the trailing comma, which is retained -11 11 | -12 12 | if bool(): -13 |- __all__ += ("x", "m", "a", "s") - 13 |+ __all__ += ("a", "m", "s", "x") -14 14 | else: -15 15 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) -16 16 | - -RUF022.py:15:16: RUF022 [*] `__all__` is not sorted +14 14 | __all__: tuple = ("b", "c", "a",) +15 15 | +16 16 | if bool(): +17 |- __all__ += ("x", "m", "a", "s") + 17 |+ __all__ += ("a", "m", "s", "x") +18 18 | else: +19 19 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +20 20 | + +RUF022.py:19:16: RUF022 [*] `__all__` is not sorted | -13 | __all__ += ("x", "m", "a", "s") -14 | else: -15 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +17 | __all__ += ("x", "m", "a", "s") +18 | else: +19 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) | ^^^^^^^^^^^^^^^^^^^^^^ RUF022 -16 | -17 | __all__: list[str] = ["the", "three", "little", "pigs"] +20 | +21 | __all__: list[str] = ["the", "three", "little", "pigs"] | = help: Apply an isort-style sorting to `__all__` ℹ Safe fix -12 12 | if bool(): -13 13 | __all__ += ("x", "m", "a", "s") -14 14 | else: -15 |- __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) - 15 |+ __all__ += "foo1", "foo2", "foo3" # NB: an implicit tuple (without parens) -16 16 | -17 17 | __all__: list[str] = ["the", "three", "little", "pigs"] -18 18 | - -RUF022.py:17:22: RUF022 [*] `__all__` is not sorted +16 16 | if bool(): +17 17 | __all__ += ("x", "m", "a", "s") +18 18 | else: +19 |- __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) + 19 |+ __all__ += "foo1", "foo2", "foo3" # NB: an implicit tuple (without parens) +20 20 | +21 21 | __all__: list[str] = ["the", "three", "little", "pigs"] +22 22 | + +RUF022.py:21:22: RUF022 [*] `__all__` is not sorted | -15 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) -16 | -17 | __all__: list[str] = ["the", "three", "little", "pigs"] +19 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +20 | +21 | __all__: list[str] = ["the", "three", "little", "pigs"] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF022 -18 | -19 | #################################### +22 | +23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") | = help: Apply an isort-style sorting to `__all__` ℹ Safe fix -14 14 | else: -15 15 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) -16 16 | -17 |-__all__: list[str] = ["the", "three", "little", "pigs"] - 17 |+__all__: list[str] = ["little", "pigs", "the", "three"] -18 18 | -19 19 | #################################### -20 20 | # Neat multiline __all__ definitions +18 18 | else: +19 19 | __all__ += "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens) +20 20 | +21 |-__all__: list[str] = ["the", "three", "little", "pigs"] + 21 |+__all__: list[str] = ["little", "pigs", "the", "three"] +22 22 | +23 23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") +24 24 | __all__.extend(["foo", "bar"]) RUF022.py:23:11: RUF022 [*] `__all__` is not sorted | -21 | #################################### -22 | -23 | __all__ = ( +21 | __all__: list[str] = ["the", "three", "little", "pigs"] +22 | +23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF022 +24 | __all__.extend(["foo", "bar"]) +25 | __all__.extend(("foo", "bar")) + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +20 20 | +21 21 | __all__: list[str] = ["the", "three", "little", "pigs"] +22 22 | +23 |-__all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") + 23 |+__all__ = "an_unparenthesized_tuple", "in", "parenthesized_item" +24 24 | __all__.extend(["foo", "bar"]) +25 25 | __all__.extend(("foo", "bar")) +26 26 | __all__.extend((((["foo", "bar"])))) + +RUF022.py:24:16: RUF022 [*] `__all__` is not sorted + | +23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") +24 | __all__.extend(["foo", "bar"]) + | ^^^^^^^^^^^^^^ RUF022 +25 | __all__.extend(("foo", "bar")) +26 | __all__.extend((((["foo", "bar"])))) + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +21 21 | __all__: list[str] = ["the", "three", "little", "pigs"] +22 22 | +23 23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") +24 |-__all__.extend(["foo", "bar"]) + 24 |+__all__.extend(["bar", "foo"]) +25 25 | __all__.extend(("foo", "bar")) +26 26 | __all__.extend((((["foo", "bar"])))) +27 27 | + +RUF022.py:25:16: RUF022 [*] `__all__` is not sorted + | +23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") +24 | __all__.extend(["foo", "bar"]) +25 | __all__.extend(("foo", "bar")) + | ^^^^^^^^^^^^^^ RUF022 +26 | __all__.extend((((["foo", "bar"])))) + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +22 22 | +23 23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") +24 24 | __all__.extend(["foo", "bar"]) +25 |-__all__.extend(("foo", "bar")) + 25 |+__all__.extend(("bar", "foo")) +26 26 | __all__.extend((((["foo", "bar"])))) +27 27 | +28 28 | #################################### + +RUF022.py:26:19: RUF022 [*] `__all__` is not sorted + | +24 | __all__.extend(["foo", "bar"]) +25 | __all__.extend(("foo", "bar")) +26 | __all__.extend((((["foo", "bar"])))) + | ^^^^^^^^^^^^^^ RUF022 +27 | +28 | #################################### + | + = help: Apply an isort-style sorting to `__all__` + +ℹ Safe fix +23 23 | __all__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple") +24 24 | __all__.extend(["foo", "bar"]) +25 25 | __all__.extend(("foo", "bar")) +26 |-__all__.extend((((["foo", "bar"])))) + 26 |+__all__.extend((((["bar", "foo"])))) +27 27 | +28 28 | #################################### +29 29 | # Neat multiline __all__ definitions + +RUF022.py:32:11: RUF022 [*] `__all__` is not sorted + | +30 | #################################### +31 | +32 | __all__ = ( | ___________^ -24 | | "d0", -25 | | "c0", # a comment regarding 'c0' -26 | | "b0", -27 | | # a comment regarding 'a0': -28 | | "a0" -29 | | ) +33 | | "d0", +34 | | "c0", # a comment regarding 'c0' +35 | | "b0", +36 | | # a comment regarding 'a0': +37 | | "a0" +38 | | ) | |_^ RUF022 -30 | -31 | __all__ = [ +39 | +40 | __all__ = [ | = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -21 21 | #################################### -22 22 | -23 23 | __all__ = ( -24 |- "d0", - 24 |+ # a comment regarding 'a0': - 25 |+ "a0", - 26 |+ "b0", -25 27 | "c0", # a comment regarding 'c0' -26 |- "b0", -27 |- # a comment regarding 'a0': -28 |- "a0" - 28 |+ "d0" -29 29 | ) -30 30 | -31 31 | __all__ = [ - -RUF022.py:31:11: RUF022 [*] `__all__` is not sorted +30 30 | #################################### +31 31 | +32 32 | __all__ = ( +33 |- "d0", + 33 |+ # a comment regarding 'a0': + 34 |+ "a0", + 35 |+ "b0", +34 36 | "c0", # a comment regarding 'c0' +35 |- "b0", +36 |- # a comment regarding 'a0': +37 |- "a0" + 37 |+ "d0" +38 38 | ) +39 39 | +40 40 | __all__ = [ + +RUF022.py:40:11: RUF022 [*] `__all__` is not sorted | -29 | ) -30 | -31 | __all__ = [ +38 | ) +39 | +40 | __all__ = [ | ___________^ -32 | | "d", -33 | | "c", # a comment regarding 'c' -34 | | "b", -35 | | # a comment regarding 'a': -36 | | "a" -37 | | ] +41 | | "d", +42 | | "c", # a comment regarding 'c' +43 | | "b", +44 | | # a comment regarding 'a': +45 | | "a" +46 | | ] | |_^ RUF022 -38 | -39 | # we implement an "isort-style sort": +47 | +48 | # we implement an "isort-style sort": | = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -29 29 | ) -30 30 | -31 31 | __all__ = [ -32 |- "d", - 32 |+ # a comment regarding 'a': - 33 |+ "a", - 34 |+ "b", -33 35 | "c", # a comment regarding 'c' -34 |- "b", -35 |- # a comment regarding 'a': -36 |- "a" - 36 |+ "d" -37 37 | ] -38 38 | -39 39 | # we implement an "isort-style sort": - -RUF022.py:45:11: RUF022 [*] `__all__` is not sorted +38 38 | ) +39 39 | +40 40 | __all__ = [ +41 |- "d", + 41 |+ # a comment regarding 'a': + 42 |+ "a", + 43 |+ "b", +42 44 | "c", # a comment regarding 'c' +43 |- "b", +44 |- # a comment regarding 'a': +45 |- "a" + 45 |+ "d" +46 46 | ] +47 47 | +48 48 | # we implement an "isort-style sort": + +RUF022.py:54:11: RUF022 [*] `__all__` is not sorted | -43 | # This (which is currently alphabetically sorted) -44 | # should get reordered accordingly: -45 | __all__ = [ +52 | # This (which is currently alphabetically sorted) +53 | # should get reordered accordingly: +54 | __all__ = [ | ___________^ -46 | | "APRIL", -47 | | "AUGUST", -48 | | "Calendar", -49 | | "DECEMBER", -50 | | "Day", -51 | | "FEBRUARY", -52 | | "FRIDAY", -53 | | "HTMLCalendar", -54 | | "IllegalMonthError", -55 | | "JANUARY", -56 | | "JULY", -57 | | "JUNE", -58 | | "LocaleHTMLCalendar", -59 | | "MARCH", -60 | | "MAY", -61 | | "MONDAY", -62 | | "Month", -63 | | "NOVEMBER", -64 | | "OCTOBER", -65 | | "SATURDAY", -66 | | "SEPTEMBER", -67 | | "SUNDAY", -68 | | "THURSDAY", -69 | | "TUESDAY", -70 | | "TextCalendar", -71 | | "WEDNESDAY", -72 | | "calendar", -73 | | "timegm", -74 | | "weekday", -75 | | "weekheader"] +55 | | "APRIL", +56 | | "AUGUST", +57 | | "Calendar", +58 | | "DECEMBER", +59 | | "Day", +60 | | "FEBRUARY", +61 | | "FRIDAY", +62 | | "HTMLCalendar", +63 | | "IllegalMonthError", +64 | | "JANUARY", +65 | | "JULY", +66 | | "JUNE", +67 | | "LocaleHTMLCalendar", +68 | | "MARCH", +69 | | "MAY", +70 | | "MONDAY", +71 | | "Month", +72 | | "NOVEMBER", +73 | | "OCTOBER", +74 | | "SATURDAY", +75 | | "SEPTEMBER", +76 | | "SUNDAY", +77 | | "THURSDAY", +78 | | "TUESDAY", +79 | | "TextCalendar", +80 | | "WEDNESDAY", +81 | | "calendar", +82 | | "timegm", +83 | | "weekday", +84 | | "weekheader"] | |_________________^ RUF022 -76 | -77 | ########################################## +85 | +86 | ########################################## | = help: Apply an isort-style sorting to `__all__` ℹ Safe fix -45 45 | __all__ = [ -46 46 | "APRIL", -47 47 | "AUGUST", -48 |- "Calendar", -49 48 | "DECEMBER", -50 |- "Day", -51 49 | "FEBRUARY", -52 50 | "FRIDAY", -53 |- "HTMLCalendar", -54 |- "IllegalMonthError", -55 51 | "JANUARY", -56 52 | "JULY", -57 53 | "JUNE", -58 |- "LocaleHTMLCalendar", -59 54 | "MARCH", -60 55 | "MAY", -61 56 | "MONDAY", -62 |- "Month", -63 57 | "NOVEMBER", -64 58 | "OCTOBER", -65 59 | "SATURDAY", +54 54 | __all__ = [ +55 55 | "APRIL", +56 56 | "AUGUST", +57 |- "Calendar", +58 57 | "DECEMBER", +59 |- "Day", +60 58 | "FEBRUARY", +61 59 | "FRIDAY", +62 |- "HTMLCalendar", +63 |- "IllegalMonthError", +64 60 | "JANUARY", +65 61 | "JULY", +66 62 | "JUNE", +67 |- "LocaleHTMLCalendar", +68 63 | "MARCH", +69 64 | "MAY", +70 65 | "MONDAY", +71 |- "Month", +72 66 | "NOVEMBER", +73 67 | "OCTOBER", +74 68 | "SATURDAY", -------------------------------------------------------------------------------- -67 61 | "SUNDAY", -68 62 | "THURSDAY", -69 63 | "TUESDAY", - 64 |+ "WEDNESDAY", - 65 |+ "Calendar", - 66 |+ "Day", - 67 |+ "HTMLCalendar", - 68 |+ "IllegalMonthError", - 69 |+ "LocaleHTMLCalendar", - 70 |+ "Month", -70 71 | "TextCalendar", -71 |- "WEDNESDAY", -72 72 | "calendar", -73 73 | "timegm", -74 74 | "weekday", - -RUF022.py:82:11: RUF022 [*] `__all__` is not sorted +76 70 | "SUNDAY", +77 71 | "THURSDAY", +78 72 | "TUESDAY", + 73 |+ "WEDNESDAY", + 74 |+ "Calendar", + 75 |+ "Day", + 76 |+ "HTMLCalendar", + 77 |+ "IllegalMonthError", + 78 |+ "LocaleHTMLCalendar", + 79 |+ "Month", +79 80 | "TextCalendar", +80 |- "WEDNESDAY", +81 81 | "calendar", +82 82 | "timegm", +83 83 | "weekday", + +RUF022.py:91:11: RUF022 [*] `__all__` is not sorted | -81 | # comment0 -82 | __all__ = ("d", "a", # comment1 +90 | # comment0 +91 | __all__ = ("d", "a", # comment1 | ___________^ -83 | | # comment2 -84 | | "f", "b", -85 | | "strangely", # comment3 -86 | | # comment4 -87 | | "formatted", -88 | | # comment5 -89 | | ) # comment6 +92 | | # comment2 +93 | | "f", "b", +94 | | "strangely", # comment3 +95 | | # comment4 +96 | | "formatted", +97 | | # comment5 +98 | | ) # comment6 | |_^ RUF022 -90 | # comment7 +99 | # comment7 | = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -79 79 | ########################################## -80 80 | -81 81 | # comment0 -82 |-__all__ = ("d", "a", # comment1 -83 |- # comment2 -84 |- "f", "b", -85 |- "strangely", # comment3 -86 |- # comment4 - 82 |+__all__ = ( - 83 |+ "a", - 84 |+ "b", - 85 |+ "d", # comment1 - 86 |+ # comment2 - 87 |+ "f", - 88 |+ # comment4 -87 89 | "formatted", - 90 |+ "strangely", # comment3 -88 91 | # comment5 -89 92 | ) # comment6 -90 93 | # comment7 - -RUF022.py:92:11: RUF022 [*] `__all__` is not sorted +88 88 | ########################################## +89 89 | +90 90 | # comment0 +91 |-__all__ = ("d", "a", # comment1 +92 |- # comment2 +93 |- "f", "b", +94 |- "strangely", # comment3 +95 |- # comment4 + 91 |+__all__ = ( + 92 |+ "a", + 93 |+ "b", + 94 |+ "d", # comment1 + 95 |+ # comment2 + 96 |+ "f", + 97 |+ # comment4 +96 98 | "formatted", + 99 |+ "strangely", # comment3 +97 100 | # comment5 +98 101 | ) # comment6 +99 102 | # comment7 + +RUF022.py:101:11: RUF022 [*] `__all__` is not sorted | - 90 | # comment7 - 91 | - 92 | __all__ = [ # comment0 + 99 | # comment7 +100 | +101 | __all__ = [ # comment0 | ___________^ - 93 | | # comment1 - 94 | | # comment2 - 95 | | "dx", "cx", "bx", "ax" # comment3 - 96 | | # comment4 - 97 | | # comment5 - 98 | | # comment6 - 99 | | ] # comment7 +102 | | # comment1 +103 | | # comment2 +104 | | "dx", "cx", "bx", "ax" # comment3 +105 | | # comment4 +106 | | # comment5 +107 | | # comment6 +108 | | ] # comment7 | |_^ RUF022 -100 | -101 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", +109 | +110 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", | = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -90 90 | # comment7 -91 91 | -92 92 | __all__ = [ # comment0 - 93 |+ "ax", - 94 |+ "bx", - 95 |+ "cx", -93 96 | # comment1 -94 97 | # comment2 -95 |- "dx", "cx", "bx", "ax" # comment3 - 98 |+ "dx" # comment3 -96 99 | # comment4 -97 100 | # comment5 -98 101 | # comment6 - -RUF022.py:101:11: RUF022 [*] `__all__` is not sorted +99 99 | # comment7 +100 100 | +101 101 | __all__ = [ # comment0 + 102 |+ "ax", + 103 |+ "bx", + 104 |+ "cx", +102 105 | # comment1 +103 106 | # comment2 +104 |- "dx", "cx", "bx", "ax" # comment3 + 107 |+ "dx" # comment3 +105 108 | # comment4 +106 109 | # comment5 +107 110 | # comment6 + +RUF022.py:110:11: RUF022 [*] `__all__` is not sorted | - 99 | ] # comment7 -100 | -101 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", +108 | ] # comment7 +109 | +110 | __all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", | ___________^ -102 | | "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", -103 | | "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", -104 | | "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", -105 | | "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", -106 | | "StreamReader", "StreamWriter", -107 | | "StreamReaderWriter", "StreamRecoder", -108 | | "getencoder", "getdecoder", "getincrementalencoder", -109 | | "getincrementaldecoder", "getreader", "getwriter", -110 | | "encode", "decode", "iterencode", "iterdecode", -111 | | "strict_errors", "ignore_errors", "replace_errors", -112 | | "xmlcharrefreplace_errors", -113 | | "backslashreplace_errors", "namereplace_errors", -114 | | "register_error", "lookup_error"] +111 | | "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", +112 | | "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", +113 | | "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", +114 | | "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", +115 | | "StreamReader", "StreamWriter", +116 | | "StreamReaderWriter", "StreamRecoder", +117 | | "getencoder", "getdecoder", "getincrementalencoder", +118 | | "getincrementaldecoder", "getreader", "getwriter", +119 | | "encode", "decode", "iterencode", "iterdecode", +120 | | "strict_errors", "ignore_errors", "replace_errors", +121 | | "xmlcharrefreplace_errors", +122 | | "backslashreplace_errors", "namereplace_errors", +123 | | "register_error", "lookup_error"] | |____________________________________________^ RUF022 -115 | -116 | __all__: tuple[str, ...] = ( # a comment about the opening paren +124 | +125 | __all__: tuple[str, ...] = ( # a comment about the opening paren | = help: Apply an isort-style sorting to `__all__` ℹ Safe fix -98 98 | # comment6 -99 99 | ] # comment7 -100 100 | -101 |-__all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", -102 |- "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", -103 |- "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", -104 |- "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", -105 |- "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", -106 |- "StreamReader", "StreamWriter", -107 |- "StreamReaderWriter", "StreamRecoder", -108 |- "getencoder", "getdecoder", "getincrementalencoder", -109 |- "getincrementaldecoder", "getreader", "getwriter", -110 |- "encode", "decode", "iterencode", "iterdecode", -111 |- "strict_errors", "ignore_errors", "replace_errors", -112 |- "xmlcharrefreplace_errors", -113 |- "backslashreplace_errors", "namereplace_errors", -114 |- "register_error", "lookup_error"] - 101 |+__all__ = [ - 102 |+ "BOM", - 103 |+ "BOM32_BE", - 104 |+ "BOM32_LE", - 105 |+ "BOM64_BE", - 106 |+ "BOM64_LE", - 107 |+ "BOM_BE", - 108 |+ "BOM_LE", - 109 |+ "BOM_UTF8", - 110 |+ "BOM_UTF16", - 111 |+ "BOM_UTF16_BE", - 112 |+ "BOM_UTF16_LE", - 113 |+ "BOM_UTF32", - 114 |+ "BOM_UTF32_BE", - 115 |+ "BOM_UTF32_LE", - 116 |+ "Codec", - 117 |+ "CodecInfo", - 118 |+ "EncodedFile", - 119 |+ "IncrementalDecoder", - 120 |+ "IncrementalEncoder", - 121 |+ "StreamReader", - 122 |+ "StreamReaderWriter", - 123 |+ "StreamRecoder", - 124 |+ "StreamWriter", - 125 |+ "backslashreplace_errors", - 126 |+ "decode", - 127 |+ "encode", - 128 |+ "getdecoder", - 129 |+ "getencoder", - 130 |+ "getincrementaldecoder", - 131 |+ "getincrementalencoder", - 132 |+ "getreader", - 133 |+ "getwriter", - 134 |+ "ignore_errors", - 135 |+ "iterdecode", - 136 |+ "iterencode", - 137 |+ "lookup", - 138 |+ "lookup_error", - 139 |+ "namereplace_errors", - 140 |+ "open", - 141 |+ "register", - 142 |+ "register_error", - 143 |+ "replace_errors", - 144 |+ "strict_errors", - 145 |+ "xmlcharrefreplace_errors"] -115 146 | -116 147 | __all__: tuple[str, ...] = ( # a comment about the opening paren -117 148 | # multiline comment about "bbb" part 1 - -RUF022.py:116:28: RUF022 [*] `__all__` is not sorted +107 107 | # comment6 +108 108 | ] # comment7 +109 109 | +110 |-__all__ = ["register", "lookup", "open", "EncodedFile", "BOM", "BOM_BE", +111 |- "BOM_LE", "BOM32_BE", "BOM32_LE", "BOM64_BE", "BOM64_LE", +112 |- "BOM_UTF8", "BOM_UTF16", "BOM_UTF16_LE", "BOM_UTF16_BE", +113 |- "BOM_UTF32", "BOM_UTF32_LE", "BOM_UTF32_BE", +114 |- "CodecInfo", "Codec", "IncrementalEncoder", "IncrementalDecoder", +115 |- "StreamReader", "StreamWriter", +116 |- "StreamReaderWriter", "StreamRecoder", +117 |- "getencoder", "getdecoder", "getincrementalencoder", +118 |- "getincrementaldecoder", "getreader", "getwriter", +119 |- "encode", "decode", "iterencode", "iterdecode", +120 |- "strict_errors", "ignore_errors", "replace_errors", +121 |- "xmlcharrefreplace_errors", +122 |- "backslashreplace_errors", "namereplace_errors", +123 |- "register_error", "lookup_error"] + 110 |+__all__ = [ + 111 |+ "BOM", + 112 |+ "BOM32_BE", + 113 |+ "BOM32_LE", + 114 |+ "BOM64_BE", + 115 |+ "BOM64_LE", + 116 |+ "BOM_BE", + 117 |+ "BOM_LE", + 118 |+ "BOM_UTF8", + 119 |+ "BOM_UTF16", + 120 |+ "BOM_UTF16_BE", + 121 |+ "BOM_UTF16_LE", + 122 |+ "BOM_UTF32", + 123 |+ "BOM_UTF32_BE", + 124 |+ "BOM_UTF32_LE", + 125 |+ "Codec", + 126 |+ "CodecInfo", + 127 |+ "EncodedFile", + 128 |+ "IncrementalDecoder", + 129 |+ "IncrementalEncoder", + 130 |+ "StreamReader", + 131 |+ "StreamReaderWriter", + 132 |+ "StreamRecoder", + 133 |+ "StreamWriter", + 134 |+ "backslashreplace_errors", + 135 |+ "decode", + 136 |+ "encode", + 137 |+ "getdecoder", + 138 |+ "getencoder", + 139 |+ "getincrementaldecoder", + 140 |+ "getincrementalencoder", + 141 |+ "getreader", + 142 |+ "getwriter", + 143 |+ "ignore_errors", + 144 |+ "iterdecode", + 145 |+ "iterencode", + 146 |+ "lookup", + 147 |+ "lookup_error", + 148 |+ "namereplace_errors", + 149 |+ "open", + 150 |+ "register", + 151 |+ "register_error", + 152 |+ "replace_errors", + 153 |+ "strict_errors", + 154 |+ "xmlcharrefreplace_errors"] +124 155 | +125 156 | __all__: tuple[str, ...] = ( # a comment about the opening paren +126 157 | # multiline comment about "bbb" part 1 + +RUF022.py:125:28: RUF022 [*] `__all__` is not sorted | -114 | "register_error", "lookup_error"] -115 | -116 | __all__: tuple[str, ...] = ( # a comment about the opening paren +123 | "register_error", "lookup_error"] +124 | +125 | __all__: tuple[str, ...] = ( # a comment about the opening paren | ____________________________^ -117 | | # multiline comment about "bbb" part 1 -118 | | # multiline comment about "bbb" part 2 -119 | | "bbb", -120 | | # multiline comment about "aaa" part 1 -121 | | # multiline comment about "aaa" part 2 -122 | | "aaa", -123 | | ) +126 | | # multiline comment about "bbb" part 1 +127 | | # multiline comment about "bbb" part 2 +128 | | "bbb", +129 | | # multiline comment about "aaa" part 1 +130 | | # multiline comment about "aaa" part 2 +131 | | "aaa", +132 | | ) | |_^ RUF022 -124 | -125 | # we use natural sort for `__all__`, +133 | +134 | # we use natural sort for `__all__`, | = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -114 114 | "register_error", "lookup_error"] -115 115 | -116 116 | __all__: tuple[str, ...] = ( # a comment about the opening paren - 117 |+ # multiline comment about "aaa" part 1 - 118 |+ # multiline comment about "aaa" part 2 - 119 |+ "aaa", -117 120 | # multiline comment about "bbb" part 1 -118 121 | # multiline comment about "bbb" part 2 -119 122 | "bbb", -120 |- # multiline comment about "aaa" part 1 -121 |- # multiline comment about "aaa" part 2 -122 |- "aaa", -123 123 | ) +123 123 | "register_error", "lookup_error"] 124 124 | -125 125 | # we use natural sort for `__all__`, - -RUF022.py:129:11: RUF022 [*] `__all__` is not sorted +125 125 | __all__: tuple[str, ...] = ( # a comment about the opening paren + 126 |+ # multiline comment about "aaa" part 1 + 127 |+ # multiline comment about "aaa" part 2 + 128 |+ "aaa", +126 129 | # multiline comment about "bbb" part 1 +127 130 | # multiline comment about "bbb" part 2 +128 131 | "bbb", +129 |- # multiline comment about "aaa" part 1 +130 |- # multiline comment about "aaa" part 2 +131 |- "aaa", +132 132 | ) +133 133 | +134 134 | # we use natural sort for `__all__`, + +RUF022.py:138:11: RUF022 [*] `__all__` is not sorted | -127 | # Also, this doesn't end with a trailing comma, -128 | # so the autofix shouldn't introduce one: -129 | __all__ = ( +136 | # Also, this doesn't end with a trailing comma, +137 | # so the autofix shouldn't introduce one: +138 | __all__ = ( | ___________^ -130 | | "aadvark237", -131 | | "aadvark10092", -132 | | "aadvark174", # the very long whitespace span before this comment is retained -133 | | "aadvark532" # the even longer whitespace span before this comment is retained -134 | | ) +139 | | "aadvark237", +140 | | "aadvark10092", +141 | | "aadvark174", # the very long whitespace span before this comment is retained +142 | | "aadvark532" # the even longer whitespace span before this comment is retained +143 | | ) | |_^ RUF022 -135 | -136 | __all__.extend(["foo", "bar"]) +144 | +145 | __all__.extend(( # comment0 | = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -127 127 | # Also, this doesn't end with a trailing comma, -128 128 | # so the autofix shouldn't introduce one: -129 129 | __all__ = ( - 130 |+ "aadvark174", # the very long whitespace span before this comment is retained -130 131 | "aadvark237", -131 |- "aadvark10092", -132 |- "aadvark174", # the very long whitespace span before this comment is retained -133 |- "aadvark532" # the even longer whitespace span before this comment is retained - 132 |+ "aadvark532", # the even longer whitespace span before this comment is retained - 133 |+ "aadvark10092" -134 134 | ) -135 135 | -136 136 | __all__.extend(["foo", "bar"]) - -RUF022.py:136:16: RUF022 [*] `__all__` is not sorted +136 136 | # Also, this doesn't end with a trailing comma, +137 137 | # so the autofix shouldn't introduce one: +138 138 | __all__ = ( + 139 |+ "aadvark174", # the very long whitespace span before this comment is retained +139 140 | "aadvark237", +140 |- "aadvark10092", +141 |- "aadvark174", # the very long whitespace span before this comment is retained +142 |- "aadvark532" # the even longer whitespace span before this comment is retained + 141 |+ "aadvark532", # the even longer whitespace span before this comment is retained + 142 |+ "aadvark10092" +143 143 | ) +144 144 | +145 145 | __all__.extend(( # comment0 + +RUF022.py:145:16: RUF022 [*] `__all__` is not sorted | -134 | ) -135 | -136 | __all__.extend(["foo", "bar"]) - | ^^^^^^^^^^^^^^ RUF022 -137 | __all__.extend(("foo", "bar")) -138 | __all__.extend(( # comment0 - | - = help: Apply an isort-style sorting to `__all__` - -ℹ Safe fix -133 133 | "aadvark532" # the even longer whitespace span before this comment is retained -134 134 | ) -135 135 | -136 |-__all__.extend(["foo", "bar"]) - 136 |+__all__.extend(["bar", "foo"]) -137 137 | __all__.extend(("foo", "bar")) -138 138 | __all__.extend(( # comment0 -139 139 | # comment about foo - -RUF022.py:137:16: RUF022 [*] `__all__` is not sorted - | -136 | __all__.extend(["foo", "bar"]) -137 | __all__.extend(("foo", "bar")) - | ^^^^^^^^^^^^^^ RUF022 -138 | __all__.extend(( # comment0 -139 | # comment about foo - | - = help: Apply an isort-style sorting to `__all__` - -ℹ Safe fix -134 134 | ) -135 135 | -136 136 | __all__.extend(["foo", "bar"]) -137 |-__all__.extend(("foo", "bar")) - 137 |+__all__.extend(("bar", "foo")) -138 138 | __all__.extend(( # comment0 -139 139 | # comment about foo -140 140 | "foo", # comment about foo - -RUF022.py:138:16: RUF022 [*] `__all__` is not sorted - | -136 | __all__.extend(["foo", "bar"]) -137 | __all__.extend(("foo", "bar")) -138 | __all__.extend(( # comment0 +143 | ) +144 | +145 | __all__.extend(( # comment0 | ________________^ -139 | | # comment about foo -140 | | "foo", # comment about foo -141 | | # comment about bar -142 | | "bar" # comment about bar -143 | | # comment1 -144 | | )) # comment2 +146 | | # comment about foo +147 | | "foo", # comment about foo +148 | | # comment about bar +149 | | "bar" # comment about bar +150 | | # comment1 +151 | | )) # comment2 | |_^ RUF022 -145 | -146 | __all__.extend( # comment0 +152 | +153 | __all__.extend( # comment0 | = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -136 136 | __all__.extend(["foo", "bar"]) -137 137 | __all__.extend(("foo", "bar")) -138 138 | __all__.extend(( # comment0 - 139 |+ # comment about bar - 140 |+ "bar", # comment about bar -139 141 | # comment about foo -140 |- "foo", # comment about foo -141 |- # comment about bar -142 |- "bar" # comment about bar - 142 |+ "foo" # comment about foo -143 143 | # comment1 -144 144 | )) # comment2 -145 145 | - -RUF022.py:148:5: RUF022 [*] `__all__` is not sorted +143 143 | ) +144 144 | +145 145 | __all__.extend(( # comment0 + 146 |+ # comment about bar + 147 |+ "bar", # comment about bar +146 148 | # comment about foo +147 |- "foo", # comment about foo +148 |- # comment about bar +149 |- "bar" # comment about bar + 149 |+ "foo" # comment about foo +150 150 | # comment1 +151 151 | )) # comment2 +152 152 | + +RUF022.py:155:5: RUF022 [*] `__all__` is not sorted | -146 | __all__.extend( # comment0 -147 | # comment1 -148 | ( # comment2 +153 | __all__.extend( # comment0 +154 | # comment1 +155 | ( # comment2 | _____^ -149 | | # comment about foo -150 | | "foo", # comment about foo -151 | | # comment about bar -152 | | "bar" # comment about bar -153 | | # comment3 -154 | | ) # comment4 +156 | | # comment about foo +157 | | "foo", # comment about foo +158 | | # comment about bar +159 | | "bar" # comment about bar +160 | | # comment3 +161 | | ) # comment4 | |_____^ RUF022 -155 | ) # comment2 +162 | ) # comment2 | = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -146 146 | __all__.extend( # comment0 -147 147 | # comment1 -148 148 | ( # comment2 - 149 |+ # comment about bar - 150 |+ "bar", # comment about bar -149 151 | # comment about foo -150 |- "foo", # comment about foo -151 |- # comment about bar -152 |- "bar" # comment about bar - 152 |+ "foo" # comment about foo -153 153 | # comment3 -154 154 | ) # comment4 -155 155 | ) # comment2 - -RUF022.py:157:16: RUF022 [*] `__all__` is not sorted +153 153 | __all__.extend( # comment0 +154 154 | # comment1 +155 155 | ( # comment2 + 156 |+ # comment about bar + 157 |+ "bar", # comment about bar +156 158 | # comment about foo +157 |- "foo", # comment about foo +158 |- # comment about bar +159 |- "bar" # comment about bar + 159 |+ "foo" # comment about foo +160 160 | # comment3 +161 161 | ) # comment4 +162 162 | ) # comment2 + +RUF022.py:164:16: RUF022 [*] `__all__` is not sorted | -155 | ) # comment2 -156 | -157 | __all__.extend([ # comment0 +162 | ) # comment2 +163 | +164 | __all__.extend([ # comment0 | ________________^ -158 | | # comment about foo -159 | | "foo", # comment about foo -160 | | # comment about bar -161 | | "bar" # comment about bar -162 | | # comment1 -163 | | ]) # comment2 +165 | | # comment about foo +166 | | "foo", # comment about foo +167 | | # comment about bar +168 | | "bar" # comment about bar +169 | | # comment1 +170 | | ]) # comment2 | |_^ RUF022 -164 | -165 | __all__.extend( # comment0 +171 | +172 | __all__.extend( # comment0 | = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -155 155 | ) # comment2 -156 156 | -157 157 | __all__.extend([ # comment0 - 158 |+ # comment about bar - 159 |+ "bar", # comment about bar -158 160 | # comment about foo -159 |- "foo", # comment about foo -160 |- # comment about bar -161 |- "bar" # comment about bar - 161 |+ "foo" # comment about foo -162 162 | # comment1 -163 163 | ]) # comment2 -164 164 | - -RUF022.py:167:5: RUF022 [*] `__all__` is not sorted +162 162 | ) # comment2 +163 163 | +164 164 | __all__.extend([ # comment0 + 165 |+ # comment about bar + 166 |+ "bar", # comment about bar +165 167 | # comment about foo +166 |- "foo", # comment about foo +167 |- # comment about bar +168 |- "bar" # comment about bar + 168 |+ "foo" # comment about foo +169 169 | # comment1 +170 170 | ]) # comment2 +171 171 | + +RUF022.py:174:5: RUF022 [*] `__all__` is not sorted | -165 | __all__.extend( # comment0 -166 | # comment1 -167 | [ # comment2 +172 | __all__.extend( # comment0 +173 | # comment1 +174 | [ # comment2 | _____^ -168 | | # comment about foo -169 | | "foo", # comment about foo -170 | | # comment about bar -171 | | "bar" # comment about bar -172 | | # comment3 -173 | | ] # comment4 +175 | | # comment about foo +176 | | "foo", # comment about foo +177 | | # comment about bar +178 | | "bar" # comment about bar +179 | | # comment3 +180 | | ] # comment4 | |_____^ RUF022 -174 | ) # comment2 +181 | ) # comment2 | = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -165 165 | __all__.extend( # comment0 -166 166 | # comment1 -167 167 | [ # comment2 - 168 |+ # comment about bar - 169 |+ "bar", # comment about bar -168 170 | # comment about foo -169 |- "foo", # comment about foo -170 |- # comment about bar -171 |- "bar" # comment about bar - 171 |+ "foo" # comment about foo -172 172 | # comment3 -173 173 | ] # comment4 -174 174 | ) # comment2 - -RUF022.py:176:11: RUF022 [*] `__all__` is not sorted +172 172 | __all__.extend( # comment0 +173 173 | # comment1 +174 174 | [ # comment2 + 175 |+ # comment about bar + 176 |+ "bar", # comment about bar +175 177 | # comment about foo +176 |- "foo", # comment about foo +177 |- # comment about bar +178 |- "bar" # comment about bar + 178 |+ "foo" # comment about foo +179 179 | # comment3 +180 180 | ] # comment4 +181 181 | ) # comment2 + +RUF022.py:183:11: RUF022 [*] `__all__` is not sorted | -174 | ) # comment2 -175 | -176 | __all__ = ["Style", "Treeview", +181 | ) # comment2 +182 | +183 | __all__ = ["Style", "Treeview", | ___________^ -177 | | # Extensions -178 | | "LabeledScale", "OptionMenu", -179 | | ] +184 | | # Extensions +185 | | "LabeledScale", "OptionMenu", +186 | | ] | |_^ RUF022 -180 | -181 | __all__ = ["Awaitable", "Coroutine", +187 | +188 | __all__ = ["Awaitable", "Coroutine", | = help: Apply an isort-style sorting to `__all__` ℹ Unsafe fix -173 173 | ] # comment4 -174 174 | ) # comment2 -175 175 | -176 |-__all__ = ["Style", "Treeview", -177 |- # Extensions -178 |- "LabeledScale", "OptionMenu", - 176 |+__all__ = [ - 177 |+ # Extensions - 178 |+ "LabeledScale", - 179 |+ "OptionMenu", - 180 |+ "Style", - 181 |+ "Treeview", -179 182 | ] -180 183 | -181 184 | __all__ = ["Awaitable", "Coroutine", - -RUF022.py:181:11: RUF022 [*] `__all__` is not sorted +180 180 | ] # comment4 +181 181 | ) # comment2 +182 182 | +183 |-__all__ = ["Style", "Treeview", +184 |- # Extensions +185 |- "LabeledScale", "OptionMenu", + 183 |+__all__ = [ + 184 |+ # Extensions + 185 |+ "LabeledScale", + 186 |+ "OptionMenu", + 187 |+ "Style", + 188 |+ "Treeview", +186 189 | ] +187 190 | +188 191 | __all__ = ["Awaitable", "Coroutine", + +RUF022.py:188:11: RUF022 [*] `__all__` is not sorted | -179 | ] -180 | -181 | __all__ = ["Awaitable", "Coroutine", +186 | ] +187 | +188 | __all__ = ["Awaitable", "Coroutine", | ___________^ -182 | | "AsyncIterable", "AsyncIterator", "AsyncGenerator", -183 | | ] +189 | | "AsyncIterable", "AsyncIterator", "AsyncGenerator", +190 | | ] | |____________^ RUF022 -184 | -185 | __all__ = [ +191 | +192 | __all__ = [ | = help: Apply an isort-style sorting to `__all__` ℹ Safe fix -178 178 | "LabeledScale", "OptionMenu", -179 179 | ] -180 180 | -181 |-__all__ = ["Awaitable", "Coroutine", -182 |- "AsyncIterable", "AsyncIterator", "AsyncGenerator", -183 |- ] - 181 |+__all__ = [ - 182 |+ "AsyncGenerator", - 183 |+ "AsyncIterable", - 184 |+ "AsyncIterator", - 185 |+ "Awaitable", - 186 |+ "Coroutine", - 187 |+] -184 188 | -185 189 | __all__ = [ -186 190 | "foo", - -RUF022.py:185:11: RUF022 [*] `__all__` is not sorted +185 185 | "LabeledScale", "OptionMenu", +186 186 | ] +187 187 | +188 |-__all__ = ["Awaitable", "Coroutine", +189 |- "AsyncIterable", "AsyncIterator", "AsyncGenerator", +190 |- ] + 188 |+__all__ = [ + 189 |+ "AsyncGenerator", + 190 |+ "AsyncIterable", + 191 |+ "AsyncIterator", + 192 |+ "Awaitable", + 193 |+ "Coroutine", + 194 |+] +191 195 | +192 196 | __all__ = [ +193 197 | "foo", + +RUF022.py:192:11: RUF022 [*] `__all__` is not sorted | -183 | ] -184 | -185 | __all__ = [ +190 | ] +191 | +192 | __all__ = [ | ___________^ -186 | | "foo", -187 | | "bar", -188 | | "baz", -189 | | ] +193 | | "foo", +194 | | "bar", +195 | | "baz", +196 | | ] | |_____^ RUF022 -190 | -191 | ################################### +197 | +198 | ######################################################################### | = help: Apply an isort-style sorting to `__all__` ℹ Safe fix -183 183 | ] -184 184 | -185 185 | __all__ = [ -186 |- "foo", -187 186 | "bar", -188 187 | "baz", - 188 |+ "foo", -189 189 | ] -190 190 | -191 191 | ################################### +190 190 | ] +191 191 | +192 192 | __all__ = [ +193 |- "foo", +194 193 | "bar", +195 194 | "baz", + 195 |+ "foo", +196 196 | ] +197 197 | +198 198 | ######################################################################### + +RUF022.py:204:11: RUF022 `__all__` is not sorted + | +202 | ######################################################################### +203 | +204 | __all__ = ( + | ___________^ +205 | | "look", +206 | | ( +207 | | "a_veeeeeeeeeeeeeeeeeeery_long_parenthesized_item" +208 | | ), +209 | | ) + | |_^ RUF022 +210 | +211 | __all__ = ("don't" "care" "about", "__all__" "with", "concatenated" "strings") + | + = help: Apply an isort-style sorting to `__all__` + +RUF022.py:211:11: RUF022 `__all__` is not sorted + | +209 | ) +210 | +211 | __all__ = ("don't" "care" "about", "__all__" "with", "concatenated" "strings") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF022 +212 | +213 | ################################### + | + = help: Apply an isort-style sorting to `__all__` From fe10c1b489b447d86ac4f0fbec99c6adb41bac69 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 15 Jan 2024 17:01:17 +0000 Subject: [PATCH 61/77] More Micha comments --- .../resources/test/fixtures/ruff/RUF022.py | 17 + .../src/rules/ruff/rules/sort_dunder_all.rs | 543 +++++++++--------- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 30 +- 3 files changed, 326 insertions(+), 264 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index af9003963f709..45ad1f327c366 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -208,6 +208,14 @@ ), ) +__all__ = ( + "b", + (( + "c" + )), + "a" +) + __all__ = ("don't" "care" "about", "__all__" "with", "concatenated" "strings") ################################### @@ -273,3 +281,12 @@ class IntroducesNonModuleScope: "duplicate_element", # comment2 "duplicate_element", # comment0 ) + +__all__ =[[]] +__all__ [()] +__all__ = (()) +__all__ = ([]) +__all__ = ((),) +__all__ = ([],) +__all__ = ("foo", [], "bar") +__all__ = ["foo", (), "bar"] diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index d7c017cff230b..5d25d8f0bf0e9 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -133,97 +133,6 @@ pub(crate) fn sort_dunder_all_ann_assign(checker: &mut Checker, node: &ast::Stmt } } -/// Return `true` if a tuple is parenthesized in the source code. -/// -/// (Yes, this function is shamelessly copied from the formatter.) -fn is_tuple_parenthesized(tuple: &ast::ExprTuple, source: &str) -> bool { - let Some(elt) = tuple.elts.first() else { - return true; - }; - - // Count the number of open parentheses between the start of the tuple and the first element. - let open_parentheses_count = - SimpleTokenizer::new(source, TextRange::new(tuple.start(), elt.start())) - .skip_trivia() - .filter(|token| token.kind() == SimpleTokenKind::LParen) - .count(); - if open_parentheses_count == 0 { - return false; - } - - // Count the number of parentheses between the end of the first element and its trailing comma. - let close_parentheses_count = - SimpleTokenizer::new(source, TextRange::new(elt.end(), tuple.end())) - .skip_trivia() - .take_while(|token| token.kind() != SimpleTokenKind::Comma) - .filter(|token| token.kind() == SimpleTokenKind::RParen) - .count(); - - // If the number of open parentheses is greater than the number of close parentheses, the tuple - // is parenthesized. - open_parentheses_count > close_parentheses_count -} - -fn sort_single_line_dunder_all( - elts: &[ast::Expr], - elements: &[&str], - kind: &DunderAllKind, - locator: &Locator, -) -> String { - let mut element_pairs = elts.iter().zip(elements).collect_vec(); - element_pairs.sort_by_cached_key(|(_, elem)| AllItemSortKey::from(**elem)); - let joined_items = element_pairs - .iter() - .map(|(elt, _)| locator.slice(elt)) - .join(", "); - match kind { - DunderAllKind::List => format!("[{joined_items}]"), - DunderAllKind::Tuple(tuple_node) => { - if is_tuple_parenthesized(tuple_node, locator.contents()) { - format!("({joined_items})") - } else { - joined_items - } - } - } -} - -enum DunderAllKind<'a> { - List, - Tuple(&'a ast::ExprTuple), -} - -fn get_fix( - range: TextRange, - elts: &[ast::Expr], - string_items: &[&str], - kind: &DunderAllKind, - checker: &Checker, -) -> Option { - let locator = checker.locator(); - let is_multiline = locator.contains_line_break(range); - - let sorted_source_code = { - if is_multiline { - MultilineDunderAllValue::from_source_range(range, locator)? - .into_sorted_source_code(locator, checker.stylist()) - } else { - sort_single_line_dunder_all(elts, string_items, kind, locator) - } - }; - - let applicability = { - if is_multiline && checker.indexer().comment_ranges().intersects(range) { - Applicability::Unsafe - } else { - Applicability::Safe - } - }; - - let edit = Edit::range_replacement(sorted_source_code, range); - Some(Fix::applicable_edit(edit, applicability)) -} - fn sort_dunder_all(checker: &mut Checker, target: &ast::Expr, node: &ast::Expr) { let ast::Expr::Name(ast::ExprName { id, .. }) = target else { return; @@ -274,6 +183,117 @@ fn sort_dunder_all(checker: &mut Checker, target: &ast::Expr, node: &ast::Expr) checker.diagnostics.push(diagnostic); } +enum DunderAllKind<'a> { + List, + Tuple(&'a ast::ExprTuple), +} + +impl DunderAllKind<'_> { + fn opening_token_for_multiline_definition(&self) -> Tok { + match self { + Self::List => Tok::Lsqb, + Self::Tuple(_) => Tok::Lpar, + } + } + + fn closing_token_for_multiline_definition(&self) -> Tok { + match self { + Self::List => Tok::Rsqb, + Self::Tuple(_) => Tok::Rpar, + } + } +} + +fn dunder_all_is_already_sorted(string_elements: &[&str]) -> bool { + let mut element_iter = string_elements.iter(); + let Some(this) = element_iter.next() else { + return true; + }; + let mut this_key = AllItemSortKey::from(*this); + for next in element_iter { + let next_key = AllItemSortKey::from(*next); + if next_key < this_key { + return false; + } + this_key = next_key; + } + true +} + +struct AllItemSortKey { + category: InferredMemberType, + value: String, +} + +impl Ord for AllItemSortKey { + fn cmp(&self, other: &Self) -> Ordering { + self.category + .cmp(&other.category) + .then_with(|| natord::compare(&self.value, &other.value)) + } +} + +impl PartialOrd for AllItemSortKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for AllItemSortKey { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} + +impl Eq for AllItemSortKey {} + +impl From<&str> for AllItemSortKey { + fn from(value: &str) -> Self { + Self { + category: InferredMemberType::of(value), + value: String::from(value), + } + } +} + +impl From<&DunderAllItem> for AllItemSortKey { + fn from(item: &DunderAllItem) -> Self { + Self::from(item.value.as_str()) + } +} + +fn get_fix( + range: TextRange, + elts: &[ast::Expr], + string_items: &[&str], + kind: &DunderAllKind, + checker: &Checker, +) -> Option { + let locator = checker.locator(); + let is_multiline = locator.contains_line_break(range); + + let sorted_source_code = { + if is_multiline { + let value = MultilineDunderAllValue::from_source_range(range, kind, locator)?; + assert_eq!(value.items.len(), elts.len()); + value.into_sorted_source_code(locator, checker.stylist()) + } else { + sort_single_line_dunder_all(elts, string_items, kind, locator) + } + }; + + let applicability = { + if is_multiline && checker.indexer().comment_ranges().intersects(range) { + Applicability::Unsafe + } else { + Applicability::Safe + } + }; + + let edit = Edit::range_replacement(sorted_source_code, range); + Some(Fix::applicable_edit(edit, applicability)) +} + /// An instance of this struct encapsulates an analysis /// of a Python tuple/list that represents an `__all__` /// definition or augmentation. @@ -288,17 +308,20 @@ impl MultilineDunderAllValue { /// definition or augmentation. Return `None` if the analysis fails /// for whatever reason, or if it looks like we're not actually looking at a /// tuple/list after all. - fn from_source_range(range: TextRange, locator: &Locator) -> Option { - // Parse the `__all__` definition using the raw tokens. + fn from_source_range( + range: TextRange, + kind: &DunderAllKind, + locator: &Locator, + ) -> Option { + // Parse the multiline `__all__` definition using the raw tokens. // See the docs for `collect_dunder_all_lines()` for why we have to // use the raw tokens, rather than just the AST, to do this parsing. // // Step (1). Start by collecting information on each line individually: - let (lines, ends_with_trailing_comma) = collect_dunder_all_lines(range, locator)?; + let (lines, ends_with_trailing_comma) = collect_dunder_all_lines(range, kind, locator)?; // Step (2). Group lines together into sortable "items": // - Any "item" contains a single element of the `__all__` list/tuple - // - "Items" are ordered according to the element they contain // - Assume that any comments on their own line are meant to be grouped // with the element immediately below them: if the element moves, // the comments above the element move with it. @@ -319,18 +342,17 @@ impl MultilineDunderAllValue { let (first_item_start, last_item_end) = match self.items.as_slice() { [first_item, .., last_item] => (first_item.start(), last_item.end()), _ => unreachable!( - "We shouldn't be attempting an autofix if `__all__` has < 2 elements, - as it cannot be unsorted in that situation." + "We shouldn't be attempting an autofix if `__all__` has < 2 elements; + an `__all__` definition with 1 or 0 elements cannot be unsorted." ), }; // As well as the "items" in the `__all__` definition, // there is also a "prelude" and a "postlude": - // - Prelude == the region of source code from the opening parenthesis - // (if there was one), up to the start of the first item in `__all__`. + // - Prelude == the region of source code from the opening parenthesis, + // up to the start of the first item in `__all__`. // - Postlude == the region of source code from the end of the last - // item in `__all__` up to and including the closing parenthesis - // (if there was one). + // item in `__all__` up to and including the closing parenthesis. // // For example: // @@ -360,14 +382,6 @@ impl MultilineDunderAllValue { // but `# comment3` becomes part of the postlude because there are no items // below it. // - // "Prelude" and "postlude" could both possibly be empty strings, for example - // in a situation like this, where there is neither an opening parenthesis - // nor a closing parenthesis: - // - // ```python - // __all__ = "foo", "bar", "baz" - // ``` - // let newline = stylist.line_ending().as_str(); let start_offset = self.start(); let leading_indent = leading_indentation(locator.full_line(start_offset)); @@ -404,79 +418,14 @@ impl Ranged for MultilineDunderAllValue { } } -fn multiline_dunder_all_prelude( - first_item_start_offset: TextSize, - newline: &str, - dunder_all_offset: TextSize, - locator: &Locator, -) -> String { - let prelude_end = { - let first_item_line_offset = locator.line_start(first_item_start_offset); - if first_item_line_offset == locator.line_start(dunder_all_offset) { - first_item_start_offset - } else { - first_item_line_offset - } - }; - let prelude = locator.slice(TextRange::new(dunder_all_offset, prelude_end)); - format!("{}{}", prelude.trim_end(), newline) -} - -fn dunder_all_is_already_sorted(string_elements: &[&str]) -> bool { - let mut element_iter = string_elements.iter(); - let Some(this) = element_iter.next() else { - return true; - }; - let mut this_key = AllItemSortKey::from(*this); - for next in element_iter { - let next_key = AllItemSortKey::from(*next); - if next_key < this_key { - return false; - } - this_key = next_key; - } - true -} - -fn multiline_dunder_all_postlude<'a>( - last_item_end_offset: TextSize, - newline: &str, - leading_indent: &str, - item_indent: &str, - dunder_all_range_end: TextSize, - locator: &'a Locator, -) -> Cow<'a, str> { - let postlude_start = { - let last_item_line_offset = locator.line_end(last_item_end_offset); - if last_item_line_offset == locator.line_end(dunder_all_range_end) { - last_item_end_offset - } else { - last_item_line_offset - } - }; - let postlude = locator.slice(TextRange::new(postlude_start, dunder_all_range_end)); - if !postlude.starts_with(newline) { - return Cow::Borrowed(postlude); - } - if TextSize::of(leading_indentation(postlude.trim_start_matches(newline))) - <= TextSize::of(item_indent) - { - return Cow::Borrowed(postlude); - } - let trimmed_postlude = postlude.trim_start(); - if trimmed_postlude.starts_with(']') || trimmed_postlude.starts_with(')') { - return Cow::Owned(format!("{newline}{leading_indent}{trimmed_postlude}")); - } - Cow::Borrowed(postlude) -} - -/// Collect data on each line of `__all__`. +/// Collect data on each line of a multiline `__all__` definition. /// Return `None` if `__all__` appears to be invalid, /// or if it's an edge case we don't support. /// /// Why do we need to do this using the raw tokens, /// when we already have the AST? The AST strips out -/// crucial information that we need to track here, such as: +/// crucial information that we need to track here for +/// a multiline `__all__` definition, such as: /// - The value of comments /// - The amount of whitespace between the end of a line /// and an inline comment @@ -489,11 +438,11 @@ fn multiline_dunder_all_postlude<'a>( /// in the original source code. fn collect_dunder_all_lines( range: TextRange, + kind: &DunderAllKind, locator: &Locator, ) -> Option<(Vec, bool)> { - // These first three variables are used for keeping track of state + // These first two variables are used for keeping track of state // regarding the entirety of the `__all__` definition... - let mut parentheses_open = false; let mut ends_with_trailing_comma = false; let mut lines = vec![]; // ... all state regarding a single line of an `__all__` definition @@ -503,38 +452,19 @@ fn collect_dunder_all_lines( // `lex_starts_at()` gives us absolute ranges rather than relative ranges, // but (surprisingly) we still need to pass in the slice of code we want it to lex, // rather than the whole source file: - for pair in lexer::lex_starts_at(locator.slice(range), Mode::Expression, range.start()) { + let mut token_iter = + lexer::lex_starts_at(locator.slice(range), Mode::Expression, range.start()); + let (first_tok, _) = token_iter.next()?.ok()?; + if first_tok != kind.opening_token_for_multiline_definition() { + return None; + } + let expected_final_token = kind.closing_token_for_multiline_definition(); + + for pair in token_iter { let (tok, subrange) = pair.ok()?; match tok { - // If exactly one `Lpar` or `Lsqb` is encountered, that's fine - // -- a valid __all__ definition has to be a list or tuple, - // and most (though not all) lists/tuples start with either a `(` or a `[`. - // - // Any more than one `(` or `[` in an `__all__` definition, however, - // indicates that we've got something here that's just too complex - // for us to handle. Maybe a string element in `__all__` is parenthesized; - // maybe the `__all__` definition is in fact invalid syntax; - // maybe there's some other thing going on that we haven't anticipated. - // - // Whatever the case -- if we encounter more than one `(` or `[`, - // we evidently don't know what to do here. So just return `None` to - // signal failure. - Tok::Lpar | Tok::Lsqb => { - if parentheses_open { - return None; - } - parentheses_open = true; - } - Tok::Rpar | Tok::Rsqb | Tok::Newline => { - if let Some(line) = line_state.into_dunder_all_line() { - lines.push(line); - } - break; - } Tok::NonLogicalNewline => { - if let Some(line) = line_state.into_dunder_all_line() { - lines.push(line); - } + lines.push(line_state.into_dunder_all_line()); line_state = LineState::default(); } Tok::Comment(_) => { @@ -548,6 +478,10 @@ fn collect_dunder_all_lines( line_state.visit_comma_token(subrange); ends_with_trailing_comma = true; } + tok if tok == expected_final_token => { + lines.push(line_state.into_dunder_all_line()); + break; + } _ => return None, } } @@ -558,6 +492,21 @@ fn collect_dunder_all_lines( /// regarding a single line in an `__all__` definition. /// It is purely internal to `collect_dunder_all_lines()`, /// and should not be used outside that function. +/// +/// There are three possible kinds of line in a multiline +/// `__all__` definition, and we don't know what kind of a line +/// we're in until all tokens in that line have been processed: +/// +/// - A line with just a comment (`DunderAllLine::JustAComment)`) +/// - A line with one or more string items in it (`DunderAllLine::OneOrMoreItems`) +/// - An empty line (`DunderAllLine::Empty`) +/// +/// As we process the tokens in a single line, +/// this struct accumulates the necessary state for us +/// to be able to determine what kind of a line we're in. +/// Once the entire line has been processed, `into_dunder_all_line()` +/// is called, which consumes `self` and produces the +/// classification for the line. #[derive(Debug, Default)] struct LineState { first_item_in_line: Option<(String, TextRange)>, @@ -581,6 +530,16 @@ impl LineState { self.comment_range_start = Some(token_range.end()); } + /// If this is a comment on its own line, + /// record the range of that comment. + /// + /// *If*, however, we've already seen a comma + /// or a stringin this line, that means that we're + /// in a line with items. In that case, we want to + /// record the range of the comment, *plus* the whitespace + /// preceding the comment. This is so that we don't + /// unnecessarily apply opinionated formatting changes + /// where they might not be welcome. fn visit_comment_token(&mut self, token_range: TextRange) { self.comment_in_line = { if let Some(comment_range_start) = self.comment_range_start { @@ -591,17 +550,18 @@ impl LineState { } } - fn into_dunder_all_line(self) -> Option { + fn into_dunder_all_line(self) -> DunderAllLine { if let Some(first_item) = self.first_item_in_line { - Some(DunderAllLine::OneOrMoreItems(LineWithItems { + DunderAllLine::OneOrMoreItems(LineWithItems { first_item, following_items: self.following_items_in_line, trailing_comment_range: self.comment_in_line, - })) - } else { - self.comment_in_line.map(|comment_range| { - DunderAllLine::JustAComment(LineWithJustAComment(comment_range)) }) + } else { + self.comment_in_line + .map_or(DunderAllLine::Empty, |comment_range| { + DunderAllLine::JustAComment(LineWithJustAComment(comment_range)) + }) } } } @@ -637,9 +597,12 @@ impl LineWithItems { enum DunderAllLine { JustAComment(LineWithJustAComment), OneOrMoreItems(LineWithItems), + Empty, } -/// Given data on each line in `__all__`, group lines together into "items". +/// Given data on each line in a multiline `__all__` definition, +/// group lines together into "items". +/// /// Each item contains exactly one string element, /// but might contain multiple comments attached to that element /// that must move with the element when `__all__` is sorted. @@ -690,6 +653,7 @@ fn collect_dunder_all_items( all_items.push(DunderAllItem::with_no_comments(value, range)); } } + DunderAllLine::Empty => continue, // discard empty lines } } all_items @@ -726,50 +690,8 @@ impl InferredMemberType { } } -struct AllItemSortKey { - category: InferredMemberType, - value: String, -} - -impl Ord for AllItemSortKey { - fn cmp(&self, other: &Self) -> Ordering { - self.category - .cmp(&other.category) - .then_with(|| natord::compare(&self.value, &other.value)) - } -} - -impl PartialOrd for AllItemSortKey { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl PartialEq for AllItemSortKey { - fn eq(&self, other: &Self) -> bool { - self.cmp(other) == Ordering::Equal - } -} - -impl Eq for AllItemSortKey {} - -impl From<&str> for AllItemSortKey { - fn from(value: &str) -> Self { - Self { - category: InferredMemberType::of(value), - value: String::from(value), - } - } -} - -impl From<&DunderAllItem> for AllItemSortKey { - fn from(item: &DunderAllItem) -> Self { - Self::from(item.value.as_str()) - } -} - /// An instance of this struct represents a single element -/// from the original tuple/list, *and* any comments that +/// from a multiline `__all__` tuple/list, *and* any comments that /// are "attached" to it. The comments "attached" to the element /// will move with the element when the `__all__` tuple/list is sorted. /// @@ -849,6 +771,24 @@ impl Ranged for DunderAllItem { } } +fn multiline_dunder_all_prelude( + first_item_start_offset: TextSize, + newline: &str, + dunder_all_offset: TextSize, + locator: &Locator, +) -> String { + let prelude_end = { + let first_item_line_offset = locator.line_start(first_item_start_offset); + if first_item_line_offset == locator.line_start(dunder_all_offset) { + first_item_start_offset + } else { + first_item_line_offset + } + }; + let prelude = locator.slice(TextRange::new(dunder_all_offset, prelude_end)); + format!("{}{}", prelude.trim_end(), newline) +} + fn join_multiline_dunder_all_items( sorted_items: &[DunderAllItem], locator: &Locator, @@ -880,3 +820,90 @@ fn join_multiline_dunder_all_items( } new_dunder_all } + +fn multiline_dunder_all_postlude<'a>( + last_item_end_offset: TextSize, + newline: &str, + leading_indent: &str, + item_indent: &str, + dunder_all_range_end: TextSize, + locator: &'a Locator, +) -> Cow<'a, str> { + let postlude_start = { + let last_item_line_offset = locator.line_end(last_item_end_offset); + if last_item_line_offset == locator.line_end(dunder_all_range_end) { + last_item_end_offset + } else { + last_item_line_offset + } + }; + let postlude = locator.slice(TextRange::new(postlude_start, dunder_all_range_end)); + if !postlude.starts_with(newline) { + return Cow::Borrowed(postlude); + } + if TextSize::of(leading_indentation(postlude.trim_start_matches(newline))) + <= TextSize::of(item_indent) + { + return Cow::Borrowed(postlude); + } + let trimmed_postlude = postlude.trim_start(); + if trimmed_postlude.starts_with(']') || trimmed_postlude.starts_with(')') { + return Cow::Owned(format!("{newline}{leading_indent}{trimmed_postlude}")); + } + Cow::Borrowed(postlude) +} + +fn sort_single_line_dunder_all( + elts: &[ast::Expr], + elements: &[&str], + kind: &DunderAllKind, + locator: &Locator, +) -> String { + let mut element_pairs = elts.iter().zip(elements).collect_vec(); + element_pairs.sort_by_cached_key(|(_, elem)| AllItemSortKey::from(**elem)); + let joined_items = element_pairs + .iter() + .map(|(elt, _)| locator.slice(elt)) + .join(", "); + match kind { + DunderAllKind::List => format!("[{joined_items}]"), + DunderAllKind::Tuple(tuple_node) => { + if is_tuple_parenthesized(tuple_node, locator.contents()) { + format!("({joined_items})") + } else { + joined_items + } + } + } +} + +/// Return `true` if a tuple is parenthesized in the source code. +/// +/// (Yes, this function is shamelessly copied from the formatter.) +fn is_tuple_parenthesized(tuple: &ast::ExprTuple, source: &str) -> bool { + let Some(elt) = tuple.elts.first() else { + return true; + }; + + // Count the number of open parentheses between the start of the tuple and the first element. + let open_parentheses_count = + SimpleTokenizer::new(source, TextRange::new(tuple.start(), elt.start())) + .skip_trivia() + .filter(|token| token.kind() == SimpleTokenKind::LParen) + .count(); + if open_parentheses_count == 0 { + return false; + } + + // Count the number of parentheses between the end of the first element and its trailing comma. + let close_parentheses_count = + SimpleTokenizer::new(source, TextRange::new(elt.end(), tuple.end())) + .skip_trivia() + .take_while(|token| token.kind() != SimpleTokenKind::Comma) + .filter(|token| token.kind() == SimpleTokenKind::RParen) + .count(); + + // If the number of open parentheses is greater than the number of close parentheses, the tuple + // is parenthesized. + open_parentheses_count > close_parentheses_count +} diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index 5f29cc29a8742..e0f9974c111b7 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -867,18 +867,36 @@ RUF022.py:204:11: RUF022 `__all__` is not sorted 209 | | ) | |_^ RUF022 210 | -211 | __all__ = ("don't" "care" "about", "__all__" "with", "concatenated" "strings") +211 | __all__ = ( | = help: Apply an isort-style sorting to `__all__` RUF022.py:211:11: RUF022 `__all__` is not sorted | -209 | ) -210 | -211 | __all__ = ("don't" "care" "about", "__all__" "with", "concatenated" "strings") +209 | ) +210 | +211 | __all__ = ( + | ___________^ +212 | | "b", +213 | | (( +214 | | "c" +215 | | )), +216 | | "a" +217 | | ) + | |_^ RUF022 +218 | +219 | __all__ = ("don't" "care" "about", "__all__" "with", "concatenated" "strings") + | + = help: Apply an isort-style sorting to `__all__` + +RUF022.py:219:11: RUF022 `__all__` is not sorted + | +217 | ) +218 | +219 | __all__ = ("don't" "care" "about", "__all__" "with", "concatenated" "strings") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF022 -212 | -213 | ################################### +220 | +221 | ################################### | = help: Apply an isort-style sorting to `__all__` From d5d5ce1ad0c3b9d4ce3b3a5003ef4469852bab23 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 15 Jan 2024 17:14:44 +0000 Subject: [PATCH 62/77] Deduplicate function that now exists on `main` --- .../src/rules/ruff/rules/sort_dunder_all.rs | 106 +++++++----------- 1 file changed, 40 insertions(+), 66 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 5d25d8f0bf0e9..eff4a0dec4df1 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -7,7 +7,7 @@ use ruff_python_ast as ast; use ruff_python_codegen::Stylist; use ruff_python_parser::{lexer, Mode, Tok}; use ruff_python_stdlib::str::is_cased_uppercase; -use ruff_python_trivia::{leading_indentation, SimpleTokenKind, SimpleTokenizer}; +use ruff_python_trivia::leading_indentation; use ruff_source_file::Locator; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -163,7 +163,7 @@ fn sort_dunder_all(checker: &mut Checker, target: &ast::Expr, node: &ast::Expr) return; }; // If any strings are implicitly concatenated, don't bother trying to autofix - if possibly_fixable && string_literal.value.is_implicit_concatenated() { + if string_literal.value.is_implicit_concatenated() { possibly_fixable = false; } string_items.push(string_literal.value.to_str()); @@ -262,6 +262,37 @@ impl From<&DunderAllItem> for AllItemSortKey { } } +/// Classification for an element in `__all__`. +/// +/// This is necessary to achieve an "isort-style" sort, +/// where elements are sorted first by category, +/// then, within categories, are sorted according +/// to a natural sort. +/// +/// You'll notice that a very similar enum exists +/// in ruff's reimplementation of isort. +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Copy)] +enum InferredMemberType { + Constant, + Class, + Other, +} + +impl InferredMemberType { + fn of(value: &str) -> Self { + // E.g. `CONSTANT` + if value.len() > 1 && is_cased_uppercase(value) { + Self::Constant + // E.g. `Class` + } else if value.chars().next().is_some_and(char::is_uppercase) { + Self::Class + // E.g. `some_variable` or `some_function` + } else { + Self::Other + } + } +} + fn get_fix( range: TextRange, elts: &[ast::Expr], @@ -338,10 +369,15 @@ impl MultilineDunderAllValue { /// Sort a multiline `__all__` definition /// that is known to be unsorted. + /// + /// Panics if this is called and `self.items` + /// has length < 2. It's redundant to call this method in this case, + /// since lists with < 2 items cannot be unsorted, + /// so this is a logic error. fn into_sorted_source_code(mut self, locator: &Locator, stylist: &Stylist) -> String { let (first_item_start, last_item_end) = match self.items.as_slice() { [first_item, .., last_item] => (first_item.start(), last_item.end()), - _ => unreachable!( + _ => panic!( "We shouldn't be attempting an autofix if `__all__` has < 2 elements; an `__all__` definition with 1 or 0 elements cannot be unsorted." ), @@ -659,37 +695,6 @@ fn collect_dunder_all_items( all_items } -/// Classification for an element in `__all__`. -/// -/// This is necessary to achieve an "isort-style" sort, -/// where elements are sorted first by category, -/// then, within categories, are sorted according -/// to a natural sort. -/// -/// You'll notice that a very similar enum exists -/// in ruff's reimplementation of isort. -#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Copy)] -enum InferredMemberType { - Constant, - Class, - Other, -} - -impl InferredMemberType { - fn of(value: &str) -> Self { - // E.g. `CONSTANT` - if value.len() > 1 && is_cased_uppercase(value) { - Self::Constant - // E.g. `Class` - } else if value.chars().next().is_some_and(char::is_uppercase) { - Self::Class - // E.g. `some_variable` or `some_function` - } else { - Self::Other - } - } -} - /// An instance of this struct represents a single element /// from a multiline `__all__` tuple/list, *and* any comments that /// are "attached" to it. The comments "attached" to the element @@ -868,7 +873,7 @@ fn sort_single_line_dunder_all( match kind { DunderAllKind::List => format!("[{joined_items}]"), DunderAllKind::Tuple(tuple_node) => { - if is_tuple_parenthesized(tuple_node, locator.contents()) { + if tuple_node.is_parenthesized(locator.contents()) { format!("({joined_items})") } else { joined_items @@ -876,34 +881,3 @@ fn sort_single_line_dunder_all( } } } - -/// Return `true` if a tuple is parenthesized in the source code. -/// -/// (Yes, this function is shamelessly copied from the formatter.) -fn is_tuple_parenthesized(tuple: &ast::ExprTuple, source: &str) -> bool { - let Some(elt) = tuple.elts.first() else { - return true; - }; - - // Count the number of open parentheses between the start of the tuple and the first element. - let open_parentheses_count = - SimpleTokenizer::new(source, TextRange::new(tuple.start(), elt.start())) - .skip_trivia() - .filter(|token| token.kind() == SimpleTokenKind::LParen) - .count(); - if open_parentheses_count == 0 { - return false; - } - - // Count the number of parentheses between the end of the first element and its trailing comma. - let close_parentheses_count = - SimpleTokenizer::new(source, TextRange::new(elt.end(), tuple.end())) - .skip_trivia() - .take_while(|token| token.kind() != SimpleTokenKind::Comma) - .filter(|token| token.kind() == SimpleTokenKind::RParen) - .count(); - - // If the number of open parentheses is greater than the number of close parentheses, the tuple - // is parenthesized. - open_parentheses_count > close_parentheses_count -} From 93da9c23b91659963742348385a329b7bafef661 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 15 Jan 2024 18:24:05 +0000 Subject: [PATCH 63/77] fix comment following refactor --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index eff4a0dec4df1..be2c22cc09509 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -335,10 +335,9 @@ struct MultilineDunderAllValue { } impl MultilineDunderAllValue { - /// Analyse an AST node for a Python tuple/list that represents an `__all__` + /// Analyse the source range for a Python tuple/list that represents an `__all__` /// definition or augmentation. Return `None` if the analysis fails - /// for whatever reason, or if it looks like we're not actually looking at a - /// tuple/list after all. + /// for whatever reason. fn from_source_range( range: TextRange, kind: &DunderAllKind, From d78e931ef1c7cf71946a4ce63ea5669aa2b27dac Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 15 Jan 2024 23:19:15 +0000 Subject: [PATCH 64/77] Make some comments more precise --- .../src/rules/ruff/rules/sort_dunder_all.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index be2c22cc09509..5553193e3e3f0 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -398,11 +398,10 @@ impl MultilineDunderAllValue { // "last item" # comment2 // # comment3 // ] # comment4 - // <-- Tokenizer emits a LogicalNewline here // ``` // // - The prelude in the above example is the source code region - // starting at the opening `[` and ending just before `# comment1`. + // starting just before the opening `[` and ending just after `# comment0`. // `comment0` here counts as part of the prelude because it is on // the same line as the opening paren, and because we haven't encountered // any elements of `__all__` yet, but `comment1` counts as part of the first item, @@ -411,11 +410,12 @@ impl MultilineDunderAllValue { // (an "item" being a region of source code that all moves as one unit // when `__all__` is sorted). // - The postlude in the above example is the source code region starting - // just after `# comment2` and ending just before the logical newline - // that follows the closing paren. `# comment2` is part of the last item, - // as it's an inline comment on the same line as an element, - // but `# comment3` becomes part of the postlude because there are no items - // below it. + // just after `# comment2` and ending just after the closing paren. + // `# comment2` is part of the last item, as it's an inline comment on the + // same line as an element, but `# comment3` becomes part of the postlude + // because there are no items below it. `# comment4` is not part of the + // postlude: it's outside of the source-code range considered by this rule, + // and should therefore be untouched. // let newline = stylist.line_ending().as_str(); let start_offset = self.start(); @@ -569,10 +569,10 @@ impl LineState { /// record the range of that comment. /// /// *If*, however, we've already seen a comma - /// or a stringin this line, that means that we're + /// or a string in this line, that means that we're /// in a line with items. In that case, we want to /// record the range of the comment, *plus* the whitespace - /// preceding the comment. This is so that we don't + /// (if any) preceding the comment. This is so that we don't /// unnecessarily apply opinionated formatting changes /// where they might not be welcome. fn visit_comment_token(&mut self, token_range: TextRange) { From 6ca35fe5393aa311a972ea69916103b8a8544334 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 16 Jan 2024 10:10:39 +0000 Subject: [PATCH 65/77] More docs --- .../src/rules/ruff/rules/sort_dunder_all.rs | 91 +++++++++++++++++-- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index be2c22cc09509..da1f7ab6e43af 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -100,7 +100,7 @@ pub(crate) fn sort_dunder_all_aug_assign(checker: &mut Checker, node: &ast::Stmt } } -/// Sort an `__all__` mutation from a call to `.extend()`. +/// Sort a tuple or list passed to `__all__.extend()`. pub(crate) fn sort_dunder_all_extend_call( checker: &mut Checker, ast::ExprCall { @@ -133,6 +133,11 @@ pub(crate) fn sort_dunder_all_ann_assign(checker: &mut Checker, node: &ast::Stmt } } +/// Sort a tuple or list that defines or mutates the global variable `__all__`. +/// +/// This routine checks whether the tuple or list is sorted, and emits a +/// violation if it is not sorted. If the tuple/list was not sorted, +/// it attempts to set a `Fix` on the violation. fn sort_dunder_all(checker: &mut Checker, target: &ast::Expr, node: &ast::Expr) { let ast::Expr::Name(ast::ExprName { id, .. }) = target else { return; @@ -183,12 +188,28 @@ fn sort_dunder_all(checker: &mut Checker, target: &ast::Expr, node: &ast::Expr) checker.diagnostics.push(diagnostic); } +/// An enumeration of the two valid ways of defining +/// `__all__`: as a list, or as a tuple. +/// +/// Whereas lists are always parenthesized +/// (they always start with `[` and end with `]`), +/// single-line tuples *can* be unparenthesized. +/// We keep the original AST node around for the +/// Tuple variant so that this can be queried later. +#[derive(Debug)] enum DunderAllKind<'a> { List, Tuple(&'a ast::ExprTuple), } impl DunderAllKind<'_> { + fn is_parenthesized(&self, source: &str) -> bool { + match self { + Self::List => true, + Self::Tuple(ast_node) => ast_node.is_parenthesized(source), + } + } + fn opening_token_for_multiline_definition(&self) -> Tok { match self { Self::List => Tok::Lsqb, @@ -204,6 +225,8 @@ impl DunderAllKind<'_> { } } +/// Given an array of strings, return `true` if they are already +/// ordered accoding to an isort-style sort. fn dunder_all_is_already_sorted(string_elements: &[&str]) -> bool { let mut element_iter = string_elements.iter(); let Some(this) = element_iter.next() else { @@ -220,6 +243,11 @@ fn dunder_all_is_already_sorted(string_elements: &[&str]) -> bool { true } +/// A struct to implement logic necessary to achieve +/// an "isort-style sort". +/// +/// See the docs for this module as a whole for the +/// definition we use here of an "isort-style sort". struct AllItemSortKey { category: InferredMemberType, value: String, @@ -293,6 +321,15 @@ impl InferredMemberType { } } +/// Attempt to return `Some(fix)`, where `fix` is a `Fix` +/// that can be set on the diagnostic to sort the user's +/// `__all__` definition +/// +/// Return `None` if it's a multiline `__all__` definition +/// and the token-based analysis in +/// `MultilineDunderAllValue::from_source_range()` encounters +/// something it doesn't expect, meaning the violation +/// is unfixable in this instance. fn get_fix( range: TextRange, elts: &[ast::Expr], @@ -304,6 +341,19 @@ fn get_fix( let is_multiline = locator.contains_line_break(range); let sorted_source_code = { + // The machinery in the `MultilineDunderAllValue` is actually + // sophisticated enough that it would work just as well for + // single-line `__all__` definitions, and we could reduce + // the number of lines of code in this file by doing that. + // Unfortunately, however, `MultilineDunderAllValue::from_source_range()` + // must process every token in an `__all__` definition as + // part of its analysis, and this is quite costly in terms + // of performance. For single-line `__all__` definitions, it's + // also unnecessary, as it's impossible to have comments in + // between the `__all__` elements if the `__all__` + // definition is all on a single line. Therfore, as an + // optimisation, we do the bare minimum of token-processing + // for single-line `__all__` definitions: if is_multiline { let value = MultilineDunderAllValue::from_source_range(range, kind, locator)?; assert_eq!(value.items.len(), elts.len()); @@ -628,6 +678,13 @@ impl LineWithItems { } } +/// An enumeration of the possible kinds of source-code lines +/// that can exist in a multiline `__all__` tuple or list: +/// +/// - A line that has no string elements, but does have a comment. +/// - A line that has one or more string elements, +/// and may also have a trailing comment. +/// - An entirely empty line. #[derive(Debug)] enum DunderAllLine { JustAComment(LineWithJustAComment), @@ -775,6 +832,12 @@ impl Ranged for DunderAllItem { } } +/// Return a string representing the "prelude" for a +/// multiline `__all__` definition. +/// +/// See inline comments in +/// `MultilineDunderAllValue::into_sorted_source_code()` +/// for a definition of the term "prelude" in this context. fn multiline_dunder_all_prelude( first_item_start_offset: TextSize, newline: &str, @@ -793,6 +856,14 @@ fn multiline_dunder_all_prelude( format!("{}{}", prelude.trim_end(), newline) } +/// Join the elements and comments of a multiline `__all__` +/// definition into a single string. +/// +/// The resulting string does not include the "prelude" or +/// "postlude" of the `__all__` definition. +/// (See inline comments in `MultilineDunderAllValue::into_sorted_source_code()` +/// for definitions of the terms "prelude" and "postlude" +/// in this context.) fn join_multiline_dunder_all_items( sorted_items: &[DunderAllItem], locator: &Locator, @@ -825,6 +896,12 @@ fn join_multiline_dunder_all_items( new_dunder_all } +/// Return a string representing the "postlude" for a +/// multiline `__all__` definition. +/// +/// See inline comments in +/// `MultilineDunderAllValue::into_sorted_source_code()` +/// for a definition of the term "postlude" in this context. fn multiline_dunder_all_postlude<'a>( last_item_end_offset: TextSize, newline: &str, @@ -857,6 +934,9 @@ fn multiline_dunder_all_postlude<'a>( Cow::Borrowed(postlude) } +/// Create a string representing a fixed-up single-line +/// `__all__` definition, that can be inserted into the +/// source code as a `range_replacement` autofix. fn sort_single_line_dunder_all( elts: &[ast::Expr], elements: &[&str], @@ -871,12 +951,9 @@ fn sort_single_line_dunder_all( .join(", "); match kind { DunderAllKind::List => format!("[{joined_items}]"), - DunderAllKind::Tuple(tuple_node) => { - if tuple_node.is_parenthesized(locator.contents()) { - format!("({joined_items})") - } else { - joined_items - } + DunderAllKind::Tuple(_) if kind.is_parenthesized(locator.contents()) => { + format!("({joined_items})") } + DunderAllKind::Tuple(_) => joined_items, } } From dd707aad16e0957fea5b10a07d296e36e9224032 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 16 Jan 2024 10:19:39 +0000 Subject: [PATCH 66/77] Address Micha's easy comments --- .../src/rules/ruff/rules/sort_dunder_all.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 7e81abd89a247..a44921d4dc843 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -161,7 +161,7 @@ fn sort_dunder_all(checker: &mut Checker, target: &ast::Expr, node: &ast::Expr) }; let mut possibly_fixable = true; - let mut string_items = vec![]; + let mut string_items = Vec::with_capacity(elts.len()); for elt in elts { // Don't flag `__all__` definitions that contain non-strings let Some(string_literal) = elt.as_string_literal_expr() else { @@ -180,7 +180,7 @@ fn sort_dunder_all(checker: &mut Checker, target: &ast::Expr, node: &ast::Expr) let mut diagnostic = Diagnostic::new(UnsortedDunderAll, range); if possibly_fixable { - if let Some(fix) = get_fix(range, elts, &string_items, &kind, checker) { + if let Some(fix) = create_fix(range, elts, &string_items, &kind, checker) { diagnostic.set_fix(fix); } } @@ -312,7 +312,7 @@ impl InferredMemberType { if value.len() > 1 && is_cased_uppercase(value) { Self::Constant // E.g. `Class` - } else if value.chars().next().is_some_and(char::is_uppercase) { + } else if value.starts_with(char::is_uppercase) { Self::Class // E.g. `some_variable` or `some_function` } else { @@ -330,7 +330,7 @@ impl InferredMemberType { /// `MultilineDunderAllValue::from_source_range()` encounters /// something it doesn't expect, meaning the violation /// is unfixable in this instance. -fn get_fix( +fn create_fix( range: TextRange, elts: &[ast::Expr], string_items: &[&str], @@ -871,11 +871,11 @@ fn join_multiline_dunder_all_items( newline: &str, needs_trailing_comma: bool, ) -> String { - let max_index = sorted_items.len() - 1; + let last_item_index = sorted_items.len() - 1; let mut new_dunder_all = String::new(); for (i, item) in sorted_items.iter().enumerate() { - let is_final_item = i == max_index; + let is_final_item = i == last_item_index; for comment_range in &item.preceding_comment_ranges { new_dunder_all.push_str(item_indent); new_dunder_all.push_str(locator.slice(comment_range)); From 2e7b5709bbda369a383f66b2fb1bc0211dc42ec4 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 16 Jan 2024 10:21:51 +0000 Subject: [PATCH 67/77] typos --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index a44921d4dc843..1277845f8f0e2 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -226,7 +226,7 @@ impl DunderAllKind<'_> { } /// Given an array of strings, return `true` if they are already -/// ordered accoding to an isort-style sort. +/// ordered according to an isort-style sort. fn dunder_all_is_already_sorted(string_elements: &[&str]) -> bool { let mut element_iter = string_elements.iter(); let Some(this) = element_iter.next() else { @@ -351,7 +351,7 @@ fn create_fix( // of performance. For single-line `__all__` definitions, it's // also unnecessary, as it's impossible to have comments in // between the `__all__` elements if the `__all__` - // definition is all on a single line. Therfore, as an + // definition is all on a single line. Therefore, as an // optimisation, we do the bare minimum of token-processing // for single-line `__all__` definitions: if is_multiline { From 2413c04034a4f67f0b25de1ddb612754e2e684c4 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 16 Jan 2024 10:28:58 +0000 Subject: [PATCH 68/77] Make `AllItemSortKey` generic over a lifetime --- .../src/rules/ruff/rules/sort_dunder_all.rs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 1277845f8f0e2..5cd8cd80600c2 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -248,44 +248,44 @@ fn dunder_all_is_already_sorted(string_elements: &[&str]) -> bool { /// /// See the docs for this module as a whole for the /// definition we use here of an "isort-style sort". -struct AllItemSortKey { +struct AllItemSortKey<'a> { category: InferredMemberType, - value: String, + value: &'a str, } -impl Ord for AllItemSortKey { +impl Ord for AllItemSortKey<'_> { fn cmp(&self, other: &Self) -> Ordering { self.category .cmp(&other.category) - .then_with(|| natord::compare(&self.value, &other.value)) + .then_with(|| natord::compare(self.value, other.value)) } } -impl PartialOrd for AllItemSortKey { +impl PartialOrd for AllItemSortKey<'_> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl PartialEq for AllItemSortKey { +impl PartialEq for AllItemSortKey<'_> { fn eq(&self, other: &Self) -> bool { self.cmp(other) == Ordering::Equal } } -impl Eq for AllItemSortKey {} +impl Eq for AllItemSortKey<'_> {} -impl From<&str> for AllItemSortKey { - fn from(value: &str) -> Self { +impl<'a> From<&'a str> for AllItemSortKey<'a> { + fn from(value: &'a str) -> Self { Self { category: InferredMemberType::of(value), - value: String::from(value), + value, } } } -impl From<&DunderAllItem> for AllItemSortKey { - fn from(item: &DunderAllItem) -> Self { +impl<'a> From<&'a DunderAllItem> for AllItemSortKey<'a> { + fn from(item: &'a DunderAllItem) -> Self { Self::from(item.value.as_str()) } } @@ -484,7 +484,7 @@ impl MultilineDunderAllValue { ); self.items - .sort_by_cached_key(|item| AllItemSortKey::from(item)); + .sort_by(|this, next| AllItemSortKey::from(this).cmp(&AllItemSortKey::from(next))); let joined_items = join_multiline_dunder_all_items( &self.items, locator, From ecb33d42b9c02955af7511566a1a7998423448c9 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 16 Jan 2024 10:56:13 +0000 Subject: [PATCH 69/77] Improve handling of prelude/postlude newlines --- .../src/rules/ruff/rules/sort_dunder_all.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 5cd8cd80600c2..e76526f9410e5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -838,12 +838,12 @@ impl Ranged for DunderAllItem { /// See inline comments in /// `MultilineDunderAllValue::into_sorted_source_code()` /// for a definition of the term "prelude" in this context. -fn multiline_dunder_all_prelude( +fn multiline_dunder_all_prelude<'a>( first_item_start_offset: TextSize, newline: &str, dunder_all_offset: TextSize, - locator: &Locator, -) -> String { + locator: &'a Locator, +) -> Cow<'a, str> { let prelude_end = { let first_item_line_offset = locator.line_start(first_item_start_offset); if first_item_line_offset == locator.line_start(dunder_all_offset) { @@ -853,7 +853,11 @@ fn multiline_dunder_all_prelude( } }; let prelude = locator.slice(TextRange::new(dunder_all_offset, prelude_end)); - format!("{}{}", prelude.trim_end(), newline) + if prelude.ends_with(['\r', '\n']) { + Cow::Borrowed(prelude) + } else { + Cow::Owned(format!("{}{}", prelude.trim_end(), newline)) + } } /// Join the elements and comments of a multiline `__all__` @@ -919,16 +923,17 @@ fn multiline_dunder_all_postlude<'a>( } }; let postlude = locator.slice(TextRange::new(postlude_start, dunder_all_range_end)); - if !postlude.starts_with(newline) { + let newline_chars = ['\r', '\n']; + if !postlude.starts_with(newline_chars) { return Cow::Borrowed(postlude); } - if TextSize::of(leading_indentation(postlude.trim_start_matches(newline))) + if TextSize::of(leading_indentation(postlude.trim_start_matches(newline_chars))) <= TextSize::of(item_indent) { return Cow::Borrowed(postlude); } let trimmed_postlude = postlude.trim_start(); - if trimmed_postlude.starts_with(']') || trimmed_postlude.starts_with(')') { + if trimmed_postlude.starts_with([']', ')']) { return Cow::Owned(format!("{newline}{leading_indent}{trimmed_postlude}")); } Cow::Borrowed(postlude) From 34b81e3e8b2e3acdcf363b0b5783cf8927df12e1 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 16 Jan 2024 11:06:40 +0000 Subject: [PATCH 70/77] fix formatting, add another comment --- .../src/rules/ruff/rules/sort_dunder_all.rs | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index e76526f9410e5..93dee4a0f7d8f 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -923,13 +923,40 @@ fn multiline_dunder_all_postlude<'a>( } }; let postlude = locator.slice(TextRange::new(postlude_start, dunder_all_range_end)); + + // The rest of this function uses heuristics to + // avoid very long indents for the closing paren + // that don't match the style for the rest of the + // new `__all__` definition. + // + // For example, we want to avoid something like this + // (not uncommon in code that hasn't been + // autoformatted)... + // + // ```python + // __all__ = ["xxxxxx", "yyyyyy", + // "aaaaaa", "bbbbbb", + // ] + // ``` + // + // ...getting autofixed to this: + // + // ```python + // __all__ = [ + // "a", + // "b", + // "x", + // "y", + // ] + // ``` + let newline_chars = ['\r', '\n']; if !postlude.starts_with(newline_chars) { return Cow::Borrowed(postlude); } - if TextSize::of(leading_indentation(postlude.trim_start_matches(newline_chars))) - <= TextSize::of(item_indent) - { + if TextSize::of(leading_indentation( + postlude.trim_start_matches(newline_chars), + )) <= TextSize::of(item_indent) { return Cow::Borrowed(postlude); } let trimmed_postlude = postlude.trim_start(); From ad6a507a76f142f4beb91c40461df228d0d28c1a Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 16 Jan 2024 11:08:03 +0000 Subject: [PATCH 71/77] . --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 93dee4a0f7d8f..352682908faa5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -956,7 +956,8 @@ fn multiline_dunder_all_postlude<'a>( } if TextSize::of(leading_indentation( postlude.trim_start_matches(newline_chars), - )) <= TextSize::of(item_indent) { + )) <= TextSize::of(item_indent) + { return Cow::Borrowed(postlude); } let trimmed_postlude = postlude.trim_start(); From 24616f3a55e18640c92b548177842ba65021bd9c Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 16 Jan 2024 11:31:17 +0000 Subject: [PATCH 72/77] Mark fix as always safe --- .../src/rules/ruff/rules/sort_dunder_all.rs | 43 +++++++++---------- ..._rules__ruff__tests__RUF022_RUF022.py.snap | 22 +++++----- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 352682908faa5..04e1449971371 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use std::cmp::Ordering; -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast as ast; use ruff_python_codegen::Stylist; @@ -60,11 +60,14 @@ use natord; /// ``` /// /// ## Fix safety -/// This rule's fix should be safe for single-line `__all__` definitions -/// and for multiline `__all__` definitions without comments. -/// For multiline `__all__` definitions that include comments, -/// the fix is marked as unsafe, as it can be hard to tell where the comments -/// should be moved to when sorting the contents of `__all__`. +/// This rule's fix is marked as always being safe, in that +/// it should never alter the semantics of any Python code. +/// However, note that for multiline `__all__` definitions +/// that include comments on their own line, it can be hard +/// to tell where the comments should be moved to when sorting +/// the contents of `__all__`. While this rule's fix will +/// never delete a comment, it might *sometimes* move a +/// comment to an unexpected location. #[violation] pub struct UnsortedDunderAll; @@ -347,13 +350,13 @@ fn create_fix( // the number of lines of code in this file by doing that. // Unfortunately, however, `MultilineDunderAllValue::from_source_range()` // must process every token in an `__all__` definition as - // part of its analysis, and this is quite costly in terms - // of performance. For single-line `__all__` definitions, it's - // also unnecessary, as it's impossible to have comments in - // between the `__all__` elements if the `__all__` - // definition is all on a single line. Therefore, as an - // optimisation, we do the bare minimum of token-processing - // for single-line `__all__` definitions: + // part of its analysis, and this is quite slow. For + // single-line `__all__` definitions, it's also unnecessary, + // as it's impossible to have comments in between the + // `__all__` elements if the `__all__` definition is all on + // a single line. Therefore, as an optimisation, we do the + // bare minimum of token-processing for single-line `__all__` + // definitions: if is_multiline { let value = MultilineDunderAllValue::from_source_range(range, kind, locator)?; assert_eq!(value.items.len(), elts.len()); @@ -363,16 +366,10 @@ fn create_fix( } }; - let applicability = { - if is_multiline && checker.indexer().comment_ranges().intersects(range) { - Applicability::Unsafe - } else { - Applicability::Safe - } - }; - - let edit = Edit::range_replacement(sorted_source_code, range); - Some(Fix::applicable_edit(edit, applicability)) + Some(Fix::safe_edit(Edit::range_replacement( + sorted_source_code, + range, + ))) } /// An instance of this struct encapsulates an analysis diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap index e0f9974c111b7..f1ff9af1d6b67 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF022_RUF022.py.snap @@ -266,7 +266,7 @@ RUF022.py:32:11: RUF022 [*] `__all__` is not sorted | = help: Apply an isort-style sorting to `__all__` -ℹ Unsafe fix +ℹ Safe fix 30 30 | #################################### 31 31 | 32 32 | __all__ = ( @@ -301,7 +301,7 @@ RUF022.py:40:11: RUF022 [*] `__all__` is not sorted | = help: Apply an isort-style sorting to `__all__` -ℹ Unsafe fix +ℹ Safe fix 38 38 | ) 39 39 | 40 40 | __all__ = [ @@ -416,7 +416,7 @@ RUF022.py:91:11: RUF022 [*] `__all__` is not sorted | = help: Apply an isort-style sorting to `__all__` -ℹ Unsafe fix +ℹ Safe fix 88 88 | ########################################## 89 89 | 90 90 | # comment0 @@ -457,7 +457,7 @@ RUF022.py:101:11: RUF022 [*] `__all__` is not sorted | = help: Apply an isort-style sorting to `__all__` -ℹ Unsafe fix +ℹ Safe fix 99 99 | # comment7 100 100 | 101 101 | __all__ = [ # comment0 @@ -583,7 +583,7 @@ RUF022.py:125:28: RUF022 [*] `__all__` is not sorted | = help: Apply an isort-style sorting to `__all__` -ℹ Unsafe fix +ℹ Safe fix 123 123 | "register_error", "lookup_error"] 124 124 | 125 125 | __all__: tuple[str, ...] = ( # a comment about the opening paren @@ -617,7 +617,7 @@ RUF022.py:138:11: RUF022 [*] `__all__` is not sorted | = help: Apply an isort-style sorting to `__all__` -ℹ Unsafe fix +ℹ Safe fix 136 136 | # Also, this doesn't end with a trailing comma, 137 137 | # so the autofix shouldn't introduce one: 138 138 | __all__ = ( @@ -650,7 +650,7 @@ RUF022.py:145:16: RUF022 [*] `__all__` is not sorted | = help: Apply an isort-style sorting to `__all__` -ℹ Unsafe fix +ℹ Safe fix 143 143 | ) 144 144 | 145 145 | __all__.extend(( # comment0 @@ -682,7 +682,7 @@ RUF022.py:155:5: RUF022 [*] `__all__` is not sorted | = help: Apply an isort-style sorting to `__all__` -ℹ Unsafe fix +ℹ Safe fix 153 153 | __all__.extend( # comment0 154 154 | # comment1 155 155 | ( # comment2 @@ -715,7 +715,7 @@ RUF022.py:164:16: RUF022 [*] `__all__` is not sorted | = help: Apply an isort-style sorting to `__all__` -ℹ Unsafe fix +ℹ Safe fix 162 162 | ) # comment2 163 163 | 164 164 | __all__.extend([ # comment0 @@ -747,7 +747,7 @@ RUF022.py:174:5: RUF022 [*] `__all__` is not sorted | = help: Apply an isort-style sorting to `__all__` -ℹ Unsafe fix +ℹ Safe fix 172 172 | __all__.extend( # comment0 173 173 | # comment1 174 174 | [ # comment2 @@ -777,7 +777,7 @@ RUF022.py:183:11: RUF022 [*] `__all__` is not sorted | = help: Apply an isort-style sorting to `__all__` -ℹ Unsafe fix +ℹ Safe fix 180 180 | ] # comment4 181 181 | ) # comment2 182 182 | From 497a94ba81d1bf8fd0dce210905ad2bee50ff921 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 16 Jan 2024 11:43:52 +0000 Subject: [PATCH 73/77] Final round of comment fixups --- .../src/rules/ruff/rules/sort_dunder_all.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 04e1449971371..4734b9710822f 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -373,8 +373,8 @@ fn create_fix( } /// An instance of this struct encapsulates an analysis -/// of a Python tuple/list that represents an `__all__` -/// definition or augmentation. +/// of a multiline Python tuple/list that represents an +/// `__all__` definition or augmentation. struct MultilineDunderAllValue { items: Vec, range: TextRange, @@ -382,9 +382,9 @@ struct MultilineDunderAllValue { } impl MultilineDunderAllValue { - /// Analyse the source range for a Python tuple/list that represents an `__all__` - /// definition or augmentation. Return `None` if the analysis fails - /// for whatever reason. + /// Analyse the source range for a multiline Python tuple/list that + /// represents an `__all__` definition or augmentation. Return `None` + /// if the analysis fails for whatever reason. fn from_source_range( range: TextRange, kind: &DunderAllKind, @@ -571,7 +571,7 @@ fn collect_dunder_all_lines( } /// This struct is for keeping track of state -/// regarding a single line in an `__all__` definition. +/// regarding a single line in a multiline `__all__` definition. /// It is purely internal to `collect_dunder_all_lines()`, /// and should not be used outside that function. /// @@ -791,6 +791,7 @@ struct DunderAllItem { // total_range incorporates the ranges of preceding comments // (which must be contiguous with the element), // but doesn't incorporate any trailing comments + // (which might be contiguous, but also might not be) total_range: TextRange, end_of_line_comments: Option, } @@ -946,7 +947,6 @@ fn multiline_dunder_all_postlude<'a>( // "y", // ] // ``` - let newline_chars = ['\r', '\n']; if !postlude.starts_with(newline_chars) { return Cow::Borrowed(postlude); From 16d9905000905d3296780860ed46ffacd519fd28 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 16 Jan 2024 11:54:57 +0000 Subject: [PATCH 74/77] One more Micha comment I missed --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 4734b9710822f..49707a20beaee 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -974,7 +974,7 @@ fn sort_single_line_dunder_all( locator: &Locator, ) -> String { let mut element_pairs = elts.iter().zip(elements).collect_vec(); - element_pairs.sort_by_cached_key(|(_, elem)| AllItemSortKey::from(**elem)); + element_pairs.sort_by_key(|(_, elem)| AllItemSortKey::from(**elem)); let joined_items = element_pairs .iter() .map(|(elt, _)| locator.slice(elt)) From d23cded272ee833a6f9c806af1588530e99b19ee Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 16 Jan 2024 13:00:03 +0000 Subject: [PATCH 75/77] Address one final remark by Micha --- .../resources/test/fixtures/ruff/RUF022.py | 32 +++++-- .../src/rules/ruff/rules/sort_dunder_all.rs | 84 ++++++++++++------- 2 files changed, 78 insertions(+), 38 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py index 45ad1f327c366..5a749d3ed4dd1 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF022.py @@ -282,11 +282,27 @@ class IntroducesNonModuleScope: "duplicate_element", # comment0 ) -__all__ =[[]] -__all__ [()] -__all__ = (()) -__all__ = ([]) -__all__ = ((),) -__all__ = ([],) -__all__ = ("foo", [], "bar") -__all__ = ["foo", (), "bar"] +__all__ =[ + [] +] +__all__ [ + () +] +__all__ = ( + () +) +__all__ = ( + [] +) +__all__ = ( + (), +) +__all__ = ( + [], +) +__all__ = ( + "foo", [], "bar" +) +__all__ = [ + "foo", (), "bar" +] diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 49707a20beaee..d456750e57974 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -13,6 +13,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; +use is_macro; use itertools::Itertools; use natord; @@ -163,27 +164,15 @@ fn sort_dunder_all(checker: &mut Checker, target: &ast::Expr, node: &ast::Expr) _ => return, }; - let mut possibly_fixable = true; - let mut string_items = Vec::with_capacity(elts.len()); - for elt in elts { - // Don't flag `__all__` definitions that contain non-strings - let Some(string_literal) = elt.as_string_literal_expr() else { - return; - }; - // If any strings are implicitly concatenated, don't bother trying to autofix - if string_literal.value.is_implicit_concatenated() { - possibly_fixable = false; - } - string_items.push(string_literal.value.to_str()); - } - if dunder_all_is_already_sorted(&string_items) { + let elts_analysis = DunderAllSortClassification::from_elements(elts); + if elts_analysis.is_invalid() || elts_analysis.is_sorted() { return; } let mut diagnostic = Diagnostic::new(UnsortedDunderAll, range); - if possibly_fixable { - if let Some(fix) = create_fix(range, elts, &string_items, &kind, checker) { + if let DunderAllSortClassification::UnsortedAndMaybeFixable { items } = elts_analysis { + if let Some(fix) = create_fix(range, elts, &items, &kind, checker) { diagnostic.set_fix(fix); } } @@ -228,22 +217,57 @@ impl DunderAllKind<'_> { } } -/// Given an array of strings, return `true` if they are already -/// ordered according to an isort-style sort. -fn dunder_all_is_already_sorted(string_elements: &[&str]) -> bool { - let mut element_iter = string_elements.iter(); - let Some(this) = element_iter.next() else { - return true; - }; - let mut this_key = AllItemSortKey::from(*this); - for next in element_iter { - let next_key = AllItemSortKey::from(*next); - if next_key < this_key { - return false; +/// Determine whether an `__all__` tuple/list is sorted, +/// unsorted, or invalid. If it's unsorted, determine whether +/// there's a possibility that we could generate a fix for it. +/// +/// ("Sorted" here means "ordered according to an isort-style sort". +/// See the module-level docs for a definition of "isort-style sort.") +#[derive(Debug, is_macro::Is)] +enum DunderAllSortClassification<'a> { + Sorted, + Invalid, + UnsortedButUnfixable, + UnsortedAndMaybeFixable { items: Vec<&'a str> }, +} + +impl<'a> DunderAllSortClassification<'a> { + fn from_elements(elements: &'a [ast::Expr]) -> Self { + let Some((first, rest @ [_, ..])) = elements.split_first() else { + return Self::Sorted; + }; + let Some(string_node) = first.as_string_literal_expr() else { + return Self::Invalid; + }; + let mut possibly_fixable = !string_node.value.is_implicit_concatenated(); + let mut this = string_node.value.to_str(); + + for expr in rest { + let Some(string_node) = expr.as_string_literal_expr() else { + return Self::Invalid; + }; + possibly_fixable |= string_node.value.is_implicit_concatenated(); + let next = string_node.value.to_str(); + if AllItemSortKey::from(next) < AllItemSortKey::from(this) { + if possibly_fixable { + let mut items = Vec::with_capacity(elements.len()); + for expr in elements { + let Some(string_node) = expr.as_string_literal_expr() else { + return Self::Invalid; + }; + if string_node.value.is_implicit_concatenated() { + return Self::UnsortedButUnfixable; + } + items.push(string_node.value.to_str()); + } + return Self::UnsortedAndMaybeFixable { items }; + } + return Self::UnsortedButUnfixable; + } + this = next; } - this_key = next_key; + Self::Sorted } - true } /// A struct to implement logic necessary to achieve From 0192488461d82aa2fab3798c291eda0248964401 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 16 Jan 2024 14:20:10 +0000 Subject: [PATCH 76/77] Rename enum variant; clean up code slightly --- .../src/rules/ruff/rules/sort_dunder_all.rs | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index d456750e57974..8f3be670159c7 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -165,7 +165,7 @@ fn sort_dunder_all(checker: &mut Checker, target: &ast::Expr, node: &ast::Expr) }; let elts_analysis = DunderAllSortClassification::from_elements(elts); - if elts_analysis.is_invalid() || elts_analysis.is_sorted() { + if elts_analysis.is_not_a_list_of_string_literals() || elts_analysis.is_sorted() { return; } @@ -217,18 +217,25 @@ impl DunderAllKind<'_> { } } -/// Determine whether an `__all__` tuple/list is sorted, -/// unsorted, or invalid. If it's unsorted, determine whether -/// there's a possibility that we could generate a fix for it. +/// An enumeration of the possible conclusions we could come to +/// regarding the ordering of the elements in an `__all__` definition: +/// +/// 1. `__all__` is a list of string literals that is already sorted +/// 2. `__all__` is an unsorted list of string literals, +/// but we wouldn't be able to autofix it +/// 3. `__all__` is an unsorted list of string literals, +/// and it's possible we could generate a fix for it +/// 4. `__all__` contains one or more items that are not string +/// literals. /// /// ("Sorted" here means "ordered according to an isort-style sort". /// See the module-level docs for a definition of "isort-style sort.") #[derive(Debug, is_macro::Is)] enum DunderAllSortClassification<'a> { Sorted, - Invalid, UnsortedButUnfixable, UnsortedAndMaybeFixable { items: Vec<&'a str> }, + NotAListOfStringLiterals, } impl<'a> DunderAllSortClassification<'a> { @@ -237,32 +244,27 @@ impl<'a> DunderAllSortClassification<'a> { return Self::Sorted; }; let Some(string_node) = first.as_string_literal_expr() else { - return Self::Invalid; + return Self::NotAListOfStringLiterals; }; - let mut possibly_fixable = !string_node.value.is_implicit_concatenated(); let mut this = string_node.value.to_str(); for expr in rest { let Some(string_node) = expr.as_string_literal_expr() else { - return Self::Invalid; + return Self::NotAListOfStringLiterals; }; - possibly_fixable |= string_node.value.is_implicit_concatenated(); let next = string_node.value.to_str(); if AllItemSortKey::from(next) < AllItemSortKey::from(this) { - if possibly_fixable { - let mut items = Vec::with_capacity(elements.len()); - for expr in elements { - let Some(string_node) = expr.as_string_literal_expr() else { - return Self::Invalid; - }; - if string_node.value.is_implicit_concatenated() { - return Self::UnsortedButUnfixable; - } - items.push(string_node.value.to_str()); + let mut items = Vec::with_capacity(elements.len()); + for expr in elements { + let Some(string_node) = expr.as_string_literal_expr() else { + return Self::NotAListOfStringLiterals; + }; + if string_node.value.is_implicit_concatenated() { + return Self::UnsortedButUnfixable; } - return Self::UnsortedAndMaybeFixable { items }; + items.push(string_node.value.to_str()); } - return Self::UnsortedButUnfixable; + return Self::UnsortedAndMaybeFixable { items }; } this = next; } From c89556c3fad3cb7fe760172ae9e8e3e802dcaf7a Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 16 Jan 2024 14:36:10 +0000 Subject: [PATCH 77/77] another comment --- crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index 8f3be670159c7..788b300dd56cc 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -999,6 +999,10 @@ fn sort_single_line_dunder_all( kind: &DunderAllKind, locator: &Locator, ) -> String { + // We grab the original source-code ranges using `locator.slice()` + // rather than using the expression generator, as this approach allows + // us to easily preserve stylistic choices in the original source code + // such as whether double or single quotes were used. let mut element_pairs = elts.iter().zip(elements).collect_vec(); element_pairs.sort_by_key(|(_, elem)| AllItemSortKey::from(**elem)); let joined_items = element_pairs