From cb6788ab5fd1ccc1cfe8a81df013cd61eb755242 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sat, 3 Jun 2023 15:17:33 +0200 Subject: [PATCH 01/41] Handle trailing body end-of-line comments (#4811) ### Summary This PR adds custom logic to handle end-of-line comments of the last statement in a body. For example: ```python while True: if something.changed: do.stuff() # trailing comment b ``` The `# trailing comment` is a trailing comment of the `do.stuff()` expression statement. We incorrectly attached the comment as a trailing comment of the enclosing `while` statement because the comment is between the end of the while statement (the `while` statement ends right after `do.stuff()`) and before the `b` statement. This PR fixes the placement to correctly attach these comments to the last statement in a body (recursively). ## Test Plan I reviewed the snapshots and they now look correct. This may appear odd because a lot comments have now disappeared. This is the expected result because we use `verbatim` formatting for the block statements (like `while`) and that means that it only formats the inner content of the block, but not any trailing comments. The comments were visible before, because they were associated with the block statement (e.g. `while`). --- .../ruff_python_formatter/src/comments/mod.rs | 14 ++++++ .../src/comments/placement.rs | 36 ++++++++++++++ ...s__while_trailing_end_of_line_comment.snap | 21 ++++++++ ...tter__tests__black_test__comments5_py.snap | 5 +- ...tter__tests__black_test__comments6_py.snap | 8 +-- ...atter__tests__black_test__fmtonoff_py.snap | 4 +- ...atter__tests__black_test__fmtskip6_py.snap | 49 +++++++++++++++++++ ...s__black_test__remove_await_parens_py.snap | 4 +- 8 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_trailing_end_of_line_comment.snap create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index fdc5ea2190..6792adfcd2 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -858,4 +858,18 @@ a = ( assert_debug_snapshot!(comments.debug(test_case.source_code)); } + + #[test] + fn while_trailing_end_of_line_comment() { + let source = r#"while True: + if something.changed: + do.stuff() # trailing comment +"#; + + let test_case = CommentsTestCase::from_code(source); + + let comments = test_case.to_comments(); + + assert_debug_snapshot!(comments.debug(test_case.source_code)); + } } diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index f689d7c127..23bc961c5f 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -18,6 +18,7 @@ pub(super) fn place_comment<'a>( .or_else(|comment| handle_match_comment(comment, locator)) .or_else(|comment| handle_in_between_bodies_comment(comment, locator)) .or_else(|comment| handle_trailing_body_comment(comment, locator)) + .or_else(handle_trailing_end_of_line_body_comment) .or_else(|comment| handle_positional_only_arguments_separator_comment(comment, locator)) .or_else(|comment| { handle_trailing_binary_expression_left_or_operator_comment(comment, locator) @@ -401,6 +402,41 @@ fn handle_trailing_body_comment<'a>( } } +/// Handles end of line comments of the last statement in an indented body: +/// +/// ```python +/// while True: +/// if something.changed: +/// do.stuff() # trailing comment +/// ``` +fn handle_trailing_end_of_line_body_comment(comment: DecoratedComment<'_>) -> CommentPlacement<'_> { + // Must be an end of line comment + if comment.text_position().is_own_line() { + return CommentPlacement::Default(comment); + } + + // Must be *after* a statement + let Some(preceding) = comment.preceding_node() else { + return CommentPlacement::Default(comment); + }; + + // Recursively get the last child of statements with a body. + let last_children = std::iter::successors(last_child_in_body(preceding), |parent| { + last_child_in_body(*parent) + }); + + if let Some(last_child) = last_children.last() { + CommentPlacement::trailing(last_child, comment) + } else { + // End of line comment of a statement that has no body. This is not what we're looking for. + // ```python + // a # trailing comment + // b + // ``` + CommentPlacement::Default(comment) + } +} + /// Attaches comments for the positional-only arguments separator `/` as trailing comments to the /// enclosing [`Arguments`] node. /// diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_trailing_end_of_line_comment.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_trailing_end_of_line_comment.snap new file mode 100644 index 0000000000..41e845c4b4 --- /dev/null +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_trailing_end_of_line_comment.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_python_formatter/src/comments/mod.rs +expression: comments.debug(test_case.source_code) +--- +{ + Node { + kind: StmtExpr, + range: 46..56, + source: `do.stuff()`, + }: { + "leading": [], + "dangling": [], + "trailing": [ + SourceComment { + text: "# trailing comment", + position: EndOfLine, + formatted: false, + }, + ], + }, +} diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap index df1348d88f..7087a8c711 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap @@ -89,12 +89,13 @@ if __name__ == "__main__": @@ -1,11 +1,6 @@ while True: if something.changed: - do.stuff() # trailing comment +- do.stuff() # trailing comment - # Comment belongs to the `if` block. - # This one belongs to the `while` block. - - # Should this one, too? I guess so. - ++ do.stuff() # This one is properly standalone now. for i in range(100): @@ -161,7 +162,7 @@ if __name__ == "__main__": ```py while True: if something.changed: - do.stuff() # trailing comment + do.stuff() # This one is properly standalone now. for i in range(100): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap index 24247fc646..6181d6489b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap @@ -168,12 +168,14 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite def f( a, # type: int b, # type: int -@@ -67,22 +57,16 @@ +@@ -66,23 +56,17 @@ + + element + another_element + another_element_with_long_name - ) # type: int +- ) # type: int - - ++ ) def f( x, # not a type comment y, # type: int @@ -272,7 +274,7 @@ def f( + element + another_element + another_element_with_long_name - ) # type: int + ) def f( x, # not a type comment y, # type: int diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap index 362982ef50..1766ef2a6b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap @@ -369,7 +369,7 @@ d={'a':1, + (1, 2, 3, 4), + (5, 6, 7, 8), + (9, 10, 11, 12) -+ } # yapf: disable ++ } cfg.rule( - "Default", - "address", @@ -544,7 +544,7 @@ def single_literal_yapf_disable(): (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) - } # yapf: disable + } cfg.rule( "Default", "address", xxxx_xxxx=["xxx-xxxxxx-xxxxxxxxxx"], diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap new file mode 100644 index 0000000000..46b8920531 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap @@ -0,0 +1,49 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip6.py +--- +## Input + +```py +class A: + def f(self): + for line in range(10): + if True: + pass # fmt: skip +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -2,4 +2,4 @@ + def f(self): + for line in range(10): + if True: +- pass # fmt: skip ++ pass +``` + +## Ruff Output + +```py +class A: + def f(self): + for line in range(10): + if True: + pass +``` + +## Black Output + +```py +class A: + def f(self): + for line in range(10): + if True: + pass # fmt: skip +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap index 62096f8534..603cab0375 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap @@ -143,7 +143,7 @@ async def main(): - + await ( + asyncio.sleep(1) -+ ) # Hello ++ ) # Long lines async def main(): - await asyncio.gather( @@ -251,7 +251,7 @@ async def main(): async def main(): await ( asyncio.sleep(1) - ) # Hello + ) # Long lines async def main(): await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1)) From d6daa615637aa47dcaca34f051eef8e6d2bdda6c Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sat, 3 Jun 2023 15:29:22 +0200 Subject: [PATCH 02/41] Handle trailing end-of-line comments in-between-bodies (#4812) ## Summary And more custom logic around comments in bodies... uff. Let's say we have the following code ```python if x == y: pass # trailing comment of pass else: # trailing comment of `else` print("I have no comments") ``` Right now, the formatter attaches the `# trailing comment of `else` as a trailing comment of `pass` because it doesn't "see" that there's an `else` keyword in between (because the else body is just a Vec and not a node). This PR adds custom logic that attaches the trailing comments after the `else` as dangling comments to the `if` statement. The if statement must then split the dangling comments by `comments.text_position()`: * All comments up to the first end-of-line comment are leading comments of the `else` keyword. * All end-of-line comments coming after are `trailing` comments for the `else` keyword. ## Test Plan I added new unit tests. --- .../ruff_python_formatter/src/comments/mod.rs | 15 ++ .../src/comments/placement.rs | 155 +++++++++++++----- ...ile_trailing_else_end_of_line_comment.snap | 21 +++ 3 files changed, 150 insertions(+), 41 deletions(-) create mode 100644 crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_trailing_else_end_of_line_comment.snap diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index 6792adfcd2..57206e3fc4 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -872,4 +872,19 @@ a = ( assert_debug_snapshot!(comments.debug(test_case.source_code)); } + + #[test] + fn while_trailing_else_end_of_line_comment() { + let source = r#"while True: + pass +else: # trailing comment + pass +"#; + + let test_case = CommentsTestCase::from_code(source); + + let comments = test_case.to_comments(); + + assert_debug_snapshot!(comments.debug(test_case.source_code)); + } } diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 23bc961c5f..d4b007e775 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -16,7 +16,8 @@ pub(super) fn place_comment<'a>( ) -> CommentPlacement<'a> { handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_comment(comment, locator) .or_else(|comment| handle_match_comment(comment, locator)) - .or_else(|comment| handle_in_between_bodies_comment(comment, locator)) + .or_else(|comment| handle_in_between_bodies_own_line_comment(comment, locator)) + .or_else(|comment| handle_in_between_bodies_end_of_line_comment(comment, locator)) .or_else(|comment| handle_trailing_body_comment(comment, locator)) .or_else(handle_trailing_end_of_line_body_comment) .or_else(|comment| handle_positional_only_arguments_separator_comment(comment, locator)) @@ -178,7 +179,7 @@ fn handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_commen CommentPlacement::Default(comment) } -/// Handles comments between the last statement and the first statement of two bodies. +/// Handles own line comments between the last statement and the first statement of two bodies. /// /// ```python /// if x == y: @@ -188,15 +189,11 @@ fn handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_commen /// else: /// print("I have no comments") /// ``` -fn handle_in_between_bodies_comment<'a>( +fn handle_in_between_bodies_own_line_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - use ruff_python_ast::prelude::*; - - // The rule only applies to own line comments. The default logic associates end of line comments - // correctly. - if comment.text_position().is_end_of_line() { + if !comment.text_position().is_own_line() { return CommentPlacement::Default(comment); } @@ -204,39 +201,7 @@ fn handle_in_between_bodies_comment<'a>( if let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) { // ...and the following statement must be the first statement in an alternate body of the parent... - let is_following_the_first_statement_in_a_parents_alternate_body = - match comment.enclosing_node() { - AnyNodeRef::StmtIf(StmtIf { orelse, .. }) - | AnyNodeRef::StmtFor(StmtFor { orelse, .. }) - | AnyNodeRef::StmtAsyncFor(StmtAsyncFor { orelse, .. }) - | AnyNodeRef::StmtWhile(StmtWhile { orelse, .. }) => { - are_same_optional(following, orelse.first()) - } - - AnyNodeRef::StmtTry(StmtTry { - handlers, - orelse, - finalbody, - .. - }) - | AnyNodeRef::StmtTryStar(StmtTryStar { - handlers, - orelse, - finalbody, - .. - }) => { - are_same_optional(following, handlers.first()) - // Comments between the handlers and the `else`, or comments between the `handlers` and the `finally` - // are already handled by `handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_comment` - || handlers.is_empty() && are_same_optional(following, orelse.first()) - || (handlers.is_empty() || !orelse.is_empty()) - && are_same_optional(following, finalbody.first()) - } - - _ => false, - }; - - if !is_following_the_first_statement_in_a_parents_alternate_body { + if !is_first_statement_in_enclosing_alternate_body(following, comment.enclosing_node()) { // ```python // if test: // a @@ -305,6 +270,75 @@ fn handle_in_between_bodies_comment<'a>( CommentPlacement::Default(comment) } +/// Handles end of line comments comments between the last statement and the first statement of two bodies. +/// +/// ```python +/// if x == y: +/// pass # trailing comment of pass +/// else: # trailing comment of `else` +/// print("I have no comments") +/// ``` +fn handle_in_between_bodies_end_of_line_comment<'a>( + comment: DecoratedComment<'a>, + locator: &Locator, +) -> CommentPlacement<'a> { + if !comment.text_position().is_end_of_line() { + return CommentPlacement::Default(comment); + } + + // The comment must be between two statements... + if let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) + { + // ...and the following statement must be the first statement in an alternate body of the parent... + if !is_first_statement_in_enclosing_alternate_body(following, comment.enclosing_node()) { + // ```python + // if test: + // a + // # comment + // b + // ``` + return CommentPlacement::Default(comment); + } + + if !locator.contains_line_break(TextRange::new(preceding.end(), comment.slice().start())) { + // Trailing comment of the preceding statement + // ```python + // while test: + // a # comment + // else: + // b + // ``` + CommentPlacement::trailing(preceding, comment) + } else if following.is_stmt_if() || following.is_except_handler() { + // The `elif` or except handlers have their own body to which we can attach the trailing comment + // ```python + // if test: + // a + // elif c: # comment + // b + // ``` + CommentPlacement::trailing(following, comment) + } else { + // There are no bodies for the "else" branch and other bodies that are represented as a `Vec`. + // This means, there's no good place to attach the comments to. + // Make this a dangling comments and manually format the comment in + // in the enclosing node's formatting logic. For `try`, it's the formatters responsibility + // to correctly identify the comments for the `finally` and `orelse` block by looking + // at the comment's range. + // + // ```python + // while x == y: + // pass + // else: # trailing + // print("nooop") + // ``` + CommentPlacement::dangling(comment.enclosing_node(), comment) + } + } else { + CommentPlacement::Default(comment) + } +} + /// Handles trailing comments at the end of a body block (or any other block that is indented). /// ```python /// def test(): @@ -703,3 +737,42 @@ fn last_child_in_body(node: AnyNodeRef) -> Option { body.last().map(AnyNodeRef::from) } + +/// Returns `true` if `following` is the first statement in an alternate `body` (e.g. the else of an if statement) of the `enclosing` node. +fn is_first_statement_in_enclosing_alternate_body( + following: AnyNodeRef, + enclosing: AnyNodeRef, +) -> bool { + use ruff_python_ast::prelude::*; + + match enclosing { + AnyNodeRef::StmtIf(StmtIf { orelse, .. }) + | AnyNodeRef::StmtFor(StmtFor { orelse, .. }) + | AnyNodeRef::StmtAsyncFor(StmtAsyncFor { orelse, .. }) + | AnyNodeRef::StmtWhile(StmtWhile { orelse, .. }) => { + are_same_optional(following, orelse.first()) + } + + AnyNodeRef::StmtTry(StmtTry { + handlers, + orelse, + finalbody, + .. + }) + | AnyNodeRef::StmtTryStar(StmtTryStar { + handlers, + orelse, + finalbody, + .. + }) => { + are_same_optional(following, handlers.first()) + // Comments between the handlers and the `else`, or comments between the `handlers` and the `finally` + // are already handled by `handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_comment` + || handlers.is_empty() && are_same_optional(following, orelse.first()) + || (handlers.is_empty() || !orelse.is_empty()) + && are_same_optional(following, finalbody.first()) + } + + _ => false, + } +} diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_trailing_else_end_of_line_comment.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_trailing_else_end_of_line_comment.snap new file mode 100644 index 0000000000..33cd36edc4 --- /dev/null +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_trailing_else_end_of_line_comment.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_python_formatter/src/comments/mod.rs +expression: comments.debug(test_case.source_code) +--- +{ + Node { + kind: StmtWhile, + range: 0..54, + source: `while True:⏎`, + }: { + "leading": [], + "dangling": [ + SourceComment { + text: "# trailing comment", + position: EndOfLine, + formatted: false, + }, + ], + "trailing": [], + }, +} From 2c41c54e0c5b80c749cb1c4a7baf0fcdfbf1e579 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sat, 3 Jun 2023 16:06:14 +0200 Subject: [PATCH 03/41] Format `ExprName` (#4803) --- .../src/expression/expr_name.rs | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/crates/ruff_python_formatter/src/expression/expr_name.rs b/crates/ruff_python_formatter/src/expression/expr_name.rs index b19333ff4c..18605603e5 100644 --- a/crates/ruff_python_formatter/src/expression/expr_name.rs +++ b/crates/ruff_python_formatter/src/expression/expr_name.rs @@ -1,5 +1,6 @@ -use crate::{verbatim_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::prelude::*; +use crate::FormatNodeRule; +use ruff_formatter::{write, FormatContext}; use rustpython_parser::ast::ExprName; #[derive(Default)] @@ -7,6 +8,44 @@ pub struct FormatExprName; impl FormatNodeRule for FormatExprName { fn fmt_fields(&self, item: &ExprName, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [verbatim_text(item.range)]) + let ExprName { id, range, ctx: _ } = item; + + debug_assert_eq!( + id.as_str(), + f.context() + .source_code() + .slice(*range) + .text(f.context().source_code()) + ); + + write!(f, [source_text_slice(*range, ContainsNewlines::No)]) + } +} + +#[cfg(test)] +mod tests { + use ruff_text_size::{TextRange, TextSize}; + use rustpython_parser::ast::{ModModule, Ranged}; + use rustpython_parser::Parse; + + #[test] + fn name_range_with_comments() { + let source = ModModule::parse("a # comment", "file.py").unwrap(); + + let expression_statement = source + .body + .first() + .expect("Expected non-empty body") + .as_expr_stmt() + .unwrap(); + let name = expression_statement + .value + .as_name_expr() + .expect("Expected name expression"); + + assert_eq!( + name.range(), + TextRange::at(TextSize::new(0), TextSize::new(1)) + ); } } From 935094c2ffed2a9f35c643aa339abb8f989ade2e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 3 Jun 2023 15:01:27 -0400 Subject: [PATCH 04/41] Move import-name matching into methods on `BindingKind` (#4818) --- .../fixtures/flake8_type_checking/strict.py | 22 ++--- .../runtime_import_in_type_checking_block.rs | 20 ++-- .../rules/typing_only_runtime_import.rs | 96 ++++--------------- ...__flake8_type_checking__tests__strict.snap | 30 +++--- ...ing-only-third-party-import_strict.py.snap | 6 +- .../src/rules/pyflakes/rules/unused_import.rs | 15 +-- crates/ruff_python_semantic/src/binding.rs | 29 ++++++ crates/ruff_python_semantic/src/model.rs | 12 +-- 8 files changed, 96 insertions(+), 134 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_type_checking/strict.py b/crates/ruff/resources/test/fixtures/flake8_type_checking/strict.py index 409ca96ff8..4882fefe9a 100644 --- a/crates/ruff/resources/test/fixtures/flake8_type_checking/strict.py +++ b/crates/ruff/resources/test/fixtures/flake8_type_checking/strict.py @@ -2,7 +2,7 @@ from __future__ import annotations def f(): - # Even in strict mode, this shouldn't rase an error, since `pkg` is used at runtime, + # Even in strict mode, this shouldn't raise an error, since `pkg` is used at runtime, # and implicitly imports `pkg.bar`. import pkg import pkg.bar @@ -12,7 +12,7 @@ def f(): def f(): - # Even in strict mode, this shouldn't rase an error, since `pkg.bar` is used at + # Even in strict mode, this shouldn't raise an error, since `pkg.bar` is used at # runtime, and implicitly imports `pkg`. import pkg import pkg.bar @@ -22,7 +22,7 @@ def f(): def f(): - # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. + # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. import pkg from pkg import A @@ -31,7 +31,7 @@ def f(): def f(): - # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. + # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. from pkg import A, B def test(value: A): @@ -39,7 +39,7 @@ def f(): def f(): - # Even in strict mode, this shouldn't rase an error, since `pkg.baz` is used at + # Even in strict mode, this shouldn't raise an error, since `pkg.baz` is used at # runtime, and implicitly imports `pkg.bar`. import pkg.bar import pkg.baz @@ -49,7 +49,7 @@ def f(): def f(): - # In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime. + # In un-strict mode, this _should_ raise an error, since `pkg.bar` isn't used at runtime import pkg from pkg.bar import A @@ -58,7 +58,7 @@ def f(): def f(): - # In un-strict mode, this shouldn't rase an error, since `pkg.bar` is used at runtime. + # In un-strict mode, this shouldn't raise an error, since `pkg.bar` is used at runtime. import pkg import pkg.bar as B @@ -67,7 +67,7 @@ def f(): def f(): - # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. + # In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime. import pkg.foo as F import pkg.foo.bar as B @@ -76,7 +76,7 @@ def f(): def f(): - # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. + # In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime. import pkg import pkg.foo.bar as B @@ -85,7 +85,7 @@ def f(): def f(): - # In un-strict mode, this _should_ rase an error, since `pkgfoo.bar` is used at runtime. + # In un-strict mode, this _should_ raise an error, since `pkg` isn't used at runtime. # Note that `pkg` is a prefix of `pkgfoo` which are both different modules. This is # testing the implementation. import pkg @@ -96,7 +96,7 @@ def f(): def f(): - # In un-strict mode, this shouldn't raise an error, since `pkg.bar` is used at runtime. + # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. import pkg.bar as B import pkg.foo as F diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index 61586fdf75..ec51578b43 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -1,8 +1,6 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::{ - Binding, BindingKind, FromImportation, Importation, SubmoduleImportation, -}; +use ruff_python_semantic::binding::Binding; use crate::autofix; use crate::checkers::ast::Checker; @@ -66,11 +64,8 @@ pub(crate) fn runtime_import_in_type_checking_block( binding: &Binding, diagnostics: &mut Vec, ) { - let full_name = match &binding.kind { - BindingKind::Importation(Importation { full_name }) => full_name, - BindingKind::FromImportation(FromImportation { full_name }) => full_name.as_str(), - BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }) => full_name, - _ => return, + let Some(qualified_name) = binding.qualified_name() else { + return; }; let Some(reference_id) = binding.references.first() else { @@ -89,7 +84,7 @@ pub(crate) fn runtime_import_in_type_checking_block( { let mut diagnostic = Diagnostic::new( RuntimeImportInTypeCheckingBlock { - full_name: full_name.to_string(), + full_name: qualified_name.to_string(), }, binding.range, ); @@ -102,7 +97,7 @@ pub(crate) fn runtime_import_in_type_checking_block( let stmt = checker.semantic_model().stmts[source]; let parent = checker.semantic_model().stmts.parent(stmt); let remove_import_edit = autofix::edits::remove_unused_imports( - std::iter::once(full_name), + std::iter::once(qualified_name), stmt, parent, checker.locator, @@ -113,7 +108,10 @@ pub(crate) fn runtime_import_in_type_checking_block( // Step 2) Add the import to the top-level. let reference = checker.semantic_model().references.resolve(*reference_id); let add_import_edit = checker.importer.runtime_import_edit( - &StmtImport { stmt, full_name }, + &StmtImport { + stmt, + full_name: qualified_name, + }, reference.range().start(), )?; diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 8abcb73943..2f74e14aee 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -1,8 +1,6 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::{ - Binding, BindingKind, FromImportation, Importation, SubmoduleImportation, -}; +use ruff_python_semantic::binding::Binding; use crate::autofix; use crate::checkers::ast::Checker; @@ -180,65 +178,13 @@ impl Violation for TypingOnlyStandardLibraryImport { /// Return `true` if `this` is implicitly loaded via importing `that`. fn is_implicit_import(this: &Binding, that: &Binding) -> bool { - match &this.kind { - BindingKind::Importation(Importation { - full_name: this_name, - }) - | BindingKind::SubmoduleImportation(SubmoduleImportation { - full_name: this_name, - }) => match &that.kind { - BindingKind::FromImportation(FromImportation { - full_name: that_name, - }) => { - // Ex) `pkg.A` vs. `pkg` - let this_name = this_name.split('.').next().unwrap_or(this_name); - that_name - .rfind('.') - .map_or(false, |i| that_name[..i] == *this_name) - } - BindingKind::Importation(Importation { - full_name: that_name, - }) - | BindingKind::SubmoduleImportation(SubmoduleImportation { - full_name: that_name, - }) => { - // Submodule importation with an alias (`import pkg.A as B`) - // are represented as `Importation`. - let this_name = this_name.split('.').next().unwrap_or(this_name); - let that_name = that_name.split('.').next().unwrap_or(that_name); - this_name == that_name - } - _ => false, - }, - BindingKind::FromImportation(FromImportation { - full_name: this_name, - }) => match &that.kind { - BindingKind::Importation(Importation { - full_name: that_name, - }) - | BindingKind::SubmoduleImportation(SubmoduleImportation { - full_name: that_name, - }) => { - // Ex) `pkg.A` vs. `pkg` - let that_name = that_name.split('.').next().unwrap_or(that_name); - this_name - .rfind('.') - .map_or(false, |i| &this_name[..i] == that_name) - } - BindingKind::FromImportation(FromImportation { - full_name: that_name, - }) => { - // Ex) `pkg.A` vs. `pkg.B` - this_name.rfind('.').map_or(false, |i| { - that_name - .rfind('.') - .map_or(false, |j| this_name[..i] == that_name[..j]) - }) - } - _ => false, - }, - _ => false, - } + let Some(this_module) = this.module_name() else { + return false; + }; + let Some(that_module) = that.module_name() else { + return false; + }; + this_module == that_module } /// Return `true` if `name` is exempt from typing-only enforcement. @@ -274,15 +220,12 @@ pub(crate) fn typing_only_runtime_import( return; } - let full_name = match &binding.kind { - BindingKind::Importation(Importation { full_name }) => full_name, - BindingKind::FromImportation(FromImportation { full_name }) => full_name.as_str(), - BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }) => full_name, - _ => return, + let Some(qualified_name) = binding.qualified_name() else { + return; }; if is_exempt( - full_name, + qualified_name, &checker .settings .flake8_type_checking @@ -312,7 +255,7 @@ pub(crate) fn typing_only_runtime_import( // Extract the module base and level from the full name. // Ex) `foo.bar.baz` -> `foo`, `0` // Ex) `.foo.bar.baz` -> `foo`, `1` - let level = full_name + let level = qualified_name .chars() .take_while(|c| *c == '.') .count() @@ -321,7 +264,7 @@ pub(crate) fn typing_only_runtime_import( // Categorize the import. let mut diagnostic = match categorize( - full_name, + qualified_name, Some(level), &checker.settings.src, checker.package(), @@ -331,7 +274,7 @@ pub(crate) fn typing_only_runtime_import( ImportSection::Known(ImportType::LocalFolder | ImportType::FirstParty) => { Diagnostic::new( TypingOnlyFirstPartyImport { - full_name: full_name.to_string(), + full_name: qualified_name.to_string(), }, binding.range, ) @@ -339,14 +282,14 @@ pub(crate) fn typing_only_runtime_import( ImportSection::Known(ImportType::ThirdParty) | ImportSection::UserDefined(_) => { Diagnostic::new( TypingOnlyThirdPartyImport { - full_name: full_name.to_string(), + full_name: qualified_name.to_string(), }, binding.range, ) } ImportSection::Known(ImportType::StandardLibrary) => Diagnostic::new( TypingOnlyStandardLibraryImport { - full_name: full_name.to_string(), + full_name: qualified_name.to_string(), }, binding.range, ), @@ -363,7 +306,7 @@ pub(crate) fn typing_only_runtime_import( let stmt = checker.semantic_model().stmts[source]; let parent = checker.semantic_model().stmts.parent(stmt); let remove_import_edit = autofix::edits::remove_unused_imports( - std::iter::once(full_name), + std::iter::once(qualified_name), stmt, parent, checker.locator, @@ -374,7 +317,10 @@ pub(crate) fn typing_only_runtime_import( // Step 2) Add the import to a `TYPE_CHECKING` block. let reference = checker.semantic_model().references.resolve(*reference_id); let add_import_edit = checker.importer.typing_import_edit( - &StmtImport { stmt, full_name }, + &StmtImport { + stmt, + full_name: qualified_name, + }, reference.range().start(), checker.semantic_model(), )?; diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap index bee4fa7c52..1e6a497223 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap @@ -3,7 +3,7 @@ source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- strict.py:27:21: TCH002 [*] Move third-party import `pkg.A` into a type-checking block | -27 | # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. +27 | # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. 28 | import pkg 29 | from pkg import A | ^ TCH002 @@ -23,7 +23,7 @@ strict.py:27:21: TCH002 [*] Move third-party import `pkg.A` into a type-checking 4 8 | def f(): -------------------------------------------------------------------------------- 24 28 | def f(): -25 29 | # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. +25 29 | # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. 26 30 | import pkg 27 |- from pkg import A 28 31 | @@ -33,7 +33,7 @@ strict.py:27:21: TCH002 [*] Move third-party import `pkg.A` into a type-checking strict.py:35:21: TCH002 [*] Move third-party import `pkg.A` into a type-checking block | 35 | def f(): -36 | # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. +36 | # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. 37 | from pkg import A, B | ^ TCH002 38 | @@ -53,7 +53,7 @@ strict.py:35:21: TCH002 [*] Move third-party import `pkg.A` into a type-checking -------------------------------------------------------------------------------- 32 36 | 33 37 | def f(): -34 38 | # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. +34 38 | # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. 35 |- from pkg import A, B 39 |+ from pkg import B 36 40 | @@ -62,7 +62,7 @@ strict.py:35:21: TCH002 [*] Move third-party import `pkg.A` into a type-checking strict.py:54:25: TCH002 [*] Move third-party import `pkg.bar.A` into a type-checking block | -54 | # In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime. +54 | # In un-strict mode, this _should_ raise an error, since `pkg.bar` isn't used at runtime 55 | import pkg 56 | from pkg.bar import A | ^ TCH002 @@ -82,7 +82,7 @@ strict.py:54:25: TCH002 [*] Move third-party import `pkg.bar.A` into a type-chec 4 8 | def f(): -------------------------------------------------------------------------------- 51 55 | def f(): -52 56 | # In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime. +52 56 | # In un-strict mode, this _should_ raise an error, since `pkg.bar` isn't used at runtime 53 57 | import pkg 54 |- from pkg.bar import A 55 58 | @@ -92,7 +92,7 @@ strict.py:54:25: TCH002 [*] Move third-party import `pkg.bar.A` into a type-chec strict.py:62:12: TCH002 [*] Move third-party import `pkg` into a type-checking block | 62 | def f(): -63 | # In un-strict mode, this shouldn't rase an error, since `pkg.bar` is used at runtime. +63 | # In un-strict mode, this shouldn't raise an error, since `pkg.bar` is used at runtime. 64 | import pkg | ^^^ TCH002 65 | import pkg.bar as B @@ -111,7 +111,7 @@ strict.py:62:12: TCH002 [*] Move third-party import `pkg` into a type-checking b -------------------------------------------------------------------------------- 59 63 | 60 64 | def f(): -61 65 | # In un-strict mode, this shouldn't rase an error, since `pkg.bar` is used at runtime. +61 65 | # In un-strict mode, this shouldn't raise an error, since `pkg.bar` is used at runtime. 62 |- import pkg 63 66 | import pkg.bar as B 64 67 | @@ -120,7 +120,7 @@ strict.py:62:12: TCH002 [*] Move third-party import `pkg` into a type-checking b strict.py:71:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block | 71 | def f(): -72 | # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. +72 | # In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime. 73 | import pkg.foo as F | ^^^^^^^^^^^^ TCH002 74 | import pkg.foo.bar as B @@ -139,7 +139,7 @@ strict.py:71:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checki -------------------------------------------------------------------------------- 68 72 | 69 73 | def f(): -70 74 | # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. +70 74 | # In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime. 71 |- import pkg.foo as F 72 75 | import pkg.foo.bar as B 73 76 | @@ -148,7 +148,7 @@ strict.py:71:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checki strict.py:80:12: TCH002 [*] Move third-party import `pkg` into a type-checking block | 80 | def f(): -81 | # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. +81 | # In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime. 82 | import pkg | ^^^ TCH002 83 | import pkg.foo.bar as B @@ -167,7 +167,7 @@ strict.py:80:12: TCH002 [*] Move third-party import `pkg` into a type-checking b -------------------------------------------------------------------------------- 77 81 | 78 82 | def f(): -79 83 | # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. +79 83 | # In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime. 80 |- import pkg 81 84 | import pkg.foo.bar as B 82 85 | @@ -193,7 +193,7 @@ strict.py:91:12: TCH002 [*] Move third-party import `pkg` into a type-checking b 3 7 | 4 8 | def f(): -------------------------------------------------------------------------------- -88 92 | # In un-strict mode, this _should_ rase an error, since `pkgfoo.bar` is used at runtime. +88 92 | # In un-strict mode, this _should_ raise an error, since `pkg` isn't used at runtime. 89 93 | # Note that `pkg` is a prefix of `pkgfoo` which are both different modules. This is 90 94 | # testing the implementation. 91 |- import pkg @@ -203,7 +203,7 @@ strict.py:91:12: TCH002 [*] Move third-party import `pkg` into a type-checking b strict.py:101:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block | -101 | # In un-strict mode, this shouldn't raise an error, since `pkg.bar` is used at runtime. +101 | # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. 102 | import pkg.bar as B 103 | import pkg.foo as F | ^^^^^^^^^^^^ TCH002 @@ -223,7 +223,7 @@ strict.py:101:12: TCH002 [*] Move third-party import `pkg.foo` into a type-check 4 8 | def f(): -------------------------------------------------------------------------------- 98 102 | def f(): -99 103 | # In un-strict mode, this shouldn't raise an error, since `pkg.bar` is used at runtime. +99 103 | # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. 100 104 | import pkg.bar as B 101 |- import pkg.foo as F 102 105 | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_strict.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_strict.py.snap index 9d6efbff99..00cc9830d0 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_strict.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_strict.py.snap @@ -3,7 +3,7 @@ source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- strict.py:54:25: TCH002 [*] Move third-party import `pkg.bar.A` into a type-checking block | -54 | # In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime. +54 | # In un-strict mode, this _should_ raise an error, since `pkg.bar` isn't used at runtime 55 | import pkg 56 | from pkg.bar import A | ^ TCH002 @@ -23,7 +23,7 @@ strict.py:54:25: TCH002 [*] Move third-party import `pkg.bar.A` into a type-chec 4 8 | def f(): -------------------------------------------------------------------------------- 51 55 | def f(): -52 56 | # In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime. +52 56 | # In un-strict mode, this _should_ raise an error, since `pkg.bar` isn't used at runtime 53 57 | import pkg 54 |- from pkg.bar import A 55 58 | @@ -50,7 +50,7 @@ strict.py:91:12: TCH002 [*] Move third-party import `pkg` into a type-checking b 3 7 | 4 8 | def f(): -------------------------------------------------------------------------------- -88 92 | # In un-strict mode, this _should_ rase an error, since `pkgfoo.bar` is used at runtime. +88 92 | # In un-strict mode, this _should_ raise an error, since `pkg` isn't used at runtime. 89 93 | # Note that `pkg` is a prefix of `pkgfoo` which are both different modules. This is 90 94 | # testing the implementation. 91 |- import pkg diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs index acb5e2c6d9..c204dfc0dd 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs @@ -5,9 +5,7 @@ use rustpython_parser::ast::Ranged; use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, IsolationLevel, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::{ - BindingKind, Exceptions, FromImportation, Importation, SubmoduleImportation, -}; +use ruff_python_semantic::binding::Exceptions; use ruff_python_semantic::node::NodeId; use ruff_python_semantic::scope::Scope; @@ -117,11 +115,8 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut continue; } - let full_name = match &binding.kind { - BindingKind::Importation(Importation { full_name }) => full_name, - BindingKind::FromImportation(FromImportation { full_name }) => full_name.as_str(), - BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }) => full_name, - _ => continue, + let Some(qualified_name) = binding.qualified_name() else { + continue; }; let stmt_id = binding.source.unwrap(); @@ -144,12 +139,12 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut ignored .entry((stmt_id, parent_id, exceptions)) .or_default() - .push((full_name, &binding.range)); + .push((qualified_name, &binding.range)); } else { unused .entry((stmt_id, parent_id, exceptions)) .or_default() - .push((full_name, &binding.range)); + .push((qualified_name, &binding.range)); } } diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index a384c54bc4..61a864e09c 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -101,6 +101,35 @@ impl<'a> Binding<'a> { ) } + /// Returns the fully-qualified symbol name, if this symbol was imported from another module. + pub fn qualified_name(&self) -> Option<&str> { + match &self.kind { + BindingKind::Importation(Importation { full_name }) => Some(full_name), + BindingKind::FromImportation(FromImportation { full_name }) => Some(full_name), + BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }) => { + Some(full_name) + } + _ => None, + } + } + + /// Returns the fully-qualified name of the module from which this symbol was imported, if this + /// symbol was imported from another module. + pub fn module_name(&self) -> Option<&str> { + match &self.kind { + BindingKind::Importation(Importation { full_name }) + | BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }) => { + Some(full_name.split('.').next().unwrap_or(full_name)) + } + BindingKind::FromImportation(FromImportation { full_name }) => Some( + full_name + .rsplit_once('.') + .map_or(full_name, |(module, _)| module), + ), + _ => None, + } + } + /// Returns the appropriate visual range for highlighting this binding. pub fn trimmed_range(&self, semantic_model: &SemanticModel, locator: &Locator) -> TextRange { match self.kind { diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index ccb7a9ea8e..67893b69e2 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -256,14 +256,8 @@ impl<'a> SemanticModel<'a> { // import pyarrow.csv // print(pa.csv.read_csv("test.csv")) // ``` - let full_name = match &self.bindings[binding_id].kind { - BindingKind::Importation(Importation { full_name }) => *full_name, - BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }) => *full_name, - BindingKind::FromImportation(FromImportation { full_name }) => full_name.as_str(), - _ => return None, - }; - - let has_alias = full_name + let qualified_name = self.bindings[binding_id].qualified_name()?; + let has_alias = qualified_name .split('.') .last() .map(|segment| segment != symbol) @@ -272,7 +266,7 @@ impl<'a> SemanticModel<'a> { return None; } - self.scopes[scope_id].get(full_name) + self.scopes[scope_id].get(qualified_name) } /// Resolves the [`Expr`] to a fully-qualified symbol-name, if `value` resolves to an imported From c14896b42c46172c69c0848f45587d18c8ff63d9 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 3 Jun 2023 15:26:55 -0400 Subject: [PATCH 05/41] Move `Binding` initialization into `SemanticModel` (#4819) --- crates/ruff/src/checkers/ast/mod.rs | 275 +++++++---------------- crates/ruff_python_semantic/src/model.rs | 31 +++ 2 files changed, 117 insertions(+), 189 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index d736aa69db..444a1a98dd 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -257,21 +257,15 @@ where Stmt::Global(ast::StmtGlobal { names, range: _ }) => { let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); if !self.semantic_model.scope_id.is_global() { - // Add the binding to the current scope. - let context = self.semantic_model.execution_context(); - let exceptions = self.semantic_model.exceptions(); - let scope = &mut self.semantic_model.scopes[self.semantic_model.scope_id]; for (name, range) in names.iter().zip(ranges.iter()) { // Add a binding to the current scope. - let binding_id = self.semantic_model.bindings.push(Binding { - kind: BindingKind::Global, - range: *range, - references: Vec::new(), - source: self.semantic_model.stmt_id, - context, - exceptions, - flags: BindingFlags::empty(), - }); + let binding = self.semantic_model.declared_binding( + *range, + BindingKind::Global, + BindingFlags::empty(), + ); + let binding_id = self.semantic_model.bindings.push(binding); + let scope = self.semantic_model.scope_mut(); scope.add(name, binding_id); } } @@ -286,20 +280,15 @@ where Stmt::Nonlocal(ast::StmtNonlocal { names, range: _ }) => { let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); if !self.semantic_model.scope_id.is_global() { - let context = self.semantic_model.execution_context(); - let exceptions = self.semantic_model.exceptions(); - let scope = &mut self.semantic_model.scopes[self.semantic_model.scope_id]; for (name, range) in names.iter().zip(ranges.iter()) { // Add a binding to the current scope. - let binding_id = self.semantic_model.bindings.push(Binding { - kind: BindingKind::Nonlocal, - range: *range, - references: Vec::new(), - source: self.semantic_model.stmt_id, - context, - exceptions, - flags: BindingFlags::empty(), - }); + let binding = self.semantic_model.declared_binding( + *range, + BindingKind::Nonlocal, + BindingFlags::empty(), + ); + let binding_id = self.semantic_model.bindings.push(binding); + let scope = self.semantic_model.scope_mut(); scope.add(name, binding_id); } @@ -841,18 +830,11 @@ where for alias in names { if &alias.name == "__future__" { let name = alias.asname.as_ref().unwrap_or(&alias.name); - self.add_binding( name, - Binding { - kind: BindingKind::FutureImportation, - range: alias.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + alias.range(), + BindingKind::FutureImportation, + BindingFlags::empty(), ); if self.enabled(Rule::LateFutureImport) { @@ -870,39 +852,25 @@ where let full_name = &alias.name; self.add_binding( name, - Binding { - kind: BindingKind::SubmoduleImportation(SubmoduleImportation { - full_name, - }), - range: alias.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + alias.range(), + BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }), + BindingFlags::empty(), ); } else { let name = alias.asname.as_ref().unwrap_or(&alias.name); let full_name = &alias.name; self.add_binding( name, - Binding { - kind: BindingKind::Importation(Importation { full_name }), - range: alias.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: if alias - .asname - .as_ref() - .map_or(false, |asname| asname == &alias.name) - { - BindingFlags::EXPLICIT_EXPORT - } else { - BindingFlags::empty() - }, + alias.range(), + BindingKind::Importation(Importation { full_name }), + if alias + .asname + .as_ref() + .map_or(false, |asname| asname == &alias.name) + { + BindingFlags::EXPLICIT_EXPORT + } else { + BindingFlags::empty() }, ); @@ -1127,15 +1095,9 @@ where self.add_binding( name, - Binding { - kind: BindingKind::FutureImportation, - range: alias.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + alias.range(), + BindingKind::FutureImportation, + BindingFlags::empty(), ); if self.enabled(Rule::FutureFeatureNotDefined) { @@ -1194,22 +1156,16 @@ where helpers::format_import_from_member(level, module, &alias.name); self.add_binding( name, - Binding { - kind: BindingKind::FromImportation(FromImportation { full_name }), - range: alias.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: if alias - .asname - .as_ref() - .map_or(false, |asname| asname == &alias.name) - { - BindingFlags::EXPLICIT_EXPORT - } else { - BindingFlags::empty() - }, + alias.range(), + BindingKind::FromImportation(FromImportation { full_name }), + if alias + .asname + .as_ref() + .map_or(false, |asname| asname == &alias.name) + { + BindingFlags::EXPLICIT_EXPORT + } else { + BindingFlags::empty() }, ); } @@ -1927,15 +1883,9 @@ where self.add_binding( name, - Binding { - kind: BindingKind::FunctionDefinition, - range: stmt.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + stmt.range(), + BindingKind::FunctionDefinition, + BindingFlags::empty(), ); let definition = docstrings::extraction::extract_definition( @@ -2163,15 +2113,9 @@ where self.semantic_model.pop_definition(); self.add_binding( name, - Binding { - kind: BindingKind::ClassDefinition, - range: stmt.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + stmt.range(), + BindingKind::ClassDefinition, + BindingFlags::empty(), ); } _ => {} @@ -4200,15 +4144,9 @@ where // upstream. self.add_binding( &arg.arg, - Binding { - kind: BindingKind::Argument, - range: arg.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + arg.range(), + BindingKind::Argument, + BindingFlags::empty(), ); if self.enabled(Rule::AmbiguousVariableName) { @@ -4248,15 +4186,9 @@ where { self.add_binding( name, - Binding { - kind: BindingKind::Assignment, - range: pattern.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + pattern.range(), + BindingKind::Assignment, + BindingFlags::empty(), ); } @@ -4380,8 +4312,16 @@ impl<'a> Checker<'a> { } /// Add a [`Binding`] to the current scope, bound to the given name. - fn add_binding(&mut self, name: &'a str, binding: Binding<'a>) -> BindingId { + fn add_binding( + &mut self, + name: &'a str, + range: TextRange, + kind: BindingKind<'a>, + flags: BindingFlags, + ) -> BindingId { let binding_id = self.semantic_model.bindings.next_id(); + let binding = self.semantic_model.declared_binding(range, kind, flags); + if let Some((stack_index, existing_binding_id)) = self .semantic_model .scopes @@ -4449,7 +4389,7 @@ impl<'a> Checker<'a> { ); if let Some(parent) = binding.source { let parent = self.semantic_model.stmts[parent]; - if matches!(parent, Stmt::ImportFrom(_)) + if parent.is_import_from_stmt() && parent.range().contains_range(binding.range) { diagnostic.set_parent(parent.start()); @@ -4521,7 +4461,6 @@ impl<'a> Checker<'a> { } fn bind_builtins(&mut self) { - let scope = &mut self.semantic_model.scopes[self.semantic_model.scope_id]; for builtin in BUILTINS .iter() .chain(MAGIC_GLOBALS.iter()) @@ -4529,15 +4468,9 @@ impl<'a> Checker<'a> { .chain(self.settings.builtins.iter().map(String::as_str)) { // Add the builtin to the scope. - let binding_id = self.semantic_model.bindings.push(Binding { - kind: BindingKind::Builtin, - range: TextRange::default(), - source: None, - references: Vec::new(), - context: ExecutionContext::Runtime, - exceptions: Exceptions::empty(), - flags: BindingFlags::empty(), - }); + let binding = self.semantic_model.builtin_binding(); + let binding_id = self.semantic_model.bindings.push(binding); + let scope = self.semantic_model.scope_mut(); scope.add(builtin, binding_id); } } @@ -4647,15 +4580,9 @@ impl<'a> Checker<'a> { ) { self.add_binding( id, - Binding { - kind: BindingKind::Annotation, - range: expr.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + expr.range(), + BindingKind::Annotation, + BindingFlags::empty(), ); return; } @@ -4663,15 +4590,9 @@ impl<'a> Checker<'a> { if matches!(parent, Stmt::For(_) | Stmt::AsyncFor(_)) { self.add_binding( id, - Binding { - kind: BindingKind::LoopVar, - range: expr.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + expr.range(), + BindingKind::LoopVar, + BindingFlags::empty(), ); return; } @@ -4679,15 +4600,9 @@ impl<'a> Checker<'a> { if helpers::is_unpacking_assignment(parent, expr) { self.add_binding( id, - Binding { - kind: BindingKind::Binding, - range: expr.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + expr.range(), + BindingKind::Binding, + BindingFlags::empty(), ); return; } @@ -4759,15 +4674,9 @@ impl<'a> Checker<'a> { self.add_binding( id, - Binding { - kind: BindingKind::Export(Export { names: all_names }), - range: expr.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + expr.range(), + BindingKind::Export(Export { names: all_names }), + BindingFlags::empty(), ); return; } @@ -4780,30 +4689,18 @@ impl<'a> Checker<'a> { { self.add_binding( id, - Binding { - kind: BindingKind::NamedExprAssignment, - range: expr.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + expr.range(), + BindingKind::NamedExprAssignment, + BindingFlags::empty(), ); return; } self.add_binding( id, - Binding { - kind: BindingKind::Assignment, - range: expr.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + expr.range(), + BindingKind::Assignment, + BindingFlags::empty(), ); } diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 67893b69e2..e98125c243 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -114,6 +114,37 @@ impl<'a> SemanticModel<'a> { false } + /// Create a new [`Binding`] for a builtin. + pub fn builtin_binding(&self) -> Binding<'static> { + Binding { + range: TextRange::default(), + kind: BindingKind::Builtin, + references: Vec::new(), + flags: BindingFlags::empty(), + source: None, + context: ExecutionContext::Runtime, + exceptions: Exceptions::empty(), + } + } + + /// Create a new `Binding` for the given `name` and `range`. + pub fn declared_binding( + &self, + range: TextRange, + kind: BindingKind<'a>, + flags: BindingFlags, + ) -> Binding<'a> { + Binding { + range, + kind, + flags, + references: Vec::new(), + source: self.stmt_id, + context: self.execution_context(), + exceptions: self.exceptions(), + } + } + /// Return the current `Binding` for a given `name`. pub fn find_binding(&self, member: &str) -> Option<&Binding> { self.scopes() From 42c071d30254eb9d38d35173ec3213ec202f36f5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 3 Jun 2023 15:39:11 -0400 Subject: [PATCH 06/41] Respect mixed variable assignment in RET504 (#4835) --- .../test/fixtures/flake8_return/RET504.py | 31 +++++++++++ .../src/rules/flake8_return/rules/function.rs | 54 ++++++++++--------- .../ruff/src/rules/flake8_return/visitor.rs | 46 +++++++++++++--- 3 files changed, 98 insertions(+), 33 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_return/RET504.py b/crates/ruff/resources/test/fixtures/flake8_return/RET504.py index 96bc755687..80b92b3193 100644 --- a/crates/ruff/resources/test/fixtures/flake8_return/RET504.py +++ b/crates/ruff/resources/test/fixtures/flake8_return/RET504.py @@ -272,3 +272,34 @@ def str_to_bool(val): if isinstance(val, bool): return some_obj return val + + +# Mixed assignments +def function_assignment(x): + def f(): ... + + return f + + +def class_assignment(x): + class Foo: ... + + return Foo + + +def mixed_function_assignment(x): + if x: + def f(): ... + else: + f = 42 + + return f + + +def mixed_class_assignment(x): + if x: + class Foo: ... + else: + Foo = 42 + + return Foo diff --git a/crates/ruff/src/rules/flake8_return/rules/function.rs b/crates/ruff/src/rules/flake8_return/rules/function.rs index cc84657be0..9bfb85ade3 100644 --- a/crates/ruff/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff/src/rules/flake8_return/rules/function.rs @@ -506,34 +506,36 @@ fn implicit_return(checker: &mut Checker, stmt: &Stmt) { } } -/// Return `true` if the `id` has multiple assignments within the function. -fn has_multiple_assigns(id: &str, stack: &Stack) -> bool { - if let Some(assigns) = stack.assignments.get(&id) { - if assigns.len() > 1 { - return true; - } - } - false +/// Return `true` if the `id` has multiple declarations within the function. +fn has_multiple_declarations(id: &str, stack: &Stack) -> bool { + stack + .declarations + .get(&id) + .map_or(false, |declarations| declarations.len() > 1) } /// Return `true` if the `id` has a (read) reference between the `return_location` and its -/// preceding assignment. -fn has_refs_before_next_assign(id: &str, return_range: TextRange, stack: &Stack) -> bool { - let mut assignment_before_return: Option = None; - let mut assignment_after_return: Option = None; - if let Some(assignments) = stack.assignments.get(&id) { +/// preceding declaration. +fn has_references_before_next_declaration( + id: &str, + return_range: TextRange, + stack: &Stack, +) -> bool { + let mut declaration_before_return: Option = None; + let mut declaration_after_return: Option = None; + if let Some(assignments) = stack.declarations.get(&id) { for location in assignments.iter().sorted() { if *location > return_range.start() { - assignment_after_return = Some(*location); + declaration_after_return = Some(*location); break; } - assignment_before_return = Some(*location); + declaration_before_return = Some(*location); } } - // If there is no assignment before the return, then the variable must be defined in + // If there is no declaration before the return, then the variable must be declared in // some other way (e.g., a function argument). No need to check for references. - let Some(assignment_before_return) = assignment_before_return else { + let Some(declaration_before_return) = declaration_before_return else { return true; }; @@ -543,9 +545,9 @@ fn has_refs_before_next_assign(id: &str, return_range: TextRange, stack: &Stack) continue; } - if assignment_before_return < *location { - if let Some(assignment_after_return) = assignment_after_return { - if *location <= assignment_after_return { + if declaration_before_return < *location { + if let Some(declaration_after_return) = declaration_after_return { + if *location <= declaration_after_return { return true; } } else { @@ -559,7 +561,7 @@ fn has_refs_before_next_assign(id: &str, return_range: TextRange, stack: &Stack) } /// Return `true` if the `id` has a read or write reference within a `try` or loop body. -fn has_refs_or_assigns_within_try_or_loop(id: &str, stack: &Stack) -> bool { +fn has_references_or_declarations_within_try_or_loop(id: &str, stack: &Stack) -> bool { if let Some(references) = stack.references.get(&id) { for location in references { for try_range in &stack.tries { @@ -574,7 +576,7 @@ fn has_refs_or_assigns_within_try_or_loop(id: &str, stack: &Stack) -> bool { } } } - if let Some(references) = stack.assignments.get(&id) { + if let Some(references) = stack.declarations.get(&id) { for location in references { for try_range in &stack.tries { if try_range.contains(*location) { @@ -594,7 +596,7 @@ fn has_refs_or_assigns_within_try_or_loop(id: &str, stack: &Stack) -> bool { /// RET504 fn unnecessary_assign(checker: &mut Checker, stack: &Stack, expr: &Expr) { if let Expr::Name(ast::ExprName { id, .. }) = expr { - if !stack.assignments.contains_key(id.as_str()) { + if !stack.assigned_names.contains(id.as_str()) { return; } @@ -605,9 +607,9 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack, expr: &Expr) { return; } - if has_multiple_assigns(id, stack) - || has_refs_before_next_assign(id, expr.range(), stack) - || has_refs_or_assigns_within_try_or_loop(id, stack) + if has_multiple_declarations(id, stack) + || has_references_before_next_declaration(id, expr.range(), stack) + || has_references_or_declarations_within_try_or_loop(id, stack) { return; } diff --git a/crates/ruff/src/rules/flake8_return/visitor.rs b/crates/ruff/src/rules/flake8_return/visitor.rs index c7c3657138..f0d419c7bc 100644 --- a/crates/ruff/src/rules/flake8_return/visitor.rs +++ b/crates/ruff/src/rules/flake8_return/visitor.rs @@ -11,9 +11,14 @@ pub(crate) struct Stack<'a> { pub(crate) yields: Vec<&'a Expr>, pub(crate) elses: Vec<&'a Stmt>, pub(crate) elifs: Vec<&'a Stmt>, + /// The names that are assigned to in the current scope (e.g., anything on the left-hand side of + /// an assignment). + pub(crate) assigned_names: FxHashSet<&'a str>, + /// The names that are declared in the current scope, and the ranges of those declarations + /// (e.g., assignments, but also function and class definitions). + pub(crate) declarations: FxHashMap<&'a str, Vec>, pub(crate) references: FxHashMap<&'a str, Vec>, pub(crate) non_locals: FxHashSet<&'a str>, - pub(crate) assignments: FxHashMap<&'a str, Vec>, pub(crate) loops: Vec, pub(crate) tries: Vec, } @@ -34,8 +39,9 @@ impl<'a> ReturnVisitor<'a> { return; } Expr::Name(ast::ExprName { id, .. }) => { + self.stack.assigned_names.insert(id.as_str()); self.stack - .assignments + .declarations .entry(id) .or_insert_with(Vec::new) .push(expr.start()); @@ -45,7 +51,7 @@ impl<'a> ReturnVisitor<'a> { // Attribute assignments are often side-effects (e.g., `self.property = value`), // so we conservatively treat them as references to every known // variable. - for name in self.stack.assignments.keys() { + for name in self.stack.declarations.keys() { self.stack .references .entry(name) @@ -68,18 +74,44 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> { .non_locals .extend(names.iter().map(Identifier::as_str)); } - Stmt::FunctionDef(ast::StmtFunctionDef { + Stmt::ClassDef(ast::StmtClassDef { decorator_list, + name, + .. + }) => { + // Mark a declaration. + self.stack + .declarations + .entry(name.as_str()) + .or_insert_with(Vec::new) + .push(stmt.start()); + + // Don't recurse into the body, but visit the decorators, etc. + for expr in decorator_list { + visitor::walk_expr(self, expr); + } + } + Stmt::FunctionDef(ast::StmtFunctionDef { + name, args, + decorator_list, returns, .. }) | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { - decorator_list, + name, args, + decorator_list, returns, .. }) => { + // Mark a declaration. + self.stack + .declarations + .entry(name.as_str()) + .or_insert_with(Vec::new) + .push(stmt.start()); + // Don't recurse into the body, but visit the decorators, etc. for expr in decorator_list { visitor::walk_expr(self, expr); @@ -138,7 +170,7 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> { if let Some(target) = targets.first() { // Skip unpacking assignments, like `x, y = my_object`. - if matches!(target, Expr::Tuple(_)) && !value.is_tuple_expr() { + if target.is_tuple_expr() && !value.is_tuple_expr() { return; } @@ -172,7 +204,7 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> { Expr::Call(_) => { // Arbitrary function calls can have side effects, so we conservatively treat // every function call as a reference to every known variable. - for name in self.stack.assignments.keys() { + for name in self.stack.declarations.keys() { self.stack .references .entry(name) From fcacd3cd95c304126c53ec443ab78d0c4dea4953 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 3 Jun 2023 15:53:57 -0400 Subject: [PATCH 07/41] Preserve quotes in F523 fixer (#4836) --- .../resources/test/fixtures/pyflakes/F523.py | 5 ++ crates/ruff/src/rules/pyflakes/fixes.rs | 37 ++++++++++-- ..._rules__pyflakes__tests__F523_F523.py.snap | 58 +++++++++++++++++++ 3 files changed, 94 insertions(+), 6 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F523.py b/crates/ruff/resources/test/fixtures/pyflakes/F523.py index 2055dd3a31..5a7897ee09 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F523.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F523.py @@ -17,3 +17,8 @@ "{0}{1}".format(1, *args) # No issues "{0}{1}".format(1, 2, *args) # No issues "{0}{1}".format(1, 2, 3, *args) # F523 + +# With nested quotes +"{''1:{0}}".format(1, 2, 3) # F523 +"{\"\"1:{0}}".format(1, 2, 3) # F523 +'{""1:{0}}'.format(1, 2, 3) # F523 diff --git a/crates/ruff/src/rules/pyflakes/fixes.rs b/crates/ruff/src/rules/pyflakes/fixes.rs index 6bc0e2aae3..297e5bb8fa 100644 --- a/crates/ruff/src/rules/pyflakes/fixes.rs +++ b/crates/ruff/src/rules/pyflakes/fixes.rs @@ -1,4 +1,5 @@ -use anyhow::{bail, Ok, Result}; +use anyhow::{anyhow, bail, Ok, Result}; +use itertools::Itertools; use libcst_native::{Codegen, CodegenState, DictElement, Expression}; use ruff_text_size::TextRange; use rustpython_format::{ @@ -9,7 +10,7 @@ use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_ast::str::raw_contents; +use ruff_python_ast::str::{leading_quote, raw_contents, trailing_quote}; use crate::cst::matchers::{ match_attribute, match_call_mut, match_dict, match_expression, match_simple_string, @@ -144,7 +145,7 @@ pub(crate) fn remove_unused_positional_arguments_from_format_call( }); let mut min_unused_index = 0; - for index in unused_arguments { + for index in unused_arguments.iter().sorted() { if *index == min_unused_index { min_unused_index += 1; } else { @@ -152,12 +153,36 @@ pub(crate) fn remove_unused_positional_arguments_from_format_call( } } - let mut new_format_string; + // If we removed an argument, we may need to rewrite the positional themselves. + // Ex) `"{1}{2}".format(a, b, c)` to `"{0}{1}".format(b, c)` + let new_format_string; if min_unused_index > 0 { + // Extract the format string verbatim. let func = match_attribute(&mut call.func)?; let simple_string = match_simple_string(&mut func.value)?; - new_format_string = update_field_types(format_string, min_unused_index); - new_format_string = format!(r#""{new_format_string}""#); + + // Extract existing quotes from the format string. + let leading_quote = leading_quote(simple_string.value).ok_or_else(|| { + anyhow!( + "Could not find leading quote for format string: {}", + simple_string.value + ) + })?; + let trailing_quote = trailing_quote(simple_string.value).ok_or_else(|| { + anyhow!( + "Could not find trailing quote for format string: {}", + simple_string.value + ) + })?; + + // Update the format string, preserving the quotes. + new_format_string = format!( + "{}{}{}", + leading_quote, + update_field_types(format_string, min_unused_index), + trailing_quote + ); + simple_string.value = new_format_string.as_str(); } diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap index 51163b5e06..ef99362d37 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap @@ -169,6 +169,8 @@ F523.py:19:1: F523 [*] `.format` call has unused arguments at position(s): 2 20 | "{0}{1}".format(1, 2, *args) # No issues 21 | "{0}{1}".format(1, 2, 3, *args) # F523 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 +22 | +23 | # With nested quotes | = help: Remove extra positional arguments at position(s): 2 @@ -178,5 +180,61 @@ F523.py:19:1: F523 [*] `.format` call has unused arguments at position(s): 2 18 18 | "{0}{1}".format(1, 2, *args) # No issues 19 |-"{0}{1}".format(1, 2, 3, *args) # F523 19 |+"{0}{1}".format(1, 2, *args) # F523 +20 20 | +21 21 | # With nested quotes +22 22 | "{''1:{0}}".format(1, 2, 3) # F523 + +F523.py:22:1: F523 [*] `.format` call has unused arguments at position(s): 1, 2 + | +22 | # With nested quotes +23 | "{''1:{0}}".format(1, 2, 3) # F523 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 +24 | "{\"\"1:{0}}".format(1, 2, 3) # F523 +25 | '{""1:{0}}'.format(1, 2, 3) # F523 + | + = help: Remove extra positional arguments at position(s): 1, 2 + +ℹ Suggested fix +19 19 | "{0}{1}".format(1, 2, 3, *args) # F523 +20 20 | +21 21 | # With nested quotes +22 |-"{''1:{0}}".format(1, 2, 3) # F523 + 22 |+"{''1:{0}}".format(1, ) # F523 +23 23 | "{\"\"1:{0}}".format(1, 2, 3) # F523 +24 24 | '{""1:{0}}'.format(1, 2, 3) # F523 + +F523.py:23:1: F523 [*] `.format` call has unused arguments at position(s): 1, 2 + | +23 | # With nested quotes +24 | "{''1:{0}}".format(1, 2, 3) # F523 +25 | "{\"\"1:{0}}".format(1, 2, 3) # F523 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 +26 | '{""1:{0}}'.format(1, 2, 3) # F523 + | + = help: Remove extra positional arguments at position(s): 1, 2 + +ℹ Suggested fix +20 20 | +21 21 | # With nested quotes +22 22 | "{''1:{0}}".format(1, 2, 3) # F523 +23 |-"{\"\"1:{0}}".format(1, 2, 3) # F523 + 23 |+"{\"\"1:{0}}".format(1, ) # F523 +24 24 | '{""1:{0}}'.format(1, 2, 3) # F523 + +F523.py:24:1: F523 [*] `.format` call has unused arguments at position(s): 1, 2 + | +24 | "{''1:{0}}".format(1, 2, 3) # F523 +25 | "{\"\"1:{0}}".format(1, 2, 3) # F523 +26 | '{""1:{0}}'.format(1, 2, 3) # F523 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 + | + = help: Remove extra positional arguments at position(s): 1, 2 + +ℹ Suggested fix +21 21 | # With nested quotes +22 22 | "{''1:{0}}".format(1, 2, 3) # F523 +23 23 | "{\"\"1:{0}}".format(1, 2, 3) # F523 +24 |-'{""1:{0}}'.format(1, 2, 3) # F523 + 24 |+'{""1:{0}}'.format(1, ) # F523 From d8a6109b698f4a53475926e88efc3907fd0386ad Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 3 Jun 2023 16:11:48 -0400 Subject: [PATCH 08/41] Fix min-index offset rewrites in F523 (#4837) --- .../resources/test/fixtures/pyflakes/F523.py | 10 +- crates/ruff/src/rules/pyflakes/fixes.rs | 43 ++++---- ..._rules__pyflakes__tests__F523_F523.py.snap | 99 +++++++++++++------ 3 files changed, 102 insertions(+), 50 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F523.py b/crates/ruff/resources/test/fixtures/pyflakes/F523.py index 5a7897ee09..33283dbd48 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F523.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F523.py @@ -19,6 +19,10 @@ "{0}{1}".format(1, 2, 3, *args) # F523 # With nested quotes -"{''1:{0}}".format(1, 2, 3) # F523 -"{\"\"1:{0}}".format(1, 2, 3) # F523 -'{""1:{0}}'.format(1, 2, 3) # F523 +"''1{0}".format(1, 2, 3) # F523 +"\"\"{1}{0}".format(1, 2, 3) # F523 +'""{1}{0}'.format(1, 2, 3) # F523 + +# With modified indexes +"{1}{2}".format(1, 2, 3) # F523, # F524 +"{1}{3}".format(1, 2, 3, 4) # F523, # F524 diff --git a/crates/ruff/src/rules/pyflakes/fixes.rs b/crates/ruff/src/rules/pyflakes/fixes.rs index 297e5bb8fa..7a69513298 100644 --- a/crates/ruff/src/rules/pyflakes/fixes.rs +++ b/crates/ruff/src/rules/pyflakes/fixes.rs @@ -1,5 +1,4 @@ use anyhow::{anyhow, bail, Ok, Result}; -use itertools::Itertools; use libcst_native::{Codegen, CodegenState, DictElement, Expression}; use ruff_text_size::TextRange; use rustpython_format::{ @@ -88,7 +87,7 @@ fn unparse_format_part(format_part: FormatPart) -> String { } } -fn update_field_types(format_string: &FormatString, min_unused: usize) -> String { +fn update_field_types(format_string: &FormatString, index_map: &[usize]) -> String { format_string .format_parts .iter() @@ -99,10 +98,11 @@ fn update_field_types(format_string: &FormatString, min_unused: usize) -> String conversion_spec, format_spec, } => { - let new_field_name = FieldName::parse(field_name).unwrap(); // This should never fail because we parsed it before + // SAFETY: We've already parsed this string before. + let new_field_name = FieldName::parse(field_name).unwrap(); let mut new_field_name_string = match new_field_name.field_type { FieldType::Auto => String::new(), - FieldType::Index(i) => (i - min_unused).to_string(), + FieldType::Index(i) => index_map[i].to_string(), FieldType::Keyword(keyword) => keyword, }; for field_name_part in &new_field_name.parts { @@ -113,8 +113,10 @@ fn update_field_types(format_string: &FormatString, min_unused: usize) -> String }; new_field_name_string.push_str(&field_name_part_string); } - let new_format_spec = FormatString::from_str(format_spec).unwrap(); // This should never fail because we parsed it before - let new_format_spec_string = update_field_types(&new_format_spec, min_unused); + + // SAFETY: We've already parsed this string before. + let new_format_spec = FormatString::from_str(format_spec).unwrap(); + let new_format_spec_string = update_field_types(&new_format_spec, index_map); FormatPart::Field { field_name: new_field_name_string, conversion_spec: *conversion_spec, @@ -138,25 +140,30 @@ pub(crate) fn remove_unused_positional_arguments_from_format_call( let mut tree = match_expression(module_text)?; let call = match_call_mut(&mut tree)?; + // Remove any unused arguments, and generate a map from previous index to new index. let mut index = 0; + let mut offset = 0; + let mut index_map = Vec::with_capacity(call.args.len()); call.args.retain(|_| { + index_map.push(index - offset); + let is_unused = unused_arguments.contains(&index); index += 1; - !unused_arguments.contains(&(index - 1)) - }); - - let mut min_unused_index = 0; - for index in unused_arguments.iter().sorted() { - if *index == min_unused_index { - min_unused_index += 1; - } else { - break; + if is_unused { + offset += 1; } - } + !is_unused + }); // If we removed an argument, we may need to rewrite the positional themselves. // Ex) `"{1}{2}".format(a, b, c)` to `"{0}{1}".format(b, c)` + let rewrite_arguments = index_map + .iter() + .enumerate() + .filter(|&(prev_index, _)| !unused_arguments.contains(&prev_index)) + .any(|(prev_index, &new_index)| prev_index != new_index); + let new_format_string; - if min_unused_index > 0 { + if rewrite_arguments { // Extract the format string verbatim. let func = match_attribute(&mut call.func)?; let simple_string = match_simple_string(&mut func.value)?; @@ -179,7 +186,7 @@ pub(crate) fn remove_unused_positional_arguments_from_format_call( new_format_string = format!( "{}{}{}", leading_quote, - update_field_types(format_string, min_unused_index), + update_field_types(format_string, &index_map), trailing_quote ); diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap index ef99362d37..1ec240a042 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap @@ -182,15 +182,15 @@ F523.py:19:1: F523 [*] `.format` call has unused arguments at position(s): 2 19 |+"{0}{1}".format(1, 2, *args) # F523 20 20 | 21 21 | # With nested quotes -22 22 | "{''1:{0}}".format(1, 2, 3) # F523 +22 22 | "''1{0}".format(1, 2, 3) # F523 F523.py:22:1: F523 [*] `.format` call has unused arguments at position(s): 1, 2 | 22 | # With nested quotes -23 | "{''1:{0}}".format(1, 2, 3) # F523 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 -24 | "{\"\"1:{0}}".format(1, 2, 3) # F523 -25 | '{""1:{0}}'.format(1, 2, 3) # F523 +23 | "''1{0}".format(1, 2, 3) # F523 + | ^^^^^^^^^^^^^^^^^^^^^^^^ F523 +24 | "\"\"{1}{0}".format(1, 2, 3) # F523 +25 | '""{1}{0}'.format(1, 2, 3) # F523 | = help: Remove extra positional arguments at position(s): 1, 2 @@ -198,43 +198,84 @@ F523.py:22:1: F523 [*] `.format` call has unused arguments at position(s): 1, 2 19 19 | "{0}{1}".format(1, 2, 3, *args) # F523 20 20 | 21 21 | # With nested quotes -22 |-"{''1:{0}}".format(1, 2, 3) # F523 - 22 |+"{''1:{0}}".format(1, ) # F523 -23 23 | "{\"\"1:{0}}".format(1, 2, 3) # F523 -24 24 | '{""1:{0}}'.format(1, 2, 3) # F523 +22 |-"''1{0}".format(1, 2, 3) # F523 + 22 |+"''1{0}".format(1, ) # F523 +23 23 | "\"\"{1}{0}".format(1, 2, 3) # F523 +24 24 | '""{1}{0}'.format(1, 2, 3) # F523 +25 25 | -F523.py:23:1: F523 [*] `.format` call has unused arguments at position(s): 1, 2 +F523.py:23:1: F523 [*] `.format` call has unused arguments at position(s): 2 | 23 | # With nested quotes -24 | "{''1:{0}}".format(1, 2, 3) # F523 -25 | "{\"\"1:{0}}".format(1, 2, 3) # F523 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 -26 | '{""1:{0}}'.format(1, 2, 3) # F523 +24 | "''1{0}".format(1, 2, 3) # F523 +25 | "\"\"{1}{0}".format(1, 2, 3) # F523 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 +26 | '""{1}{0}'.format(1, 2, 3) # F523 | - = help: Remove extra positional arguments at position(s): 1, 2 + = help: Remove extra positional arguments at position(s): 2 ℹ Suggested fix 20 20 | 21 21 | # With nested quotes -22 22 | "{''1:{0}}".format(1, 2, 3) # F523 -23 |-"{\"\"1:{0}}".format(1, 2, 3) # F523 - 23 |+"{\"\"1:{0}}".format(1, ) # F523 -24 24 | '{""1:{0}}'.format(1, 2, 3) # F523 +22 22 | "''1{0}".format(1, 2, 3) # F523 +23 |-"\"\"{1}{0}".format(1, 2, 3) # F523 + 23 |+"\"\"{1}{0}".format(1, 2, ) # F523 +24 24 | '""{1}{0}'.format(1, 2, 3) # F523 +25 25 | +26 26 | # With modified indexes -F523.py:24:1: F523 [*] `.format` call has unused arguments at position(s): 1, 2 +F523.py:24:1: F523 [*] `.format` call has unused arguments at position(s): 2 | -24 | "{''1:{0}}".format(1, 2, 3) # F523 -25 | "{\"\"1:{0}}".format(1, 2, 3) # F523 -26 | '{""1:{0}}'.format(1, 2, 3) # F523 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 +24 | "''1{0}".format(1, 2, 3) # F523 +25 | "\"\"{1}{0}".format(1, 2, 3) # F523 +26 | '""{1}{0}'.format(1, 2, 3) # F523 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 +27 | +28 | # With modified indexes | - = help: Remove extra positional arguments at position(s): 1, 2 + = help: Remove extra positional arguments at position(s): 2 ℹ Suggested fix 21 21 | # With nested quotes -22 22 | "{''1:{0}}".format(1, 2, 3) # F523 -23 23 | "{\"\"1:{0}}".format(1, 2, 3) # F523 -24 |-'{""1:{0}}'.format(1, 2, 3) # F523 - 24 |+'{""1:{0}}'.format(1, ) # F523 +22 22 | "''1{0}".format(1, 2, 3) # F523 +23 23 | "\"\"{1}{0}".format(1, 2, 3) # F523 +24 |-'""{1}{0}'.format(1, 2, 3) # F523 + 24 |+'""{1}{0}'.format(1, 2, ) # F523 +25 25 | +26 26 | # With modified indexes +27 27 | "{1}{2}".format(1, 2, 3) # F523, # F524 + +F523.py:27:1: F523 [*] `.format` call has unused arguments at position(s): 0 + | +27 | # With modified indexes +28 | "{1}{2}".format(1, 2, 3) # F523, # F524 + | ^^^^^^^^^^^^^^^^^^^^^^^^ F523 +29 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 + | + = help: Remove extra positional arguments at position(s): 0 + +ℹ Suggested fix +24 24 | '""{1}{0}'.format(1, 2, 3) # F523 +25 25 | +26 26 | # With modified indexes +27 |-"{1}{2}".format(1, 2, 3) # F523, # F524 + 27 |+"{0}{1}".format(2, 3) # F523, # F524 +28 28 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 + +F523.py:28:1: F523 [*] `.format` call has unused arguments at position(s): 0, 2 + | +28 | # With modified indexes +29 | "{1}{2}".format(1, 2, 3) # F523, # F524 +30 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 + | + = help: Remove extra positional arguments at position(s): 0, 2 + +ℹ Suggested fix +25 25 | +26 26 | # With modified indexes +27 27 | "{1}{2}".format(1, 2, 3) # F523, # F524 +28 |-"{1}{3}".format(1, 2, 3, 4) # F523, # F524 + 28 |+"{0}{1}".format(2, 4) # F523, # F524 From 5ae4667fd5f2821ec65dd8650d63e0665d822b87 Mon Sep 17 00:00:00 2001 From: Zanie Adkins Date: Sat, 3 Jun 2023 15:33:44 -0500 Subject: [PATCH 09/41] Upgrade `criterion` to `0.5.1` (#4838) --- Cargo.lock | 257 +++++++++++++------------------ crates/ruff_benchmark/Cargo.toml | 2 +- 2 files changed, 110 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3df63e6e02..4d3f97ac18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -188,9 +194,9 @@ checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" [[package]] name = "bstr" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" +checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5" dependencies = [ "memchr", "once_cell", @@ -200,9 +206,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.2" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "cachedir" @@ -242,13 +248,13 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "time", "wasm-bindgen", @@ -284,21 +290,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.25" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" -dependencies = [ - "bitflags 1.3.2", - "clap_lex 0.2.4", - "indexmap", - "textwrap", -] - -[[package]] -name = "clap" -version = "4.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" +checksum = "b4ed2379f8603fa2b7509891660e802b88c70a79a6427a70abb5968054de2c28" dependencies = [ "clap_builder", "clap_derive", @@ -307,24 +301,24 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.2.7" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd" +checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" dependencies = [ "anstream", "anstyle", "bitflags 1.3.2", - "clap_lex 0.4.1", + "clap_lex", "strsim", ] [[package]] name = "clap_complete" -version = "4.2.3" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1594fe2312ec4abf402076e407628f5c313e54c32ade058521df4ee34ecac8a8" +checksum = "7f6b5c519bab3ea61843a7923d074b04245624bb84a64a8c150f5deb014e388b" dependencies = [ - "clap 4.2.7", + "clap", ] [[package]] @@ -333,7 +327,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "183495371ea78d4c9ff638bfc6497d46fed2396e4f9c50aebc1278a4a9919a3d" dependencies = [ - "clap 4.2.7", + "clap", "clap_complete", "clap_complete_fig", "clap_complete_nushell", @@ -341,50 +335,41 @@ dependencies = [ [[package]] name = "clap_complete_fig" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3af28956330989baa428ed4d3471b853715d445c62de21b67292e22cf8a41fa" +checksum = "99fee1d30a51305a6c2ed3fc5709be3c8af626c9c958e04dd9ae94e27bcbce9f" dependencies = [ - "clap 4.2.7", + "clap", "clap_complete", ] [[package]] name = "clap_complete_nushell" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fa41f5e6aa83bd151b70fd0ceaee703d68cd669522795dc812df9edad1252c" +checksum = "5d02bc8b1a18ee47c4d2eec3fb5ac034dc68ebea6125b1509e9ccdffcddce66e" dependencies = [ - "clap 4.2.7", + "clap", "clap_complete", ] [[package]] name = "clap_derive" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" +checksum = "59e9ef9a08ee1c0e1f2e162121665ac45ac3783b0f897db7244ae75ad9a8f65b" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "clap_lex" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "clearscreen" @@ -424,14 +409,14 @@ checksum = "5458d9d1a587efaf5091602c59d299696a3877a439c8f6d461a2d3cce11df87a" [[package]] name = "console" -version = "0.15.5" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" dependencies = [ "encode_unicode", "lazy_static", "libc", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -477,19 +462,19 @@ dependencies = [ [[package]] name = "criterion" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", - "atty", "cast", "ciborium", - "clap 3.2.25", + "clap", "criterion-plot", + "is-terminal", "itertools", - "lazy_static", "num-traits", + "once_cell", "oorandom", "plotters", "rayon", @@ -709,7 +694,7 @@ name = "flake8-to-ruff" version = "0.0.270" dependencies = [ "anyhow", - "clap 4.2.7", + "clap", "colored", "configparser", "once_cell", @@ -950,9 +935,9 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.1", "libc", @@ -1001,9 +986,9 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" -version = "0.3.62" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c16e1bfd491478ab155fd8b4896b86f9ede344949b641e61501e07c2b8b4d5" +checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" dependencies = [ "wasm-bindgen", ] @@ -1118,18 +1103,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "log" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" [[package]] name = "matches" @@ -1178,14 +1160,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -1230,9 +1212,9 @@ dependencies = [ [[package]] name = "notify" -version = "5.1.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ea850aa68a06e48fdb069c0ec44d0d64c8dbffa49bf3b6f7f0a901fdea1ba9" +checksum = "729f63e1ca555a43fe3efa4f3efdf4801c479da85b432242a7b726f353c88486" dependencies = [ "bitflags 1.3.2", "crossbeam-channel", @@ -1243,7 +1225,7 @@ dependencies = [ "libc", "mio", "walkdir", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -1288,9 +1270,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" [[package]] name = "oorandom" @@ -1563,9 +1545,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" dependencies = [ "unicode-ident", ] @@ -1608,9 +1590,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -1683,9 +1665,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.1" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" dependencies = [ "aho-corasick 1.0.1", "memchr", @@ -1700,9 +1682,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "result-like" @@ -1749,7 +1731,7 @@ dependencies = [ "anyhow", "bitflags 2.3.1", "chrono", - "clap 4.2.7", + "clap", "colored", "dirs 5.0.1", "fern", @@ -1846,7 +1828,7 @@ dependencies = [ "bitflags 2.3.1", "cachedir", "chrono", - "clap 4.2.7", + "clap", "clap_complete_command", "clearscreen", "colored", @@ -1885,7 +1867,7 @@ name = "ruff_dev" version = "0.0.0" dependencies = [ "anyhow", - "clap 4.2.7", + "clap", "itertools", "libcst", "once_cell", @@ -1944,7 +1926,7 @@ dependencies = [ "proc-macro2", "quote", "ruff_textwrap", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -1984,7 +1966,7 @@ name = "ruff_python_formatter" version = "0.0.0" dependencies = [ "anyhow", - "clap 4.2.7", + "clap", "countme", "insta", "is-macro", @@ -2041,7 +2023,7 @@ dependencies = [ "glob", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -2290,7 +2272,7 @@ checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -2318,9 +2300,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" dependencies = [ "serde", ] @@ -2405,9 +2387,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", @@ -2490,12 +2472,6 @@ dependencies = [ "test-case-core", ] -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - [[package]] name = "thiserror" version = "1.0.40" @@ -2513,7 +2489,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -2593,9 +2569,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" dependencies = [ "serde", "serde_spanned", @@ -2605,18 +2581,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.8" +version = "0.19.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" dependencies = [ "indexmap", "serde", @@ -2646,7 +2622,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -2736,9 +2712,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-normalization" @@ -2805,9 +2781,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" +checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" [[package]] name = "version_check" @@ -2848,9 +2824,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.85" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4" +checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2858,24 +2834,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.85" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e522ed4105a9d626d885b35d62501b30d9666283a5c8be12c14a8bdafe7822" +checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.35" +version = "0.4.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "083abe15c5d88556b77bdf7aef403625be9e327ad37c62c4e4129af740168163" +checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" dependencies = [ "cfg-if", "js-sys", @@ -2885,9 +2861,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.85" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "358a79a0cb89d21db8120cbfb91392335913e4890665b1a7981d9e956903b434" +checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2895,28 +2871,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.85" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" +checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.85" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb" +checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" [[package]] name = "wasm-bindgen-test" -version = "0.3.35" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b27e15b4a3030b9944370ba1d8cec6f21f66a1ad4fd14725c5685600460713ec" +checksum = "c9e636f3a428ff62b3742ebc3c70e254dfe12b8c2b469d688ea59cdd4abcf502" dependencies = [ "console_error_panic_hook", "js-sys", @@ -2928,9 +2904,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.35" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dbaa9b9a574eac00c4f3a9c4941ac051f07632ecd0484a8588abd95af6b99d2" +checksum = "f18c1fad2f7c4958e7bcce014fa212f59a65d5e3721d0f77e6c0b27ede936ba3" dependencies = [ "proc-macro2", "quote", @@ -2938,9 +2914,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.62" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b5f940c7edfdc6d12126d98c9ef4d1b3d470011c47c76a6581df47ad9ba721" +checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" dependencies = [ "js-sys", "wasm-bindgen", @@ -3025,21 +3001,6 @@ dependencies = [ "windows-targets 0.48.0", ] -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-sys" version = "0.45.0" diff --git a/crates/ruff_benchmark/Cargo.toml b/crates/ruff_benchmark/Cargo.toml index 3e738d518c..21d6f99e05 100644 --- a/crates/ruff_benchmark/Cargo.toml +++ b/crates/ruff_benchmark/Cargo.toml @@ -30,7 +30,7 @@ ureq = "2.6.2" [dev-dependencies] ruff.path = "../ruff" ruff_python_ast.path = "../ruff_python_ast" -criterion = { version = "0.4.0"} +criterion = { version = "0.5.1"} rustpython-parser.workspace = true [target.'cfg(target_os = "windows")'.dev-dependencies] From 67b43ab72aef2db5635a12e5a43e32324ae326f9 Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Sat, 3 Jun 2023 15:35:06 -0500 Subject: [PATCH 10/41] Make FLY002 autofix into a constant string instead of an f-string if all `join()` arguments are strings (#4834) --- .../flynt/rules/static_join_to_fstring.rs | 42 +++++++++++++++++-- ...rules__flynt__tests__FLY002_FLY002.py.snap | 6 +-- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs b/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs index cd05bace7c..2faab59911 100644 --- a/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs +++ b/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs @@ -1,5 +1,6 @@ +use itertools::Itertools; use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Expr, Ranged}; +use rustpython_parser::ast::{self, Constant, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -27,15 +28,48 @@ impl AlwaysAutofixableViolation for StaticJoinToFString { } fn is_static_length(elts: &[Expr]) -> bool { - elts.iter().all(|e| !matches!(e, Expr::Starred(_))) + elts.iter().all(|e| !e.is_starred_expr()) } fn build_fstring(joiner: &str, joinees: &[Expr]) -> Option { + // If all elements are string constants, join them into a single string. + if joinees.iter().all(|expr| { + matches!( + expr, + Expr::Constant(ast::ExprConstant { + value: Constant::Str(_), + .. + }) + ) + }) { + let node = ast::ExprConstant { + value: Constant::Str( + joinees + .iter() + .filter_map(|expr| { + if let Expr::Constant(ast::ExprConstant { + value: Constant::Str(string), + .. + }) = expr + { + Some(string.as_str()) + } else { + None + } + }) + .join(joiner), + ), + range: TextRange::default(), + kind: None, + }; + return Some(node.into()); + } + let mut fstring_elems = Vec::with_capacity(joinees.len() * 2); let mut first = true; for expr in joinees { - if matches!(expr, Expr::JoinedStr(_)) { + if expr.is_joined_str_expr() { // Oops, already an f-string. We don't know how to handle those // gracefully right now. return None; @@ -58,7 +92,7 @@ pub(crate) fn static_join_to_fstring(checker: &mut Checker, expr: &Expr, joiner: args, keywords, .. - })= expr else { + }) = expr else { return; }; diff --git a/crates/ruff/src/rules/flynt/snapshots/ruff__rules__flynt__tests__FLY002_FLY002.py.snap b/crates/ruff/src/rules/flynt/snapshots/ruff__rules__flynt__tests__FLY002_FLY002.py.snap index d268b251ba..e987f86206 100644 --- a/crates/ruff/src/rules/flynt/snapshots/ruff__rules__flynt__tests__FLY002_FLY002.py.snap +++ b/crates/ruff/src/rules/flynt/snapshots/ruff__rules__flynt__tests__FLY002_FLY002.py.snap @@ -42,7 +42,7 @@ FLY002.py:6:7: FLY002 [*] Consider `f"Finally, {a} World"` instead of string joi 8 8 | ok4 = "y".join([1, 2, 3]) # Technically OK, though would've been an error originally 9 9 | ok5 = "a".join([random(), random()]) # OK (simple calls) -FLY002.py:7:7: FLY002 [*] Consider `f"1x2x3"` instead of string join +FLY002.py:7:7: FLY002 [*] Consider `"1x2x3"` instead of string join | 7 | ok1 = " ".join([a, " World"]) # OK 8 | ok2 = "".join(["Finally, ", a, " World"]) # OK @@ -51,14 +51,14 @@ FLY002.py:7:7: FLY002 [*] Consider `f"1x2x3"` instead of string join 10 | ok4 = "y".join([1, 2, 3]) # Technically OK, though would've been an error originally 11 | ok5 = "a".join([random(), random()]) # OK (simple calls) | - = help: Replace with `f"1x2x3"` + = help: Replace with `"1x2x3"` ℹ Suggested fix 4 4 | a = "Hello" 5 5 | ok1 = " ".join([a, " World"]) # OK 6 6 | ok2 = "".join(["Finally, ", a, " World"]) # OK 7 |-ok3 = "x".join(("1", "2", "3")) # OK - 7 |+ok3 = f"1x2x3" # OK + 7 |+ok3 = "1x2x3" # OK 8 8 | ok4 = "y".join([1, 2, 3]) # Technically OK, though would've been an error originally 9 9 | ok5 = "a".join([random(), random()]) # OK (simple calls) 10 10 | ok6 = "a".join([secrets.token_urlsafe(), secrets.token_hex()]) # OK (attr calls) From e7a2e0f43771eada465f4df19b1f3f682e51bce7 Mon Sep 17 00:00:00 2001 From: Zanie Adkins Date: Sat, 3 Jun 2023 16:31:06 -0500 Subject: [PATCH 11/41] Remove unused mutable variables (#4839) --- crates/ruff/src/rules/flake8_comprehensions/fixes.rs | 4 ++-- .../ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs | 2 +- .../ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs | 4 ++-- crates/ruff/src/rules/pyupgrade/rules/format_literals.rs | 2 +- .../src/rules/ruff/rules/explicit_f_string_type_conversion.rs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs index b9054be763..dcd062187c 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs @@ -291,7 +291,7 @@ pub(crate) fn fix_unnecessary_literal_set( // Expr(Call(List|Tuple)))) -> Expr(Set))) let module_text = locator.slice(expr.range()); let mut tree = match_expression(module_text)?; - let mut call = match_call_mut(&mut tree)?; + let call = match_call_mut(&mut tree)?; let arg = match_arg(call)?; let (elements, whitespace_after, whitespace_before) = match &arg.value { @@ -765,7 +765,7 @@ pub(crate) fn fix_unnecessary_double_cast_or_process( ) -> Result { let module_text = locator.slice(expr.range()); let mut tree = match_expression(module_text)?; - let mut outer_call = match_call_mut(&mut tree)?; + let outer_call = match_call_mut(&mut tree)?; outer_call.args = match outer_call.args.split_first() { Some((first, rest)) => { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs index c52b6d12dc..e21b586f90 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs @@ -59,7 +59,7 @@ fn reverse_comparison(expr: &Expr, locator: &Locator, stylist: &Stylist) -> Resu let contents = locator.slice(range); let mut expression = match_expression(contents)?; - let mut comparison = match_comparison(&mut expression)?; + let comparison = match_comparison(&mut expression)?; let left = (*comparison.left).clone(); diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs index d99c272926..f13f8b9825 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs @@ -127,7 +127,7 @@ fn format_import( ) -> Result { let module_text = locator.slice(stmt.range()); let mut tree = match_statement(module_text)?; - let mut import = match_import(&mut tree)?; + let import = match_import(&mut tree)?; let Import { names, .. } = import.clone(); let (clean_aliases, mock_aliases) = clean_import_aliases(names); @@ -161,7 +161,7 @@ fn format_import_from( ) -> Result { let module_text = locator.slice(stmt.range()); let mut tree = match_statement(module_text).unwrap(); - let mut import = match_import_from(&mut tree)?; + let import = match_import_from(&mut tree)?; if let ImportFrom { names: ImportNames::Star(..), diff --git a/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs b/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs index 5352f3c008..83e7f4d839 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs @@ -89,7 +89,7 @@ fn generate_call( ) -> Result { let module_text = locator.slice(expr.range()); let mut expression = match_expression(module_text)?; - let mut call = match_call_mut(&mut expression)?; + let call = match_call_mut(&mut expression)?; // Fix the call arguments. if !is_sequential(correct_order) { diff --git a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs index a08071d29d..a9b6d989b0 100644 --- a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs +++ b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs @@ -59,7 +59,7 @@ fn fix_explicit_f_string_type_conversion( let formatted_string = match_formatted_string(&mut expression)?; // Replace the formatted call expression at `index` with a conversion flag. - let mut formatted_string_expression = + let formatted_string_expression = match_formatted_string_expression(&mut formatted_string.parts[index])?; let call = match_call_mut(&mut formatted_string_expression.expression)?; let name = match_name(&call.func)?; From 14e06f9f8b83dc701fc610b8bbc31972a52a7d7f Mon Sep 17 00:00:00 2001 From: Zanie Adkins Date: Sat, 3 Jun 2023 17:13:51 -0500 Subject: [PATCH 12/41] Rename `ruff_formatter::builders::BestFitting` to `FormatBestFitting` (#4841) --- crates/ruff_formatter/src/builders.rs | 6 +++--- crates/ruff_formatter/src/lib.rs | 2 +- crates/ruff_formatter/src/macros.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/ruff_formatter/src/builders.rs b/crates/ruff_formatter/src/builders.rs index 02000019c4..24879eb868 100644 --- a/crates/ruff_formatter/src/builders.rs +++ b/crates/ruff_formatter/src/builders.rs @@ -2131,11 +2131,11 @@ impl<'a, 'buf, Context> FillBuilder<'a, 'buf, Context> { /// The first variant is the most flat, and the last is the most expanded variant. /// See [`best_fitting!`] macro for a more in-detail documentation #[derive(Copy, Clone)] -pub struct BestFitting<'a, Context> { +pub struct FormatBestFitting<'a, Context> { variants: Arguments<'a, Context>, } -impl<'a, Context> BestFitting<'a, Context> { +impl<'a, Context> FormatBestFitting<'a, Context> { /// Creates a new best fitting IR with the given variants. The method itself isn't unsafe /// but it is to discourage people from using it because the printer will panic if /// the slice doesn't contain at least the least and most expanded variants. @@ -2154,7 +2154,7 @@ impl<'a, Context> BestFitting<'a, Context> { } } -impl Format for BestFitting<'_, Context> { +impl Format for FormatBestFitting<'_, Context> { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { let mut buffer = VecBuffer::new(f.state_mut()); let variants = self.variants.items(); diff --git a/crates/ruff_formatter/src/lib.rs b/crates/ruff_formatter/src/lib.rs index a5e9401412..e1f9e4f21e 100644 --- a/crates/ruff_formatter/src/lib.rs +++ b/crates/ruff_formatter/src/lib.rs @@ -48,7 +48,7 @@ pub use buffer::{ Buffer, BufferExtensions, BufferSnapshot, Inspect, PreambleBuffer, RemoveSoftLinesBuffer, VecBuffer, }; -pub use builders::BestFitting; +pub use builders::FormatBestFitting; pub use source_code::{SourceCode, SourceCodeSlice}; pub use crate::diagnostics::{ActualStart, FormatError, InvalidDocumentError, PrintError}; diff --git a/crates/ruff_formatter/src/macros.rs b/crates/ruff_formatter/src/macros.rs index 88ad961240..fb6c66e6fa 100644 --- a/crates/ruff_formatter/src/macros.rs +++ b/crates/ruff_formatter/src/macros.rs @@ -320,7 +320,7 @@ macro_rules! format { /// the content up to the first non-soft line break without exceeding the configured print width. /// This definition differs from groups as that non-soft line breaks make group expand. /// -/// [crate::BestFitting] acts as a "break" boundary, meaning that it is considered to fit +/// [crate::FormatBestFitting] acts as a "break" boundary, meaning that it is considered to fit /// /// /// [`Flat`]: crate::format_element::PrintMode::Flat @@ -330,7 +330,7 @@ macro_rules! format { macro_rules! best_fitting { ($least_expanded:expr, $($tail:expr),+ $(,)?) => {{ unsafe { - $crate::BestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+)) + $crate::FormatBestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+)) } }} } From 3fa4440d8798f988bf19592b24a1d787f11a5d62 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 3 Jun 2023 22:28:25 -0400 Subject: [PATCH 13/41] Modify semantic model API to push bindings upon creation (#4846) --- crates/ruff/src/checkers/ast/mod.rs | 90 ++++++++++++------------ crates/ruff_python_semantic/src/model.rs | 18 ++--- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 444a1a98dd..c3fcb19cd9 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -259,12 +259,11 @@ where if !self.semantic_model.scope_id.is_global() { for (name, range) in names.iter().zip(ranges.iter()) { // Add a binding to the current scope. - let binding = self.semantic_model.declared_binding( + let binding_id = self.semantic_model.push_binding( *range, BindingKind::Global, BindingFlags::empty(), ); - let binding_id = self.semantic_model.bindings.push(binding); let scope = self.semantic_model.scope_mut(); scope.add(name, binding_id); } @@ -282,12 +281,11 @@ where if !self.semantic_model.scope_id.is_global() { for (name, range) in names.iter().zip(ranges.iter()) { // Add a binding to the current scope. - let binding = self.semantic_model.declared_binding( + let binding_id = self.semantic_model.push_binding( *range, BindingKind::Nonlocal, BindingFlags::empty(), ); - let binding_id = self.semantic_model.bindings.push(binding); let scope = self.semantic_model.scope_mut(); scope.add(name, binding_id); } @@ -4319,9 +4317,25 @@ impl<'a> Checker<'a> { kind: BindingKind<'a>, flags: BindingFlags, ) -> BindingId { - let binding_id = self.semantic_model.bindings.next_id(); - let binding = self.semantic_model.declared_binding(range, kind, flags); + // Determine the scope to which the binding belongs. + // Per [PEP 572](https://peps.python.org/pep-0572/#scope-of-the-target), named + // expressions in generators and comprehensions bind to the scope that contains the + // outermost comprehension. + let scope_id = if kind.is_named_expr_assignment() { + self.semantic_model + .scopes + .ancestor_ids(self.semantic_model.scope_id) + .find_or_last(|scope_id| !self.semantic_model.scopes[*scope_id].kind.is_generator()) + .unwrap_or(self.semantic_model.scope_id) + } else { + self.semantic_model.scope_id + }; + // Create the `Binding`. + let binding_id = self.semantic_model.push_binding(range, kind, flags); + let binding = &self.semantic_model.bindings[binding_id]; + + // Determine whether the binding shadows any existing bindings. if let Some((stack_index, existing_binding_id)) = self .semantic_model .scopes @@ -4408,56 +4422,43 @@ impl<'a> Checker<'a> { } } - // Per [PEP 572](https://peps.python.org/pep-0572/#scope-of-the-target), named - // expressions in generators and comprehensions bind to the scope that contains the - // outermost comprehension. - let scope_id = if binding.kind.is_named_expr_assignment() { - self.semantic_model - .scopes - .ancestor_ids(self.semantic_model.scope_id) - .find_or_last(|scope_id| !self.semantic_model.scopes[*scope_id].kind.is_generator()) - .unwrap_or(self.semantic_model.scope_id) - } else { - self.semantic_model.scope_id - }; - let scope = &mut self.semantic_model.scopes[scope_id]; - - let binding = if let Some(binding_id) = scope.get(name) { - let existing = &self.semantic_model.bindings[binding_id]; + // If there's an existing binding in this scope, copy its references. + if let Some(existing) = self.semantic_model.scopes[scope_id] + .get(name) + .map(|binding_id| &self.semantic_model.bindings[binding_id]) + { match &existing.kind { BindingKind::Builtin => { // Avoid overriding builtins. - binding } kind @ (BindingKind::Global | BindingKind::Nonlocal) => { - // If the original binding was a global or nonlocal, and the new binding conflicts within - // the current scope, then the new binding is also as the same. - Binding { - references: existing.references.clone(), - kind: kind.clone(), - ..binding - } + // If the original binding was a global or nonlocal, then the new binding is + // too. + let references = existing.references.clone(); + self.semantic_model.bindings[binding_id].kind = kind.clone(); + self.semantic_model.bindings[binding_id].references = references; + } + _ => { + let references = existing.references.clone(); + self.semantic_model.bindings[binding_id].references = references; } - _ => Binding { - references: existing.references.clone(), - ..binding - }, } - } else { - binding - }; - // Don't treat annotations as assignments if there is an existing value - // in scope. - if binding.kind.is_annotation() && scope.defines(name) { - return self.semantic_model.bindings.push(binding); + // If this is an annotation, and we already have an existing value in the same scope, + // don't treat it as an assignment (i.e., avoid adding it to the scope). + if self.semantic_model.bindings[binding_id] + .kind + .is_annotation() + { + return binding_id; + } } // Add the binding to the scope. + let scope = &mut self.semantic_model.scopes[scope_id]; scope.add(name, binding_id); - // Add the binding to the arena. - self.semantic_model.bindings.push(binding) + binding_id } fn bind_builtins(&mut self) { @@ -4468,8 +4469,7 @@ impl<'a> Checker<'a> { .chain(self.settings.builtins.iter().map(String::as_str)) { // Add the builtin to the scope. - let binding = self.semantic_model.builtin_binding(); - let binding_id = self.semantic_model.bindings.push(binding); + let binding_id = self.semantic_model.push_builtin(); let scope = self.semantic_model.scope_mut(); scope.add(builtin, binding_id); } diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index e98125c243..d4d723a108 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -115,8 +115,8 @@ impl<'a> SemanticModel<'a> { } /// Create a new [`Binding`] for a builtin. - pub fn builtin_binding(&self) -> Binding<'static> { - Binding { + pub fn push_builtin(&mut self) -> BindingId { + self.bindings.push(Binding { range: TextRange::default(), kind: BindingKind::Builtin, references: Vec::new(), @@ -124,17 +124,17 @@ impl<'a> SemanticModel<'a> { source: None, context: ExecutionContext::Runtime, exceptions: Exceptions::empty(), - } + }) } - /// Create a new `Binding` for the given `name` and `range`. - pub fn declared_binding( - &self, + /// Create a new [`Binding`] for the given `name` and `range`. + pub fn push_binding( + &mut self, range: TextRange, kind: BindingKind<'a>, flags: BindingFlags, - ) -> Binding<'a> { - Binding { + ) -> BindingId { + self.bindings.push(Binding { range, kind, flags, @@ -142,7 +142,7 @@ impl<'a> SemanticModel<'a> { source: self.stmt_id, context: self.execution_context(), exceptions: self.exceptions(), - } + }) } /// Return the current `Binding` for a given `name`. From 466719247bc68c0b608bc85be341c5aa5e2a5ec2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 4 Jun 2023 00:18:46 -0400 Subject: [PATCH 14/41] Invert parent-shadowed bindings map (#4847) --- crates/ruff/src/checkers/ast/mod.rs | 104 ++++++++++------------- crates/ruff_python_semantic/src/model.rs | 14 ++- 2 files changed, 57 insertions(+), 61 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index c3fcb19cd9..7a15483683 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -4336,7 +4336,7 @@ impl<'a> Checker<'a> { let binding = &self.semantic_model.bindings[binding_id]; // Determine whether the binding shadows any existing bindings. - if let Some((stack_index, existing_binding_id)) = self + if let Some((stack_index, shadowed_id)) = self .semantic_model .scopes .ancestors(self.semantic_model.scope_id) @@ -4345,26 +4345,26 @@ impl<'a> Checker<'a> { scope.get(name).map(|binding_id| (stack_index, binding_id)) }) { - let existing = &self.semantic_model.bindings[existing_binding_id]; + let shadowed = &self.semantic_model.bindings[shadowed_id]; let in_current_scope = stack_index == 0; - if !existing.kind.is_builtin() - && existing.source.map_or(true, |left| { + if !shadowed.kind.is_builtin() + && shadowed.source.map_or(true, |left| { binding.source.map_or(true, |right| { !branch_detection::different_forks(left, right, &self.semantic_model.stmts) }) }) { - let existing_is_import = matches!( - existing.kind, + let shadows_import = matches!( + shadowed.kind, BindingKind::Importation(..) | BindingKind::FromImportation(..) | BindingKind::SubmoduleImportation(..) | BindingKind::FutureImportation ); - if binding.kind.is_loop_var() && existing_is_import { + if binding.kind.is_loop_var() && shadows_import { if self.enabled(Rule::ImportShadowedByLoopVar) { #[allow(deprecated)] - let line = self.locator.compute_line_index(existing.range.start()); + let line = self.locator.compute_line_index(shadowed.range.start()); self.diagnostics.push(Diagnostic::new( pyflakes::rules::ImportShadowedByLoopVar { @@ -4375,21 +4375,21 @@ impl<'a> Checker<'a> { )); } } else if in_current_scope { - if !existing.is_used() - && binding.redefines(existing) - && (!self.settings.dummy_variable_rgx.is_match(name) || existing_is_import) - && !(existing.kind.is_function_definition() + if !shadowed.is_used() + && binding.redefines(shadowed) + && (!self.settings.dummy_variable_rgx.is_match(name) || shadows_import) + && !(shadowed.kind.is_function_definition() && analyze::visibility::is_overload( &self.semantic_model, cast::decorator_list( - self.semantic_model.stmts[existing.source.unwrap()], + self.semantic_model.stmts[shadowed.source.unwrap()], ), )) { if self.enabled(Rule::RedefinedWhileUnused) { #[allow(deprecated)] let line = self.locator.compute_line_index( - existing + shadowed .trimmed_range(&self.semantic_model, self.locator) .start(), ); @@ -4412,34 +4412,32 @@ impl<'a> Checker<'a> { self.diagnostics.push(diagnostic); } } - } else if existing_is_import && binding.redefines(existing) { + } else if shadows_import && binding.redefines(shadowed) { self.semantic_model .shadowed_bindings - .entry(existing_binding_id) - .or_insert_with(Vec::new) - .push(binding_id); + .insert(binding_id, shadowed_id); } } } // If there's an existing binding in this scope, copy its references. - if let Some(existing) = self.semantic_model.scopes[scope_id] + if let Some(shadowed) = self.semantic_model.scopes[scope_id] .get(name) .map(|binding_id| &self.semantic_model.bindings[binding_id]) { - match &existing.kind { + match &shadowed.kind { BindingKind::Builtin => { // Avoid overriding builtins. } kind @ (BindingKind::Global | BindingKind::Nonlocal) => { // If the original binding was a global or nonlocal, then the new binding is // too. - let references = existing.references.clone(); + let references = shadowed.references.clone(); self.semantic_model.bindings[binding_id].kind = kind.clone(); self.semantic_model.bindings[binding_id].references = references; } _ => { - let references = existing.references.clone(); + let references = shadowed.references.clone(); self.semantic_model.bindings[binding_id].references = references; } } @@ -5054,52 +5052,40 @@ impl<'a> Checker<'a> { } // Look for any bindings that were redefined in another scope, and remain - // unused. Note that we only store references in `redefinitions` if + // unused. Note that we only store references in `shadowed_bindings` if // the bindings are in different scopes. if self.enabled(Rule::RedefinedWhileUnused) { for (name, binding_id) in scope.bindings() { - let binding = &self.semantic_model.bindings[binding_id]; - - if matches!( - binding.kind, - BindingKind::Importation(..) - | BindingKind::FromImportation(..) - | BindingKind::SubmoduleImportation(..) - ) { - if binding.is_used() { + if let Some(shadowed) = self.semantic_model.shadowed_binding(binding_id) { + if shadowed.is_used() { continue; } - if let Some(shadowed_ids) = - self.semantic_model.shadowed_bindings.get(&binding_id) - { - for binding_id in shadowed_ids.iter().copied() { - let rebound = &self.semantic_model.bindings[binding_id]; - #[allow(deprecated)] - let line = self.locator.compute_line_index( - binding - .trimmed_range(&self.semantic_model, self.locator) - .start(), - ); + let binding = &self.semantic_model.bindings[binding_id]; - let mut diagnostic = Diagnostic::new( - pyflakes::rules::RedefinedWhileUnused { - name: (*name).to_string(), - line, - }, - rebound.trimmed_range(&self.semantic_model, self.locator), - ); - if let Some(source) = rebound.source { - let parent = &self.semantic_model.stmts[source]; - if matches!(parent, Stmt::ImportFrom(_)) - && parent.range().contains_range(rebound.range) - { - diagnostic.set_parent(parent.start()); - } - }; - diagnostics.push(diagnostic); + #[allow(deprecated)] + let line = self.locator.compute_line_index( + shadowed + .trimmed_range(&self.semantic_model, self.locator) + .start(), + ); + + let mut diagnostic = Diagnostic::new( + pyflakes::rules::RedefinedWhileUnused { + name: (*name).to_string(), + line, + }, + binding.trimmed_range(&self.semantic_model, self.locator), + ); + if let Some(parent) = binding + .source + .map(|source| &self.semantic_model.stmts[source]) + { + if parent.is_import_from_stmt() { + diagnostic.set_parent(parent.start()); } } + diagnostics.push(diagnostic); } } } diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index d4d723a108..589e06624c 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -47,7 +47,7 @@ pub struct SemanticModel<'a> { // Arena of global bindings. globals: GlobalsArena<'a>, // Map from binding index to indexes of bindings that shadow it in other scopes. - pub shadowed_bindings: HashMap, BuildNoHashHasher>, + pub shadowed_bindings: HashMap>, // Body iteration; used to peek at siblings. pub body: &'a [Stmt], pub body_index: usize, @@ -145,13 +145,23 @@ impl<'a> SemanticModel<'a> { }) } - /// Return the current `Binding` for a given `name`. + /// Return the current [`Binding`] for a given `name`. pub fn find_binding(&self, member: &str) -> Option<&Binding> { self.scopes() .find_map(|scope| scope.get(member)) .map(|binding_id| &self.bindings[binding_id]) } + /// Return the [`Binding`] that the given [`BindingId`] shadows, if any. + /// + /// Note that this will only return bindings that are shadowed by a binding in a parent scope. + pub fn shadowed_binding(&self, binding_id: BindingId) -> Option<&Binding> { + self.shadowed_bindings + .get(&binding_id) + .copied() + .map(|id| &self.bindings[id]) + } + /// Return `true` if `member` is bound as a builtin. pub fn is_builtin(&self, member: &str) -> bool { self.find_binding(member) From 694bf7f5b8c829462c31cb30d410bc0c685b10f6 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sun, 4 Jun 2023 19:51:47 +0200 Subject: [PATCH 15/41] Upgrade to Rust 1.70 (#4848) --- Cargo.toml | 2 +- rust-toolchain | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 89f15ffe0b..8e477ffd6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*"] [workspace.package] edition = "2021" -rust-version = "1.69" +rust-version = "1.70" homepage = "https://beta.ruff.rs/docs/" documentation = "https://beta.ruff.rs/docs/" repository = "https://github.com/charliermarsh/ruff" diff --git a/rust-toolchain b/rust-toolchain index 883d7d6539..bfe79d0bdd 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.69 +1.70 From a0721912a43aefb6042f843b407de0cc97bf894e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 4 Jun 2023 22:03:21 -0400 Subject: [PATCH 16/41] Invert structure of Scope#shadowed_bindings (#4855) --- crates/ruff_python_semantic/src/scope.rs | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index 6d6c2bde29..09cadf47cf 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -1,3 +1,5 @@ +use nohash_hasher::{BuildNoHashHasher, IntMap}; +use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use rustc_hash::FxHashMap; @@ -17,10 +19,10 @@ pub struct Scope<'a> { /// A list of star imports in this scope. These represent _module_ imports (e.g., `sys` in /// `from sys import *`), rather than individual bindings (e.g., individual members in `sys`). star_imports: Vec>, - /// A map from bound name to binding index, for current bindings. + /// A map from bound name to binding ID. bindings: FxHashMap<&'a str, BindingId>, - /// A map from bound name to binding index, for bindings that were shadowed later in the scope. - shadowed_bindings: FxHashMap<&'a str, Vec>, + /// A map from binding ID to binding ID that it shadows. + shadowed_bindings: HashMap>, /// Index into the globals arena, if the scope contains any globally-declared symbols. globals_id: Option, } @@ -33,7 +35,7 @@ impl<'a> Scope<'a> { uses_locals: false, star_imports: Vec::default(), bindings: FxHashMap::default(), - shadowed_bindings: FxHashMap::default(), + shadowed_bindings: IntMap::default(), globals_id: None, } } @@ -45,7 +47,7 @@ impl<'a> Scope<'a> { uses_locals: false, star_imports: Vec::default(), bindings: FxHashMap::default(), - shadowed_bindings: FxHashMap::default(), + shadowed_bindings: IntMap::default(), globals_id: None, } } @@ -57,9 +59,9 @@ impl<'a> Scope<'a> { /// Adds a new binding with the given name to this scope. pub fn add(&mut self, name: &'a str, id: BindingId) -> Option { - if let Some(id) = self.bindings.insert(name, id) { - self.shadowed_bindings.entry(name).or_default().push(id); - Some(id) + if let Some(shadowed) = self.bindings.insert(name, id) { + self.shadowed_bindings.insert(id, shadowed); + Some(shadowed) } else { None } @@ -88,11 +90,9 @@ impl<'a> Scope<'a> { /// Returns an iterator over all [bindings](BindingId) bound to the given name, including /// those that were shadowed by later bindings. pub fn bindings_for_name(&self, name: &str) -> impl Iterator + '_ { - self.bindings - .get(name) - .into_iter() - .chain(self.shadowed_bindings.get(name).into_iter().flatten().rev()) - .copied() + std::iter::successors(self.bindings.get(name).copied(), |id| { + self.shadowed_bindings.get(id).copied() + }) } /// Adds a reference to a star import (e.g., `from sys import *`) to this scope. From 95e61987d1cd9c9b603c5ea22cb6db8249eb409a Mon Sep 17 00:00:00 2001 From: Evan Rittenhouse Date: Sun, 4 Jun 2023 21:25:00 -0500 Subject: [PATCH 17/41] Change fixable_set to include RuleSelector::All/Nursery (#4852) --- crates/ruff/src/rule_selector.rs | 53 +++++++++++++++++++------------- crates/ruff/src/settings/mod.rs | 5 ++- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/crates/ruff/src/rule_selector.rs b/crates/ruff/src/rule_selector.rs index bc5a3ee523..d4d31f8efa 100644 --- a/crates/ruff/src/rule_selector.rs +++ b/crates/ruff/src/rule_selector.rs @@ -5,8 +5,8 @@ use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use strum_macros::EnumIter; -use crate::codes::RuleCodePrefix; use crate::codes::RuleIter; +use crate::codes::{self, RuleCodePrefix}; use crate::registry::{Linter, Rule, RuleNamespace}; use crate::rule_redirects::get_redirect; @@ -14,6 +14,8 @@ use crate::rule_redirects::get_redirect; pub enum RuleSelector { /// Select all rules. All, + /// Select all nursery rules. + Nursery, /// Legacy category to select both the `mccabe` and `flake8-comprehensions` linters /// via a single selector. C, @@ -39,30 +41,30 @@ impl FromStr for RuleSelector { type Err = ParseError; fn from_str(s: &str) -> Result { - if s == "ALL" { - Ok(Self::All) - } else if s == "C" { - Ok(Self::C) - } else if s == "T" { - Ok(Self::T) - } else { - let (s, redirected_from) = match get_redirect(s) { - Some((from, target)) => (target, Some(from)), - None => (s, None), - }; + match s { + "ALL" => Ok(Self::All), + "NURSERY" => Ok(Self::Nursery), + "C" => Ok(Self::C), + "T" => Ok(Self::T), + _ => { + let (s, redirected_from) = match get_redirect(s) { + Some((from, target)) => (target, Some(from)), + None => (s, None), + }; - let (linter, code) = - Linter::parse_code(s).ok_or_else(|| ParseError::Unknown(s.to_string()))?; + let (linter, code) = + Linter::parse_code(s).ok_or_else(|| ParseError::Unknown(s.to_string()))?; - if code.is_empty() { - return Ok(Self::Linter(linter)); + if code.is_empty() { + return Ok(Self::Linter(linter)); + } + + Ok(Self::Prefix { + prefix: RuleCodePrefix::parse(&linter, code) + .map_err(|_| ParseError::Unknown(s.to_string()))?, + redirected_from, + }) } - - Ok(Self::Prefix { - prefix: RuleCodePrefix::parse(&linter, code) - .map_err(|_| ParseError::Unknown(s.to_string()))?, - redirected_from, - }) } } } @@ -79,6 +81,7 @@ impl RuleSelector { pub fn prefix_and_code(&self) -> (&'static str, &'static str) { match self { RuleSelector::All => ("", "ALL"), + RuleSelector::Nursery => ("", "NURSERY"), RuleSelector::C => ("", "C"), RuleSelector::T => ("", "T"), RuleSelector::Prefix { prefix, .. } => { @@ -157,6 +160,9 @@ impl IntoIterator for &RuleSelector { RuleSelector::All => { RuleSelectorIter::All(Rule::iter().filter(|rule| select_all(*rule))) } + RuleSelector::Nursery => { + RuleSelectorIter::Nursery(Rule::iter().filter(codes::Rule::is_nursery)) + } RuleSelector::C => RuleSelectorIter::Chain( Linter::Flake8Comprehensions .into_iter() @@ -175,6 +181,7 @@ impl IntoIterator for &RuleSelector { pub enum RuleSelectorIter { All(std::iter::Filter bool>), + Nursery(std::iter::Filter bool>), Chain(std::iter::Chain, std::vec::IntoIter>), Vec(std::vec::IntoIter), } @@ -185,6 +192,7 @@ impl Iterator for RuleSelectorIter { fn next(&mut self) -> Option { match self { RuleSelectorIter::All(iter) => iter.next(), + RuleSelectorIter::Nursery(iter) => iter.next(), RuleSelectorIter::Chain(iter) => iter.next(), RuleSelectorIter::Vec(iter) => iter.next(), } @@ -262,6 +270,7 @@ impl RuleSelector { pub(crate) fn specificity(&self) -> Specificity { match self { RuleSelector::All => Specificity::All, + RuleSelector::Nursery => Specificity::All, RuleSelector::T => Specificity::LinterGroup, RuleSelector::C => Specificity::LinterGroup, RuleSelector::Linter(..) => Specificity::Linter, diff --git a/crates/ruff/src/settings/mod.rs b/crates/ruff/src/settings/mod.rs index 453a93dac5..f2e2aec2db 100644 --- a/crates/ruff/src/settings/mod.rs +++ b/crates/ruff/src/settings/mod.rs @@ -259,7 +259,10 @@ impl From<&Configuration> for RuleTable { // The select_set keeps track of which rules have been selected. let mut select_set: RuleSet = defaults::PREFIXES.iter().flatten().collect(); // The fixable set keeps track of which rules are fixable. - let mut fixable_set: RuleSet = RuleSelector::All.into_iter().collect(); + let mut fixable_set: RuleSet = RuleSelector::All + .into_iter() + .chain(RuleSelector::Nursery.into_iter()) + .collect(); // Ignores normally only subtract from the current set of selected // rules. By that logic the ignore in `select = [], ignore = ["E501"]` From 1fba98681e82277629206e3d3292d37d2792aa75 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 4 Jun 2023 22:31:30 -0400 Subject: [PATCH 18/41] Remove codes import from rule_selector.rs (#4856) --- crates/ruff/src/rule_selector.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/crates/ruff/src/rule_selector.rs b/crates/ruff/src/rule_selector.rs index d4d31f8efa..6985c1be3c 100644 --- a/crates/ruff/src/rule_selector.rs +++ b/crates/ruff/src/rule_selector.rs @@ -5,14 +5,14 @@ use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use strum_macros::EnumIter; +use crate::codes::RuleCodePrefix; use crate::codes::RuleIter; -use crate::codes::{self, RuleCodePrefix}; use crate::registry::{Linter, Rule, RuleNamespace}; use crate::rule_redirects::get_redirect; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum RuleSelector { - /// Select all rules. + /// Select all stable rules. All, /// Select all nursery rules. Nursery, @@ -144,13 +144,6 @@ impl From for RuleSelector { } } -/// Returns `true` if the given rule should be selected by the `RuleSelector::All` selector. -fn select_all(rule: Rule) -> bool { - // Nursery rules have to be explicitly selected, so we ignore them when looking at - // prefixes. - !rule.is_nursery() -} - impl IntoIterator for &RuleSelector { type Item = Rule; type IntoIter = RuleSelectorIter; @@ -158,10 +151,10 @@ impl IntoIterator for &RuleSelector { fn into_iter(self) -> Self::IntoIter { match self { RuleSelector::All => { - RuleSelectorIter::All(Rule::iter().filter(|rule| select_all(*rule))) + RuleSelectorIter::All(Rule::iter().filter(|rule| !rule.is_nursery())) } RuleSelector::Nursery => { - RuleSelectorIter::Nursery(Rule::iter().filter(codes::Rule::is_nursery)) + RuleSelectorIter::Nursery(Rule::iter().filter(Rule::is_nursery)) } RuleSelector::C => RuleSelectorIter::Chain( Linter::Flake8Comprehensions From 576e0c7b80300c922390f7415107ca6648cfa0bb Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 5 Jun 2023 09:22:43 +0200 Subject: [PATCH 19/41] Abstract stylist to libcst style conversion (#4749) * Abstract codegen with stylist into a CodegenStylist trait Replace all duplicate invocations of ```rust let mut state = CodegenState { default_newline: &stylist.line_ending(), default_indent: stylist.indentation(), ..CodegenState::default() } tree.codegen(&mut state); state.to_string() ``` with ```rust tree.codegen_stylist(&stylist); ``` No functional changes. --- crates/ruff/src/autofix/codemods.rs | 34 +-- crates/ruff/src/importer/mod.rs | 14 +- .../src/rules/flake8_comprehensions/fixes.rs | 201 ++++++------------ .../flake8_pytest_style/rules/assertion.rs | 16 +- .../src/rules/flake8_simplify/rules/fix_if.rs | 15 +- .../rules/flake8_simplify/rules/fix_with.rs | 12 +- .../flake8_simplify/rules/key_in_dict.rs | 12 +- .../flake8_simplify/rules/yoda_conditions.rs | 11 +- crates/ruff/src/rules/pyflakes/fixes.rs | 39 ++-- crates/ruff/src/rules/pyupgrade/fixes.rs | 24 +-- .../pyupgrade/rules/deprecated_mock_import.rs | 31 +-- .../rules/pyupgrade/rules/format_literals.rs | 22 +- .../explicit_f_string_type_conversion.rs | 11 +- 13 files changed, 139 insertions(+), 303 deletions(-) diff --git a/crates/ruff/src/autofix/codemods.rs b/crates/ruff/src/autofix/codemods.rs index 389107f787..6d290d81a0 100644 --- a/crates/ruff/src/autofix/codemods.rs +++ b/crates/ruff/src/autofix/codemods.rs @@ -11,6 +11,23 @@ use ruff_python_ast::source_code::{Locator, Stylist}; use crate::cst::helpers::compose_module_path; use crate::cst::matchers::match_statement; +/// Glue code to make libcst codegen work with ruff's Stylist +pub(crate) trait CodegenStylist<'a>: Codegen<'a> { + fn codegen_stylist(&self, stylist: &'a Stylist) -> String; +} + +impl<'a, T: Codegen<'a>> CodegenStylist<'a> for T { + fn codegen_stylist(&self, stylist: &'a Stylist) -> String { + let mut state = CodegenState { + default_newline: stylist.line_ending().as_str(), + default_indent: stylist.indentation(), + ..Default::default() + }; + self.codegen(&mut state); + state.to_string() + } +} + /// Given an import statement, remove any imports that are specified in the `imports` iterator. /// /// Returns `Ok(None)` if the statement is empty after removing the imports. @@ -114,14 +131,7 @@ pub(crate) fn remove_imports<'a>( return Ok(None); } - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Some(state.to_string())) + Ok(Some(tree.codegen_stylist(stylist))) } /// Given an import statement, remove any imports that are not specified in the `imports` slice. @@ -200,11 +210,5 @@ pub(crate) fn retain_imports( } } - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - Ok(state.to_string()) + Ok(tree.codegen_stylist(stylist)) } diff --git a/crates/ruff/src/importer/mod.rs b/crates/ruff/src/importer/mod.rs index ca05d6b958..9df741641f 100644 --- a/crates/ruff/src/importer/mod.rs +++ b/crates/ruff/src/importer/mod.rs @@ -3,11 +3,12 @@ use std::error::Error; use anyhow::Result; -use libcst_native::{Codegen, CodegenState, ImportAlias, Name, NameOrAttribute}; +use libcst_native::{ImportAlias, Name, NameOrAttribute}; use ruff_text_size::TextSize; use rustpython_parser::ast::{self, Ranged, Stmt, Suite}; use crate::autofix; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::Edit; use ruff_python_ast::imports::{AnyImport, Import, ImportFrom}; use ruff_python_ast::source_code::{Locator, Stylist}; @@ -324,13 +325,10 @@ impl<'a> Importer<'a> { asname: None, comma: aliases.last().and_then(|alias| alias.comma.clone()), }); - let mut state = CodegenState { - default_newline: &self.stylist.line_ending(), - default_indent: self.stylist.indentation(), - ..CodegenState::default() - }; - statement.codegen(&mut state); - Ok(Edit::range_replacement(state.to_string(), stmt.range())) + Ok(Edit::range_replacement( + statement.codegen_stylist(self.stylist), + stmt.range(), + )) } /// Add a `TYPE_CHECKING` block to the given module. diff --git a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs index dcd062187c..b10156a39a 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs @@ -1,14 +1,15 @@ use anyhow::{bail, Result}; use itertools::Itertools; use libcst_native::{ - Arg, AssignEqual, AssignTargetExpression, Call, Codegen, CodegenState, Comment, CompFor, Dict, - DictComp, DictElement, Element, EmptyLine, Expression, GeneratorExp, LeftCurlyBrace, LeftParen, - LeftSquareBracket, List, ListComp, Name, ParenthesizableWhitespace, ParenthesizedWhitespace, - RightCurlyBrace, RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace, + Arg, AssignEqual, AssignTargetExpression, Call, Comment, CompFor, Dict, DictComp, DictElement, + Element, EmptyLine, Expression, GeneratorExp, LeftCurlyBrace, LeftParen, LeftSquareBracket, + List, ListComp, Name, ParenthesizableWhitespace, ParenthesizedWhitespace, RightCurlyBrace, + RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace, TrailingWhitespace, Tuple, }; use rustpython_parser::ast::Ranged; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::source_code::{Locator, Stylist}; @@ -44,14 +45,10 @@ pub(crate) fn fix_unnecessary_generator_list( rpar: generator_exp.rpar.clone(), })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C401) Convert `set(x for x in y)` to `{x for x in y}`. @@ -82,14 +79,7 @@ pub(crate) fn fix_unnecessary_generator_set( rpar: generator_exp.rpar.clone(), })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - let mut content = state.to_string(); + let mut content = tree.codegen_stylist(stylist); // If the expression is embedded in an f-string, surround it with spaces to avoid // syntax errors. @@ -136,14 +126,7 @@ pub(crate) fn fix_unnecessary_generator_dict( whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" ")), })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - let mut content = state.to_string(); + let mut content = tree.codegen_stylist(stylist); // If the expression is embedded in an f-string, surround it with spaces to avoid // syntax errors. @@ -182,14 +165,10 @@ pub(crate) fn fix_unnecessary_list_comprehension_set( rpar: list_comp.rpar.clone(), })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C404) Convert `dict([(i, i) for i in range(3)])` to `{i: i for i in @@ -229,14 +208,10 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict( rpar: list_comp.rpar.clone(), })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// Drop a trailing comma from a list of tuple elements. @@ -318,14 +293,10 @@ pub(crate) fn fix_unnecessary_literal_set( })); } - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C406) Convert `dict([(1, 2)])` to `{1: 2}`. @@ -386,14 +357,10 @@ pub(crate) fn fix_unnecessary_literal_dict( rpar: vec![], })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C408) @@ -495,14 +462,10 @@ pub(crate) fn fix_unnecessary_collection_call( } }; - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C409) Convert `tuple([1, 2])` to `tuple(1, 2)` @@ -549,14 +512,10 @@ pub(crate) fn fix_unnecessary_literal_within_tuple_call( }], })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C410) Convert `list([1, 2])` to `[1, 2]` @@ -605,14 +564,10 @@ pub(crate) fn fix_unnecessary_literal_within_list_call( rpar: vec![], })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C411) Convert `list([i * i for i in x])` to `[i * i for i in x]`. @@ -629,14 +584,10 @@ pub(crate) fn fix_unnecessary_list_call( tree = arg.value.clone(); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C413) Convert `list(sorted([2, 3, 1]))` to `sorted([2, 3, 1])`. @@ -747,14 +698,10 @@ pub(crate) fn fix_unnecessary_call_around_sorted( } } - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C414) Convert `sorted(list(foo))` to `sorted(foo)` @@ -781,14 +728,10 @@ pub(crate) fn fix_unnecessary_double_cast_or_process( None => bail!("Expected at least one argument in outer function call"), }; - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C416) Convert `[i for i in x]` to `list(x)`. @@ -872,14 +815,10 @@ pub(crate) fn fix_unnecessary_comprehension( } } - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C417) Convert `map(lambda x: x * 2, bar)` to `(x * 2 for x in bar)`. @@ -1018,14 +957,7 @@ pub(crate) fn fix_unnecessary_map( } } - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - let mut content = state.to_string(); + let mut content = tree.codegen_stylist(stylist); // If the expression is embedded in an f-string, surround it with spaces to avoid // syntax errors. @@ -1054,14 +986,10 @@ pub(crate) fn fix_unnecessary_literal_within_dict_call( tree = arg.value.clone(); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C419) Convert `[i for i in a]` into `i for i in a` @@ -1231,15 +1159,8 @@ pub(crate) fn fix_unnecessary_comprehension_any_all( _ => whitespace_after_arg, }; - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - Ok(Fix::suggested(Edit::range_replacement( - state.to_string(), + tree.codegen_stylist(stylist), expr.range(), ))) } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs index aa59cf845d..ee2f4f7c10 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs @@ -3,12 +3,13 @@ use std::borrow::Cow; use anyhow::bail; use anyhow::Result; use libcst_native::{ - Assert, BooleanOp, Codegen, CodegenState, CompoundStatement, Expression, - ParenthesizableWhitespace, ParenthesizedNode, SimpleStatementLine, SimpleWhitespace, - SmallStatement, Statement, TrailingWhitespace, UnaryOp, UnaryOperation, + Assert, BooleanOp, CompoundStatement, Expression, ParenthesizableWhitespace, ParenthesizedNode, + SimpleStatementLine, SimpleWhitespace, SmallStatement, Statement, TrailingWhitespace, UnaryOp, + UnaryOperation, }; use rustpython_parser::ast::{self, Boolop, Excepthandler, Expr, Keyword, Ranged, Stmt, Unaryop}; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{has_comments_in, Truthiness}; @@ -410,15 +411,8 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> })); } - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - // Reconstruct and reformat the code. - let module_text = state.to_string(); + let module_text = tree.codegen_stylist(stylist); let contents = if outer_indent.is_empty() { module_text } else { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs b/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs index 4a58149ff8..a36364dedf 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs @@ -2,12 +2,12 @@ use std::borrow::Cow; use anyhow::{bail, Result}; use libcst_native::{ - BooleanOp, BooleanOperation, Codegen, CodegenState, CompoundStatement, Expression, If, - LeftParen, ParenthesizableWhitespace, ParenthesizedNode, RightParen, SimpleWhitespace, - Statement, Suite, + BooleanOp, BooleanOperation, CompoundStatement, Expression, If, LeftParen, + ParenthesizableWhitespace, ParenthesizedNode, RightParen, SimpleWhitespace, Statement, Suite, }; use rustpython_parser::ast::Ranged; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; use ruff_python_ast::whitespace; @@ -111,15 +111,8 @@ pub(crate) fn fix_nested_if_statements( })); outer_if.body = inner_if.body.clone(); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..Default::default() - }; - tree.codegen(&mut state); - // Reconstruct and reformat the code. - let module_text = state.to_string(); + let module_text = tree.codegen_stylist(stylist); let module_text = if outer_indent.is_empty() { &module_text } else { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs b/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs index eaae0f8575..b3636cabbc 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs @@ -1,7 +1,8 @@ use anyhow::{bail, Result}; -use libcst_native::{Codegen, CodegenState, CompoundStatement, Statement, Suite, With}; +use libcst_native::{CompoundStatement, Statement, Suite, With}; use rustpython_parser::ast::Ranged; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; use ruff_python_ast::whitespace; @@ -70,15 +71,8 @@ pub(crate) fn fix_multiple_with_statements( } outer_with.body = inner_with.body.clone(); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - // Reconstruct and reformat the code. - let module_text = state.to_string(); + let module_text = tree.codegen_stylist(stylist); let contents = if outer_indent.is_empty() { module_text } else { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs b/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs index e63827a352..8463476e96 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs @@ -1,9 +1,10 @@ use anyhow::Result; -use libcst_native::{Codegen, CodegenState}; + use log::error; use ruff_text_size::TextRange; use rustpython_parser::ast::{self, Cmpop, Expr, Ranged}; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::Edit; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -42,14 +43,7 @@ fn get_value_content_for_key_in_dict( let call = match_call_mut(&mut expression)?; let attribute = match_attribute(&mut call.func)?; - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - attribute.value.codegen(&mut state); - - Ok(state.to_string()) + Ok(attribute.value.codegen_stylist(stylist)) } /// SIM118 diff --git a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs index e21b586f90..b790019576 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs @@ -1,7 +1,8 @@ use anyhow::Result; -use libcst_native::{Codegen, CodegenState, CompOp}; +use libcst_native::CompOp; use rustpython_parser::ast::{self, Cmpop, Expr, Ranged, Unaryop}; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::{Locator, Stylist}; @@ -117,13 +118,7 @@ fn reverse_comparison(expr: &Expr, locator: &Locator, stylist: &Stylist) -> Resu _ => panic!("Expected comparison operator"), }; - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - expression.codegen(&mut state); - Ok(state.to_string()) + Ok(expression.codegen_stylist(stylist)) } /// SIM300 diff --git a/crates/ruff/src/rules/pyflakes/fixes.rs b/crates/ruff/src/rules/pyflakes/fixes.rs index 7a69513298..83cdc6130b 100644 --- a/crates/ruff/src/rules/pyflakes/fixes.rs +++ b/crates/ruff/src/rules/pyflakes/fixes.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, bail, Ok, Result}; -use libcst_native::{Codegen, CodegenState, DictElement, Expression}; +use libcst_native::{DictElement, Expression}; use ruff_text_size::TextRange; use rustpython_format::{ FieldName, FieldNamePart, FieldType, FormatPart, FormatString, FromTemplate, @@ -7,6 +7,7 @@ use rustpython_format::{ use rustpython_parser::ast::{Excepthandler, Expr, Ranged}; use rustpython_parser::{lexer, Mode, Tok}; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; use ruff_python_ast::str::{leading_quote, raw_contents, trailing_quote}; @@ -33,14 +34,10 @@ pub(crate) fn remove_unused_format_arguments_from_dict( } if raw_contents(name.value).map_or(false, |name| unused_arguments.contains(&name))) }); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), stmt.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + stmt.range(), + )) } /// Generate a [`Edit`] to remove unused keyword arguments from a `format` call. @@ -57,14 +54,10 @@ pub(crate) fn remove_unused_keyword_arguments_from_format_call( call.args .retain(|e| !matches!(&e.keyword, Some(kw) if unused_arguments.contains(&kw.value))); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), location)) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + location, + )) } fn unparse_format_part(format_part: FormatPart) -> String { @@ -193,14 +186,10 @@ pub(crate) fn remove_unused_positional_arguments_from_format_call( simple_string.value = new_format_string.as_str(); } - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), location)) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + location, + )) } /// Generate a [`Edit`] to remove the binding from an exception handler. diff --git a/crates/ruff/src/rules/pyupgrade/fixes.rs b/crates/ruff/src/rules/pyupgrade/fixes.rs index ce242175f1..5f37034a2d 100644 --- a/crates/ruff/src/rules/pyupgrade/fixes.rs +++ b/crates/ruff/src/rules/pyupgrade/fixes.rs @@ -1,9 +1,10 @@ use anyhow::Result; -use libcst_native::{Codegen, CodegenState, ParenthesizableWhitespace}; +use libcst_native::ParenthesizableWhitespace; use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::{Expr, Ranged}; use rustpython_parser::{lexer, Mode, Tok}; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; @@ -29,14 +30,7 @@ pub(crate) fn adjust_indentation( let indented_block = match_indented_block(&mut embedding.body)?; indented_block.indent = Some(indentation); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..Default::default() - }; - indented_block.codegen(&mut state); - - let module_text = state.to_string(); + let module_text = indented_block.codegen_stylist(stylist); let module_text = module_text .strip_prefix(stylist.line_ending().as_str()) .unwrap() @@ -61,14 +55,10 @@ pub(crate) fn remove_super_arguments( body.whitespace_before_args = ParenthesizableWhitespace::default(); body.whitespace_after_func = ParenthesizableWhitespace::default(); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Some(Edit::range_replacement(state.to_string(), range)) + Some(Edit::range_replacement( + tree.codegen_stylist(stylist), + range, + )) } /// Remove any imports matching `members` from an import-from statement. diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs index f13f8b9825..9680aa8998 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs @@ -1,11 +1,12 @@ use anyhow::Result; use libcst_native::{ - AsName, AssignTargetExpression, Attribute, Codegen, CodegenState, Dot, Expression, Import, - ImportAlias, ImportFrom, ImportNames, Name, NameOrAttribute, ParenthesizableWhitespace, + AsName, AssignTargetExpression, Attribute, Dot, Expression, Import, ImportAlias, ImportFrom, + ImportNames, Name, NameOrAttribute, ParenthesizableWhitespace, }; use log::error; use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; @@ -137,14 +138,7 @@ fn format_import( } else { import.names = clean_aliases; - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - let mut content = state.to_string(); + let mut content = tree.codegen_stylist(stylist); content.push_str(&stylist.line_ending()); content.push_str(indent); content.push_str(&format_mocks(mock_aliases, indent, stylist)); @@ -187,13 +181,7 @@ fn format_import_from( lpar: vec![], rpar: vec![], }))); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - Ok(state.to_string()) + Ok(tree.codegen_stylist(stylist)) } else if let ImportFrom { names: ImportNames::Aliases(aliases), .. @@ -224,14 +212,7 @@ fn format_import_from( rpar: vec![], }))); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - let mut content = state.to_string(); + let mut content = tree.codegen_stylist(stylist); if !mock_aliases.is_empty() { content.push_str(&stylist.line_ending()); content.push_str(indent); diff --git a/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs b/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs index 83e7f4d839..0918d05078 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs @@ -1,9 +1,10 @@ use anyhow::{anyhow, bail, Result}; -use libcst_native::{Arg, Codegen, CodegenState}; +use libcst_native::Arg; use once_cell::sync::Lazy; use regex::Regex; use rustpython_parser::ast::{Expr, Ranged}; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::{Locator, Stylist}; @@ -99,27 +100,16 @@ fn generate_call( // Fix the string itself. let item = match_attribute(&mut call.func)?; - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - item.codegen(&mut state); - let cleaned = remove_specifiers(&state.to_string()); + let cleaned = remove_specifiers(&item.codegen_stylist(stylist)); call.func = Box::new(match_expression(&cleaned)?); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - expression.codegen(&mut state); - if module_text == state.to_string() { + let state = expression.codegen_stylist(stylist); + if module_text == state { // Ex) `'{' '0}'.format(1)` bail!("Failed to generate call expression for: {module_text}") } - Ok(state.to_string()) + Ok(state) } /// UP030 diff --git a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs index a9b6d989b0..9ed8091258 100644 --- a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs +++ b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs @@ -1,7 +1,7 @@ use anyhow::{bail, Result}; -use libcst_native::{Codegen, CodegenState}; use rustpython_parser::ast::{self, Expr, Ranged}; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::{Locator, Stylist}; @@ -77,15 +77,8 @@ fn fix_explicit_f_string_type_conversion( } formatted_string_expression.expression = call.args[0].value.clone(); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - expression.codegen(&mut state); - Ok(Fix::automatic(Edit::range_replacement( - state.to_string(), + expression.codegen_stylist(stylist), range, ))) } From d1d06960f037f77f5fc5054a0c19e89574283455 Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 5 Jun 2023 09:33:33 +0200 Subject: [PATCH 20/41] Add a formatter CLI for debugging (#4809) * Add a formatter CLI for debugging This adds a ruff_python_formatter cli modelled aber `rustfmt` that i use for debugging * clippy * Add print IR and print comments options Tested with `cargo run --bin ruff_python_formatter -- --print-ir --print-comments scratch.py` --- crates/ruff_python_formatter/src/cli.rs | 72 ++++++++++++++++++++++-- crates/ruff_python_formatter/src/main.rs | 52 ++++++++++++++--- 2 files changed, 113 insertions(+), 11 deletions(-) diff --git a/crates/ruff_python_formatter/src/cli.rs b/crates/ruff_python_formatter/src/cli.rs index 87d7dc06aa..f605e66407 100644 --- a/crates/ruff_python_formatter/src/cli.rs +++ b/crates/ruff_python_formatter/src/cli.rs @@ -1,11 +1,75 @@ +#![allow(clippy::print_stdout)] + use std::path::PathBuf; -use clap::{command, Parser}; +use anyhow::{bail, Context, Result}; +use clap::{command, Parser, ValueEnum}; +use rustpython_parser::lexer::lex; +use rustpython_parser::{parse_tokens, Mode}; + +use ruff_formatter::SourceCode; +use ruff_python_ast::source_code::CommentRangesBuilder; + +use crate::format_node; + +#[derive(ValueEnum, Clone, Debug)] +pub enum Emit { + /// Write back to the original files + Files, + /// Write to stdout + Stdout, +} #[derive(Parser)] #[command(author, version, about, long_about = None)] pub struct Cli { - /// Python file to round-trip. - #[arg(required = true)] - pub file: PathBuf, + /// Python files to format. If there are none, stdin will be used. `-` as stdin is not supported + pub files: Vec, + #[clap(long)] + pub emit: Option, + /// Run in 'check' mode. Exits with 0 if input is formatted correctly. Exits with 1 and prints + /// a diff if formatting is required. + #[clap(long)] + pub check: bool, + #[clap(long)] + pub print_ir: bool, + #[clap(long)] + pub print_comments: bool, +} + +pub fn format_and_debug_print(input: &str, cli: &Cli) -> Result { + let mut tokens = Vec::new(); + let mut comment_ranges = CommentRangesBuilder::default(); + + for result in lex(input, Mode::Module) { + let (token, range) = match result { + Ok((token, range)) => (token, range), + Err(err) => bail!("Source contains syntax errors {err:?}"), + }; + + comment_ranges.visit_token(&token, range); + tokens.push(Ok((token, range))); + } + + let comment_ranges = comment_ranges.finish(); + + // Parse the AST. + let python_ast = parse_tokens(tokens, Mode::Module, "") + .with_context(|| "Syntax error in input")?; + + let formatted = format_node(&python_ast, &comment_ranges, input)?; + if cli.print_ir { + println!("{}", formatted.document().display(SourceCode::new(input))); + } + if cli.print_comments { + println!( + "{:?}", + formatted.context().comments().debug(SourceCode::new(input)) + ); + } + Ok(formatted + .print() + .with_context(|| "Failed to print the formatter IR")? + .as_code() + .to_string()) } diff --git a/crates/ruff_python_formatter/src/main.rs b/crates/ruff_python_formatter/src/main.rs index 19178aa6ac..63b789888c 100644 --- a/crates/ruff_python_formatter/src/main.rs +++ b/crates/ruff_python_formatter/src/main.rs @@ -1,15 +1,53 @@ -use std::fs; +use std::io::{stdout, Read, Write}; +use std::{fs, io}; -use anyhow::Result; +use anyhow::{bail, Context, Result}; use clap::Parser as ClapParser; -use ruff_python_formatter::cli::Cli; -use ruff_python_formatter::format_module; +use ruff_python_formatter::cli::{format_and_debug_print, Cli, Emit}; + +/// Read a `String` from `stdin`. +pub(crate) fn read_from_stdin() -> Result { + let mut buffer = String::new(); + io::stdin().lock().read_to_string(&mut buffer)?; + Ok(buffer) +} #[allow(clippy::print_stdout)] fn main() -> Result<()> { - let cli = Cli::parse(); - let contents = fs::read_to_string(cli.file)?; - println!("{}", format_module(&contents)?.as_code()); + let cli: Cli = Cli::parse(); + + if cli.files.is_empty() { + if !matches!(cli.emit, None | Some(Emit::Stdout)) { + bail!( + "Can only write to stdout when formatting from stdin, but you asked for {:?}", + cli.emit + ); + } + let input = read_from_stdin()?; + let formatted = format_and_debug_print(&input, &cli)?; + if cli.check { + if formatted == input { + return Ok(()); + } + bail!("Content not correctly formatted") + } + stdout().lock().write_all(formatted.as_bytes())?; + } else { + for file in &cli.files { + let input = fs::read_to_string(file) + .with_context(|| format!("Could not read {}: ", file.display()))?; + let formatted = format_and_debug_print(&input, &cli)?; + match cli.emit { + Some(Emit::Stdout) => stdout().lock().write_all(formatted.as_bytes())?, + None | Some(Emit::Files) => { + fs::write(file, formatted.as_bytes()).with_context(|| { + format!("Could not write to {}, exiting", file.display()) + })?; + } + } + } + } + Ok(()) } From c65f47d7c4a787c3a325cf0e27a77a4f96e88951 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 5 Jun 2023 10:24:00 +0200 Subject: [PATCH 21/41] Format `while` Statement (#4810) --- .../test/fixtures/ruff/statement/while.py | 30 +++++ crates/ruff_python_formatter/src/builders.rs | 14 ++- .../src/comments/format.rs | 108 ++++++++++++++---- .../ruff_python_formatter/src/comments/mod.rs | 9 +- .../src/comments/placement.rs | 88 +++++++++++++- ...formatter__comments__tests__base_test.snap | 10 +- crates/ruff_python_formatter/src/context.rs | 10 +- .../src/expression/maybe_parenthesize.rs | 53 +++++++++ .../src/expression/mod.rs | 1 + crates/ruff_python_formatter/src/lib.rs | 68 +++++++++-- ...tter__tests__black_test__comments5_py.snap | 19 +-- ...ter__tests__black_test__expression_py.snap | 24 ++-- ...atter__tests__black_test__fmtskip8_py.snap | 23 ++-- ...move_newline_after_code_block_open_py.snap | 91 +++++++-------- ...formatter__tests__ruff_test__while_py.snap | 70 ++++++++++++ .../src/statement/stmt_while.rs | 64 ++++++++++- .../src/statement/suite.rs | 17 ++- crates/ruff_python_formatter/src/trivia.rs | 1 - 18 files changed, 555 insertions(+), 145 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/while.py create mode 100644 crates/ruff_python_formatter/src/expression/maybe_parenthesize.rs create mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__while_py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/while.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/while.py new file mode 100644 index 0000000000..79c0d09c8c --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/while.py @@ -0,0 +1,30 @@ +while 34: # trailing test comment + pass # trailing last statement comment + + # trailing while body comment + +# leading else comment + +else: # trailing else comment + pass + + # trailing else body comment + + +while aVeryLongConditionThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn: # trailing comment + pass + +else: + ... + +while ( + some_condition(unformatted, args) and anotherCondition or aThirdCondition +): # comment + print("Do something") + + +while ( + some_condition(unformatted, args) # trailing some condition + and anotherCondition or aThirdCondition # trailing third condition +): # comment + print("Do something") diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 62d7317f36..45a9ee248e 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -11,7 +11,7 @@ pub(crate) trait PyFormatterExtensions<'ast, 'buf> { /// empty lines between any two nodes. Separates any two nodes by at least a hard line break. /// /// * [`NodeLevel::Module`]: Up to two empty lines - /// * [`NodeLevel::Statement`]: Up to one empty line + /// * [`NodeLevel::CompoundStatement`]: Up to one empty line /// * [`NodeLevel::Parenthesized`]: No empty lines fn join_nodes<'fmt>(&'fmt mut self, level: NodeLevel) -> JoinNodesBuilder<'fmt, 'ast, 'buf>; } @@ -53,10 +53,12 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> { 2 => empty_line().fmt(f), _ => write!(f, [empty_line(), empty_line()]), }, - NodeLevel::Statement => match lines_before(f.context().contents(), node.start()) { - 0 | 1 => hard_line_break().fmt(f), - _ => empty_line().fmt(f), - }, + NodeLevel::CompoundStatement => { + match lines_before(f.context().contents(), node.start()) { + 0 | 1 => hard_line_break().fmt(f), + _ => empty_line().fmt(f), + } + } NodeLevel::Parenthesized => hard_line_break().fmt(f), }); @@ -180,7 +182,7 @@ no_leading_newline = 30"# // Should keep at most one empty level #[test] fn ranged_builder_statement_level() { - let printed = format_ranged(NodeLevel::Statement); + let printed = format_ranged(NodeLevel::CompoundStatement); assert_eq!( &printed, diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index 1eb2f44184..286905ce85 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -6,27 +6,37 @@ use ruff_formatter::{format_args, write, FormatError, SourceCode}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::prelude::AstNode; use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_parser::ast::Ranged; /// Formats the leading comments of a node. -pub(crate) fn leading_comments(node: &T) -> FormatLeadingComments +pub(crate) fn leading_node_comments(node: &T) -> FormatLeadingComments where T: AstNode, { - FormatLeadingComments { - node: node.as_any_node_ref(), - } + FormatLeadingComments::Node(node.as_any_node_ref()) +} + +/// Formats the passed comments as leading comments +pub(crate) const fn leading_comments(comments: &[SourceComment]) -> FormatLeadingComments { + FormatLeadingComments::Comments(comments) } #[derive(Copy, Clone, Debug)] -pub(crate) struct FormatLeadingComments<'a> { - node: AnyNodeRef<'a>, +pub(crate) enum FormatLeadingComments<'a> { + Node(AnyNodeRef<'a>), + Comments(&'a [SourceComment]), } impl Format> for FormatLeadingComments<'_> { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { let comments = f.context().comments().clone(); - for comment in comments.leading_comments(self.node) { + let leading_comments = match self { + FormatLeadingComments::Node(node) => comments.leading_comments(*node), + FormatLeadingComments::Comments(comments) => comments, + }; + + for comment in leading_comments { let slice = comment.slice(); let lines_after_comment = lines_after(f.context().contents(), slice.end()); @@ -42,32 +52,88 @@ impl Format> for FormatLeadingComments<'_> { } } -/// Formats the trailing comments of `node` -pub(crate) fn trailing_comments(node: &T) -> FormatTrailingComments +/// Formats the leading `comments` of an alternate branch and ensures that it preserves the right +/// number of empty lines before. The `last_node` is the last node of the preceding body. +/// +/// For example, `last_node` is the last statement in the if body when formatting the leading +/// comments of the `else` branch. +pub(crate) fn leading_alternate_branch_comments<'a, T>( + comments: &'a [SourceComment], + last_node: Option, +) -> FormatLeadingAlternateBranchComments<'a> where - T: AstNode, + T: Into>, { - FormatTrailingComments { - node: node.as_any_node_ref(), + FormatLeadingAlternateBranchComments { + comments, + last_node: last_node.map(std::convert::Into::into), } } -pub(crate) struct FormatTrailingComments<'a> { - node: AnyNodeRef<'a>, +pub(crate) struct FormatLeadingAlternateBranchComments<'a> { + comments: &'a [SourceComment], + last_node: Option>, +} + +impl Format> for FormatLeadingAlternateBranchComments<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + if let Some(first_leading) = self.comments.first() { + // Leading comments only preserves the lines after the comment but not before. + // Insert the necessary lines. + if lines_before(f.context().contents(), first_leading.slice().start()) > 1 { + write!(f, [empty_line()])?; + } + + write!(f, [leading_comments(self.comments)])?; + } else if let Some(last_preceding) = self.last_node { + // The leading comments formatting ensures that it preserves the right amount of lines after + // We need to take care of this ourselves, if there's no leading `else` comment. + if lines_after(f.context().contents(), last_preceding.end()) > 1 { + write!(f, [empty_line()])?; + } + } + + Ok(()) + } +} + +/// Formats the trailing comments of `node` +pub(crate) fn trailing_node_comments(node: &T) -> FormatTrailingComments +where + T: AstNode, +{ + FormatTrailingComments::Node(node.as_any_node_ref()) +} + +/// Formats the passed comments as trailing comments +pub(crate) fn trailing_comments(comments: &[SourceComment]) -> FormatTrailingComments { + FormatTrailingComments::Comments(comments) +} + +pub(crate) enum FormatTrailingComments<'a> { + Node(AnyNodeRef<'a>), + Comments(&'a [SourceComment]), } impl Format> for FormatTrailingComments<'_> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { let comments = f.context().comments().clone(); - let mut has_empty_lines_before = false; - for trailing in comments.trailing_comments(self.node) { + let trailing_comments = match self { + FormatTrailingComments::Node(node) => comments.trailing_comments(*node), + FormatTrailingComments::Comments(comments) => comments, + }; + + let mut has_trailing_own_line_comment = false; + + for trailing in trailing_comments { let slice = trailing.slice(); - let lines_before_comment = lines_before(f.context().contents(), slice.start()); - has_empty_lines_before |= lines_before_comment > 0; + has_trailing_own_line_comment |= trailing.position().is_own_line(); + + if has_trailing_own_line_comment { + let lines_before_comment = lines_before(f.context().contents(), slice.start()); - if has_empty_lines_before { // A trailing comment at the end of a body or list // ```python // def test(): @@ -105,7 +171,7 @@ impl Format> for FormatTrailingComments<'_> { } /// Formats the dangling comments of `node`. -pub(crate) fn dangling_comments(node: &T) -> FormatDanglingComments +pub(crate) fn dangling_node_comments(node: &T) -> FormatDanglingComments where T: AstNode, { @@ -229,7 +295,7 @@ impl Format> for FormatEmptyLines { _ => write!(f, [empty_line(), empty_line()]), }, - NodeLevel::Statement => match self.lines { + NodeLevel::CompoundStatement => match self.lines { 0 | 1 => write!(f, [hard_line_break()]), _ => write!(f, [empty_line()]), }, diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index 57206e3fc4..9476ccda45 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -103,7 +103,10 @@ use crate::comments::debug::{DebugComment, DebugComments}; use crate::comments::map::MultiMap; use crate::comments::node_key::NodeRefEqualityKey; use crate::comments::visitor::CommentsVisitor; -pub(crate) use format::{dangling_comments, leading_comments, trailing_comments}; +pub(crate) use format::{ + dangling_node_comments, leading_alternate_branch_comments, leading_node_comments, + trailing_comments, trailing_node_comments, +}; use ruff_formatter::{SourceCode, SourceCodeSlice}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::source_code::CommentRanges; @@ -121,8 +124,6 @@ pub(crate) struct SourceComment { position: CommentTextPosition, } -#[allow(unused)] -// TODO(micha): Remove after using the new comments infrastructure in the formatter. impl SourceComment { /// Returns the location of the comment in the original source code. /// Allows retrieving the text of the comment. @@ -184,8 +185,6 @@ pub(crate) enum CommentTextPosition { OwnLine, } -#[allow(unused)] -// TODO(micha): Remove after using the new comments infrastructure in the formatter. impl CommentTextPosition { pub(crate) const fn is_own_line(self) -> bool { matches!(self, CommentTextPosition::OwnLine) diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index d4b007e775..062cd17a8d 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -5,7 +5,7 @@ use crate::trivia::find_first_non_trivia_character_in_range; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::source_code::Locator; use ruff_python_ast::whitespace; -use ruff_text_size::{TextRange, TextSize}; +use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser::ast::Ranged; use std::cmp::Ordering; @@ -20,6 +20,7 @@ pub(super) fn place_comment<'a>( .or_else(|comment| handle_in_between_bodies_end_of_line_comment(comment, locator)) .or_else(|comment| handle_trailing_body_comment(comment, locator)) .or_else(handle_trailing_end_of_line_body_comment) + .or_else(|comment| handle_trailing_end_of_line_condition_comment(comment, locator)) .or_else(|comment| handle_positional_only_arguments_separator_comment(comment, locator)) .or_else(|comment| { handle_trailing_binary_expression_left_or_operator_comment(comment, locator) @@ -471,6 +472,91 @@ fn handle_trailing_end_of_line_body_comment(comment: DecoratedComment<'_>) -> Co } } +/// Handles end of line comments after the `:` of a condition +/// +/// ```python +/// while True: # comment +/// pass +/// ``` +/// +/// It attaches the comment as dangling comment to the enclosing `while` statement. +fn handle_trailing_end_of_line_condition_comment<'a>( + comment: DecoratedComment<'a>, + locator: &Locator, +) -> CommentPlacement<'a> { + use ruff_python_ast::prelude::*; + + // Must be an end of line comment + if comment.text_position().is_own_line() { + return CommentPlacement::Default(comment); + } + + // Must be between the condition expression and the first body element + let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) else { + return CommentPlacement::Default(comment); + }; + + let expression_before_colon = match comment.enclosing_node() { + AnyNodeRef::StmtIf(StmtIf { test: expr, .. }) + | AnyNodeRef::StmtWhile(StmtWhile { test: expr, .. }) + | AnyNodeRef::StmtFor(StmtFor { iter: expr, .. }) + | AnyNodeRef::StmtAsyncFor(StmtAsyncFor { iter: expr, .. }) => { + Some(AnyNodeRef::from(expr.as_ref())) + } + + AnyNodeRef::StmtWith(StmtWith { items, .. }) + | AnyNodeRef::StmtAsyncWith(StmtAsyncWith { items, .. }) => { + items.last().map(AnyNodeRef::from) + } + _ => None, + }; + + let Some(last_before_colon) = expression_before_colon else { + return CommentPlacement::Default(comment); + }; + + // If the preceding is the node before the `colon` + // `while true:` The node before the `colon` is the `true` constant. + if preceding.ptr_eq(last_before_colon) { + let mut start = preceding.end(); + while let Some((offset, c)) = find_first_non_trivia_character_in_range( + locator.contents(), + TextRange::new(start, following.start()), + ) { + match c { + ':' => { + if comment.slice().start() > offset { + // Comment comes after the colon + // ```python + // while a: # comment + // ... + // ``` + return CommentPlacement::dangling(comment.enclosing_node(), comment); + } + + // Comment comes before the colon + // ```python + // while ( + // a # comment + // ): + // ... + // ``` + break; + } + ')' => { + // Skip over any closing parentheses + start = offset + ')'.text_len(); + } + _ => { + unreachable!("Only ')' or ':' should follow the condition") + } + } + } + } + + CommentPlacement::Default(comment) +} + /// Attaches comments for the positional-only arguments separator `/` as trailing comments to the /// enclosing [`Arguments`] node. /// diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__base_test.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__base_test.snap index 51d2b14f09..ef24b1dbef 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__base_test.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__base_test.snap @@ -19,19 +19,19 @@ expression: comments.debug(test_case.source_code) "trailing": [], }, Node { - kind: ExprCompare, - range: 51..57, - source: `x == y`, + kind: StmtIf, + range: 48..212, + source: `if x == y: # if statement e...ne comment⏎`, }: { "leading": [], - "dangling": [], - "trailing": [ + "dangling": [ SourceComment { text: "# if statement end of line comment", position: EndOfLine, formatted: false, }, ], + "trailing": [], }, Node { kind: StmtIf, diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index 21d03fec72..bef99c4514 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -25,7 +25,6 @@ impl<'a> PyFormatContext<'a> { } } - #[allow(unused)] pub(crate) fn contents(&self) -> &'a str { self.contents } @@ -35,7 +34,6 @@ impl<'a> PyFormatContext<'a> { Locator::new(self.contents) } - #[allow(unused)] pub(crate) fn set_node_level(&mut self, level: NodeLevel) { self.node_level = level; } @@ -44,7 +42,6 @@ impl<'a> PyFormatContext<'a> { self.node_level } - #[allow(unused)] pub(crate) fn comments(&self) -> &Comments<'a> { &self.comments } @@ -80,11 +77,10 @@ pub(crate) enum NodeLevel { #[default] TopLevel, - /// Formatting nodes that are enclosed by a statement. - #[allow(unused)] - Statement, + /// Formatting the body statements of a [compound statement](https://docs.python.org/3/reference/compound_stmts.html#compound-statements) + /// (`if`, `while`, `match`, etc.). + CompoundStatement, /// Formatting nodes that are enclosed in a parenthesized expression. - #[allow(unused)] Parenthesized, } diff --git a/crates/ruff_python_formatter/src/expression/maybe_parenthesize.rs b/crates/ruff_python_formatter/src/expression/maybe_parenthesize.rs new file mode 100644 index 0000000000..aa7b4f5702 --- /dev/null +++ b/crates/ruff_python_formatter/src/expression/maybe_parenthesize.rs @@ -0,0 +1,53 @@ +use crate::context::NodeLevel; +use crate::prelude::*; +use ruff_formatter::{format_args, write}; +use rustpython_parser::ast::Expr; + +/// Formats the passed expression. Adds parentheses if the expression doesn't fit on a line. +pub(crate) const fn maybe_parenthesize(expression: &Expr) -> MaybeParenthesize { + MaybeParenthesize { expression } +} + +pub(crate) struct MaybeParenthesize<'a> { + expression: &'a Expr, +} + +impl Format> for MaybeParenthesize<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let saved_level = f.context().node_level(); + f.context_mut().set_node_level(NodeLevel::Parenthesized); + + let result = if needs_parentheses(self.expression) { + write!( + f, + [group(&format_args![ + if_group_breaks(&text("(")), + soft_block_indent(&self.expression.format()), + if_group_breaks(&text(")")) + ])] + ) + } else { + // Don't add parentheses around expressions that have parentheses on their own (e.g. list, dict, tuple, call expression) + self.expression.format().fmt(f) + }; + + f.context_mut().set_node_level(saved_level); + + result + } +} + +const fn needs_parentheses(expr: &Expr) -> bool { + !matches!( + expr, + Expr::Tuple(_) + | Expr::List(_) + | Expr::Set(_) + | Expr::Dict(_) + | Expr::ListComp(_) + | Expr::SetComp(_) + | Expr::DictComp(_) + | Expr::GeneratorExp(_) + | Expr::Call(_) + ) +} diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 362b1daa86..039c104d55 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -29,6 +29,7 @@ pub(crate) mod expr_tuple; pub(crate) mod expr_unary_op; pub(crate) mod expr_yield; pub(crate) mod expr_yield_from; +pub(crate) mod maybe_parenthesize; #[derive(Default)] pub struct FormatExpr; diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 49cc9fadc9..dcbf68acc9 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -14,7 +14,9 @@ use ruff_formatter::{ use ruff_python_ast::node::AstNode; use ruff_python_ast::source_code::{CommentRanges, CommentRangesBuilder, Locator}; -use crate::comments::{dangling_comments, leading_comments, trailing_comments, Comments}; +use crate::comments::{ + dangling_node_comments, leading_node_comments, trailing_node_comments, Comments, +}; use crate::context::PyFormatContext; pub(crate) mod builders; @@ -64,7 +66,7 @@ where /// You may want to override this method if you want to manually handle the formatting of comments /// inside of the `fmt_fields` method or customize the formatting of the leading comments. fn fmt_leading_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> { - leading_comments(node).fmt(f) + leading_node_comments(node).fmt(f) } /// Formats the [dangling comments](comments#dangling-comments) of the node. @@ -75,7 +77,7 @@ where /// /// A node can have dangling comments if all its children are tokens or if all node childrens are optional. fn fmt_dangling_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> { - dangling_comments(node).fmt(f) + dangling_node_comments(node).fmt(f) } /// Formats the [trailing comments](comments#trailing-comments) of the node. @@ -83,7 +85,7 @@ where /// You may want to override this method if you want to manually handle the formatting of comments /// inside of the `fmt_fields` method or customize the formatting of the trailing comments. fn fmt_trailing_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> { - trailing_comments(node).fmt(f) + trailing_node_comments(node).fmt(f) } } @@ -285,14 +287,58 @@ Formatted twice: Ok(()) } + #[fixture(pattern = "resources/test/fixtures/ruff/**/*.py")] + #[test] + fn ruff_test(input_path: &Path) -> Result<()> { + let content = fs::read_to_string(input_path)?; + + let printed = format_module(&content)?; + let formatted_code = printed.as_code(); + + let reformatted = + format_module(formatted_code).expect("Expected formatted code to be valid syntax"); + + if reformatted.as_code() != formatted_code { + let diff = TextDiff::from_lines(formatted_code, reformatted.as_code()) + .unified_diff() + .header("Formatted once", "Formatted twice") + .to_string(); + panic!( + r#"Reformatting the formatted code a second time resulted in formatting changes. +{diff} + +Formatted once: +{formatted_code} + +Formatted twice: +{}"#, + reformatted.as_code() + ); + } + + let snapshot = format!( + r#"## Input +{} + +## Output +{}"#, + CodeFrame::new("py", &content), + CodeFrame::new("py", formatted_code) + ); + assert_snapshot!(snapshot); + + Ok(()) + } + /// Use this test to debug the formatting of some snipped #[ignore] #[test] fn quick_test() { let src = r#" -{ - k: v for k, v in a_very_long_variable_name_that_exceeds_the_line_length_by_far_keep_going -} +while True: + if something.changed: + do.stuff() # trailing comment +other "#; // Tokenize once let mut tokens = Vec::new(); @@ -320,10 +366,10 @@ Formatted twice: assert_eq!( printed.as_code(), - r#"{ - k: v - for k, v in a_very_long_variable_name_that_exceeds_the_line_length_by_far_keep_going -}"# + r#"while True: + if something.changed: + do.stuff() # trailing comment +"# ); } diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap index 7087a8c711..dcace90ac2 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap @@ -86,20 +86,20 @@ if __name__ == "__main__": ```diff --- Black +++ Ruff -@@ -1,11 +1,6 @@ +@@ -1,11 +1,9 @@ while True: if something.changed: - do.stuff() # trailing comment - # Comment belongs to the `if` block. -- # This one belongs to the `while` block. -- -- # Should this one, too? I guess so. -- + do.stuff() + # This one belongs to the `while` block. + + # Should this one, too? I guess so. +- # This one is properly standalone now. for i in range(100): -@@ -15,27 +10,18 @@ +@@ -15,27 +13,18 @@ # then we do this print(i) @@ -127,7 +127,7 @@ if __name__ == "__main__": # SECTION COMMENT -@@ -47,8 +33,6 @@ +@@ -47,8 +36,6 @@ @deco3 def decorated1(): ... @@ -136,7 +136,7 @@ if __name__ == "__main__": # leading 1 @deco1 # leading 2 -@@ -56,18 +40,12 @@ +@@ -56,18 +43,12 @@ # leading function comment def decorated1(): ... @@ -163,6 +163,9 @@ if __name__ == "__main__": while True: if something.changed: do.stuff() + # This one belongs to the `while` block. + + # Should this one, too? I guess so. # This one is properly standalone now. for i in range(100): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap index cf47dd1cbe..8dbc397d78 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap @@ -511,7 +511,7 @@ last_call() Ø = set() authors.łukasz.say_thanks() mapping = { -@@ -233,138 +170,83 @@ +@@ -233,138 +170,84 @@ C: 0.1 * (10.0 / 12), D: 0.1 * (10.0 / 12), } @@ -550,15 +550,6 @@ last_call() - ... -for j in 1 + (2 + 3): - ... --while this and that: -- ... --for ( -- addr_family, -- addr_type, -- addr_proto, -- addr_canonname, -- addr_sockaddr, --) in socket.getaddrinfo("google.com", "http"): +print(* lambda x: x) +assert(not Test),("Short message") +assert this is ComplexTest and not requirements.fit_in_a_single_line(force=False), "Short message" @@ -568,7 +559,15 @@ last_call() +for z in (i for i in (1, 2, 3)): ... +for i in (call()): ... +for j in (1 + (2 + 3)): ... -+while(this and that): ... + while this and that: + ... +-for ( +- addr_family, +- addr_type, +- addr_proto, +- addr_canonname, +- addr_sockaddr, +-) in socket.getaddrinfo("google.com", "http"): +for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'): pass -a = ( @@ -879,7 +878,8 @@ for y in (): ... for z in (i for i in (1, 2, 3)): ... for i in (call()): ... for j in (1 + (2 + 3)): ... -while(this and that): ... +while this and that: + ... for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'): pass a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap index 358018b4e0..0c9e4e258b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap @@ -100,20 +100,24 @@ async def test_async_with(): # Make sure a leading comment is not removed. if unformatted_call( args ): # fmt: skip print("First branch") -@@ -30,33 +23,21 @@ +@@ -29,34 +22,22 @@ + elif another_unformatted_call( args ): # fmt: skip print("Second branch") else : # fmt: skip - print("Last branch") +- print("Last branch") - - - while some_condition( unformatted, args ): # fmt: skip +-while some_condition( unformatted, args ): # fmt: skip ++ print("Last branch") # fmt: skip ++while some_condition( unformatted, args ): # fmt: skip print("Do something") - - for i in some_iter( unformatted, args ): # fmt: skip - print("Do something") +- print("Do something") - - ++ print("Do something") # fmt: skip async def test_async_for(): async for i in some_async_iter( unformatted, args ): # fmt: skip print("Do something") @@ -128,9 +132,10 @@ async def test_async_with(): - - with give_me_context( unformatted, args ): # fmt: skip - print("Do something") +- print("Do something") - - ++ print("Do something") # fmt: skip async def test_async_with(): async with give_me_async_context( unformatted, args ): # fmt: skip print("Do something") @@ -163,11 +168,11 @@ if unformatted_call( args ): # fmt: skip elif another_unformatted_call( args ): # fmt: skip print("Second branch") else : # fmt: skip - print("Last branch") -while some_condition( unformatted, args ): # fmt: skip + print("Last branch") # fmt: skip +while some_condition( unformatted, args ): # fmt: skip print("Do something") for i in some_iter( unformatted, args ): # fmt: skip - print("Do something") + print("Do something") # fmt: skip async def test_async_for(): async for i in some_async_iter( unformatted, args ): # fmt: skip print("Do something") @@ -178,7 +183,7 @@ except UnformattedError as ex: # fmt: skip finally : # fmt: skip finally_call() with give_me_context( unformatted, args ): # fmt: skip - print("Do something") + print("Do something") # fmt: skip async def test_async_with(): async with give_me_async_context( unformatted, args ): # fmt: skip print("Do something") diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap index bb3d5adbec..fcfe41d5a6 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap @@ -121,30 +121,30 @@ with open("/path/to/file.txt", mode="r") as read_file: ```diff --- Black +++ Ruff -@@ -1,78 +1,74 @@ +@@ -1,78 +1,68 @@ import random -- -- - def foo1(): -- print("The newline above me should be deleted!") -- ++def foo1(): + print("The newline above me should be deleted!") - def foo2(): -- print("All the newlines above me should be deleted!") ++def foo2(): + +-def foo1(): +- print("The newline above me should be deleted!") -+ -+ print("All the newlines above me should be deleted!") +-def foo2(): + print("All the newlines above me should be deleted!") +- +- def foo3(): + print("No newline above me!") print("There is a newline above me, and that's OK!") -+def foo4(): - - --def foo4(): +- + def foo4(): ++ # There is a comment here print("The newline above me should not be deleted!") @@ -154,23 +154,23 @@ with open("/path/to/file.txt", mode="r") as read_file: def bar(self): + print("The newline above me should be deleted!") -- -- - for i in range(5): -- print(f"{i}) The line above me should be removed!") -- ++for i in range(5): + print(f"{i}) The line above me should be removed!") ++for i in range(5): + +-for i in range(5): +- print(f"{i}) The line above me should be removed!") + + ++ print(f"{i}) The lines above me should be removed!") for i in range(5): - print(f"{i}) The lines above me should be removed!") ++ for j in range(7): -+ -+ print(f"{i}) The lines above me should be removed!") - for i in range(5): -+ - for j in range(7): -+ +-for i in range(5): +- for j in range(7): print(f"{i}) The lines above me should be removed!") +if random.randint(0, 3) == 0: @@ -189,38 +189,33 @@ with open("/path/to/file.txt", mode="r") as read_file: if random.uniform(0, 1) > 0.5: print("Two lines above me are about to be removed!") - -+while True: - -+ print("The newline above me should be deleted!") +- + while True: + print("The newline above me should be deleted!") +- +- while True: -- print("The newline above me should be deleted!") - - --while True: -+ print("The newlines above me should be deleted!") -+while True: - - --while True: +- + while True: while False: -- print("The newlines above me should be deleted!") -- + print("The newlines above me should be deleted!") ++with open("/path/to/file.txt", mode="w") as file: -+ print("The newlines above me should be deleted!") - with open("/path/to/file.txt", mode="w") as file: -- file.write("The new line above me is about to be removed!") - - + file.write("The new line above me is about to be removed!") with open("/path/to/file.txt", mode="w") as file: -- file.write("The new lines above me is about to be removed!") +- file.write("The new line above me is about to be removed!") +-with open("/path/to/file.txt", mode="w") as file: + -+ file.write("The new lines above me is about to be removed!") - with open("/path/to/file.txt", mode="r") as read_file: -+ + file.write("The new lines above me is about to be removed!") ++with open("/path/to/file.txt", mode="r") as read_file: + +- +-with open("/path/to/file.txt", mode="r") as read_file: with open("/path/to/output_file.txt", mode="w") as write_file: + write_file.writelines(read_file.readlines()) @@ -278,17 +273,11 @@ if random.randint(0, 3) == 0: if random.uniform(0, 1) > 0.5: print("Two lines above me are about to be removed!") while True: - print("The newline above me should be deleted!") while True: - - - print("The newlines above me should be deleted!") while True: - while False: - print("The newlines above me should be deleted!") with open("/path/to/file.txt", mode="w") as file: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__while_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__while_py.snap new file mode 100644 index 0000000000..03203dfbc6 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__while_py.snap @@ -0,0 +1,70 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +--- +## Input +```py +while 34: # trailing test comment + pass # trailing last statement comment + + # trailing while body comment + +# leading else comment + +else: # trailing else comment + pass + + # trailing else body comment + + +while aVeryLongConditionThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn: # trailing comment + pass + +else: + ... + +while ( + some_condition(unformatted, args) and anotherCondition or aThirdCondition +): # comment + print("Do something") + + +while ( + some_condition(unformatted, args) # trailing some condition + and anotherCondition or aThirdCondition # trailing third condition +): # comment + print("Do something") +``` + + + +## Output +```py +while 34: # trailing test comment + pass # trailing last statement comment + + # trailing while body comment + +# leading else comment + +else: # trailing else comment + pass + + # trailing else body comment +while ( + aVeryLongConditionThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn +): # trailing comment + pass + +else: + ... +while some_condition(unformatted, args) and anotherCondition or aThirdCondition: # comment + print("Do something") +while ( + some_condition(unformatted, args) # trailing some condition + and anotherCondition or aThirdCondition # trailing third condition +): # comment + print("Do something") +``` + + diff --git a/crates/ruff_python_formatter/src/statement/stmt_while.rs b/crates/ruff_python_formatter/src/statement/stmt_while.rs index c11cd9cbf4..7ff55a21de 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_while.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_while.rs @@ -1,12 +1,68 @@ -use crate::{verbatim_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::StmtWhile; +use crate::comments::{leading_alternate_branch_comments, trailing_comments}; +use crate::expression::maybe_parenthesize::maybe_parenthesize; +use crate::prelude::*; +use crate::FormatNodeRule; +use ruff_formatter::write; +use ruff_python_ast::node::AstNode; +use rustpython_parser::ast::{Ranged, Stmt, StmtWhile}; #[derive(Default)] pub struct FormatStmtWhile; impl FormatNodeRule for FormatStmtWhile { fn fmt_fields(&self, item: &StmtWhile, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [verbatim_text(item.range)]) + let StmtWhile { + range: _, + test, + body, + orelse, + } = item; + + let comments = f.context().comments().clone(); + let dangling_comments = comments.dangling_comments(item.as_any_node_ref()); + + let body_start = body.first().map_or(test.end(), Stmt::start); + let or_else_comments_start = + dangling_comments.partition_point(|comment| comment.slice().end() < body_start); + + let (trailing_condition_comments, or_else_comments) = + dangling_comments.split_at(or_else_comments_start); + + write!( + f, + [ + text("while"), + space(), + maybe_parenthesize(test), + text(":"), + trailing_comments(trailing_condition_comments), + block_indent(&body.format()) + ] + )?; + + if !orelse.is_empty() { + // Split between leading comments before the `else` keyword and end of line comments at the end of + // the `else:` line. + let trailing_start = + or_else_comments.partition_point(|comment| comment.position().is_own_line()); + let (leading, trailing) = or_else_comments.split_at(trailing_start); + + write!( + f, + [ + leading_alternate_branch_comments(leading, body.last()), + text("else:"), + trailing_comments(trailing), + block_indent(&orelse.format()) + ] + )?; + } + + Ok(()) + } + + fn fmt_dangling_comments(&self, _node: &StmtWhile, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled in `fmt_fields` + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 86c7654c71..7f53ec0da3 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -28,10 +28,15 @@ impl Default for FormatSuite { impl FormatRule> for FormatSuite { fn fmt(&self, statements: &Suite, f: &mut PyFormatter) -> FormatResult<()> { - let mut joiner = f.join_nodes(match self.level { + let node_level = match self.level { SuiteLevel::TopLevel => NodeLevel::TopLevel, - SuiteLevel::Nested => NodeLevel::Statement, - }); + SuiteLevel::Nested => NodeLevel::CompoundStatement, + }; + + let saved_level = f.context().node_level(); + f.context_mut().set_node_level(node_level); + + let mut joiner = f.join_nodes(node_level); let mut iter = statements.iter(); let Some(first) = iter.next() else { @@ -67,7 +72,11 @@ impl FormatRule> for FormatSuite { is_last_function_or_class_definition = is_current_function_or_class_definition; } - joiner.finish() + let result = joiner.finish(); + + f.context_mut().set_node_level(saved_level); + + result } } diff --git a/crates/ruff_python_formatter/src/trivia.rs b/crates/ruff_python_formatter/src/trivia.rs index db99c85198..e1224883b0 100644 --- a/crates/ruff_python_formatter/src/trivia.rs +++ b/crates/ruff_python_formatter/src/trivia.rs @@ -41,7 +41,6 @@ pub(crate) fn find_first_non_trivia_character_in_range( } /// Returns the number of newlines between `offset` and the first non whitespace character in the source code. -#[allow(unused)] // TODO(micha) Remove after using for statements. pub(crate) fn lines_before(code: &str, offset: TextSize) -> u32 { let head = &code[TextRange::up_to(offset)]; let mut newlines = 0u32; From ff37d7af23dccce2e56bbc4efa2025f21d60ce84 Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 5 Jun 2023 10:35:05 +0200 Subject: [PATCH 22/41] Implement module formatting using JoinNodesBuilder (#4808) * Implement module formatting using JoinNodesBuilder This uses JoinNodesBuilder to implement module formatting for #4800 See the snapshots for the changed behaviour. See one PR up for a CLI that i used to verify the trailing new line behaviour --- .../src/module/mod_module.rs | 17 +- ...ttribute_access_on_number_literals_py.snap | 11 +- ...lack_test__class_blank_parentheses_py.snap | 34 +- ...black_test__class_methods_new_line_py.snap | 127 ++++--- ...er__tests__black_test__collections_py.snap | 26 +- ...est__comment_after_escaped_newline_py.snap | 8 +- ...tter__tests__black_test__comments2_py.snap | 47 +-- ...tter__tests__black_test__comments4_py.snap | 338 ------------------ ...tter__tests__black_test__comments5_py.snap | 72 ++-- ...tter__tests__black_test__comments6_py.snap | 98 ++--- ...tter__tests__black_test__comments9_py.snap | 174 ++++----- ..._test__comments_non_breaking_space_py.snap | 14 +- ...atter__tests__black_test__comments_py.snap | 54 +-- ...sts__black_test__docstring_preview_py.snap | 64 ++-- ...tter__tests__black_test__docstring_py.snap | 260 ++++++++------ ...er__tests__black_test__empty_lines_py.snap | 23 +- ...ter__tests__black_test__expression_py.snap | 22 +- ...tter__tests__black_test__fmtonoff2_py.snap | 33 +- ...tter__tests__black_test__fmtonoff3_py.snap | 6 +- ...tter__tests__black_test__fmtonoff4_py.snap | 10 +- ...tter__tests__black_test__fmtonoff5_py.snap | 59 ++- ...atter__tests__black_test__fmtonoff_py.snap | 153 +++++--- ...atter__tests__black_test__fmtskip8_py.snap | 78 ++-- ...tter__tests__black_test__function2_py.snap | 27 +- ...atter__tests__black_test__function_py.snap | 107 +++--- ...lack_test__function_trailing_comma_py.snap | 62 ++-- ..._tests__black_test__import_spacing_py.snap | 27 +- ...ests__black_test__power_op_spacing_py.snap | 42 +-- ...s__black_test__remove_await_parens_py.snap | 110 +++--- ...__black_test__remove_except_parens_py.snap | 8 +- ...s__black_test__remove_for_brackets_py.snap | 5 +- ...move_newline_after_code_block_open_py.snap | 143 ++++---- ...__tests__black_test__remove_parens_py.snap | 73 ++-- ...k_test__return_annotation_brackets_py.snap | 143 +++++--- ...ck_test__skip_magic_trailing_comma_py.snap | 17 +- ...rmatter__tests__black_test__slices_py.snap | 36 +- ...tests__black_test__string_prefixes_py.snap | 19 +- ...matter__tests__black_test__torture_py.snap | 32 +- ...t__trailing_comma_optional_parens1_py.snap | 22 +- ...__trailing_commas_in_leading_parts_py.snap | 28 +- ...black_test__tricky_unicode_symbols_py.snap | 61 ---- ...er__tests__black_test__tupleassign_py.snap | 5 +- ...formatter__tests__ruff_test__while_py.snap | 5 + 43 files changed, 1176 insertions(+), 1524 deletions(-) delete mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap delete mode 100644 crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tricky_unicode_symbols_py.snap diff --git a/crates/ruff_python_formatter/src/module/mod_module.rs b/crates/ruff_python_formatter/src/module/mod_module.rs index a2d0370b5c..fe21ed65fa 100644 --- a/crates/ruff_python_formatter/src/module/mod_module.rs +++ b/crates/ruff_python_formatter/src/module/mod_module.rs @@ -1,8 +1,7 @@ -use crate::AsFormat; -use crate::{FormatNodeRule, PyFormatter}; +use crate::statement::suite::SuiteLevel; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::hard_line_break; use ruff_formatter::{write, Buffer, FormatResult}; - use rustpython_parser::ast::ModModule; #[derive(Default)] @@ -10,9 +9,13 @@ pub struct FormatModModule; impl FormatNodeRule for FormatModModule { fn fmt_fields(&self, item: &ModModule, f: &mut PyFormatter) -> FormatResult<()> { - for stmt in &item.body { - write!(f, [stmt.format(), hard_line_break()])?; - } - Ok(()) + write!( + f, + [ + item.body.format().with_options(SuiteLevel::TopLevel), + // Trailing newline at the end of the file + hard_line_break() + ] + ) } } diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap index 2849790d1f..0128d6ed45 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap @@ -35,7 +35,7 @@ y = 100(no) ```diff --- Black +++ Ruff -@@ -1,22 +1,20 @@ +@@ -1,21 +1,21 @@ -x = (123456789).bit_count() +x = 123456789 .bit_count() x = (123456).__abs__() @@ -53,8 +53,6 @@ y = 100(no) -x = 0o777.real -x = (0.000000006).hex() -x = -100.0000j -- --if (10).real: +x = .1.is_integer() +x = 1. .imag +x = 1E+1.imag @@ -69,11 +67,12 @@ y = 100(no) +x = 0O777 .real +x = 0.000000006 .hex() +x = -100.0000J + +-if (10).real: +if 10 .real: ... -- + y = 100[no] - y = 100(no) ``` ## Ruff Output @@ -95,8 +94,10 @@ x = 0B1011 .conjugate() x = 0O777 .real x = 0.000000006 .hex() x = -100.0000J + if 10 .real: ... + y = 100[no] y = 100(no) ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_blank_parentheses_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_blank_parentheses_py.snap index 5e1f3eff02..c40c09ee3a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_blank_parentheses_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_blank_parentheses_py.snap @@ -36,12 +36,12 @@ class NormalClass ( ```diff --- Black +++ Ruff -@@ -1,30 +1,21 @@ +@@ -1,16 +1,16 @@ -class SimpleClassWithBlankParentheses: +class SimpleClassWithBlankParentheses(): pass -- -- + + -class ClassWithSpaceParentheses: +class ClassWithSpaceParentheses ( ): first_test_data = 90 @@ -49,24 +49,22 @@ class NormalClass ( - def test_func(self): return None -- -- + + class ClassWithEmptyFunc(object): + def func_with_blank_parentheses(): return 5 -- -- - def public_func_with_blank_parentheses(): - return None -- -- + +@@ -20,11 +20,12 @@ + + def class_under_the_func_with_blank_parentheses(): - class InsideFunc: + class InsideFunc(): pass -- -- + + -class NormalClass: +class NormalClass ( +): @@ -80,20 +78,30 @@ class NormalClass ( ```py class SimpleClassWithBlankParentheses(): pass + + class ClassWithSpaceParentheses ( ): first_test_data = 90 second_test_data = 100 def test_func(self): return None + + class ClassWithEmptyFunc(object): def func_with_blank_parentheses(): return 5 + + def public_func_with_blank_parentheses(): return None + + def class_under_the_func_with_blank_parentheses(): class InsideFunc(): pass + + class NormalClass ( ): def func_for_testing(self, first, second): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap index 5cdc7dd0c2..c569564e5c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap @@ -113,38 +113,22 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: ```diff --- Black +++ Ruff -@@ -1,165 +1,100 @@ - class ClassSimplest: - pass -- -- - class ClassWithSingleField: - a = 1 -- -- - class ClassWithJustTheDocstring: - """Just a docstring.""" -- -- - class ClassWithInit: - def __init__(self): - pass -- -- +@@ -17,23 +17,19 @@ + class ClassWithTheDocstringAndInit: """Just a docstring.""" - def __init__(self): pass -- -- + + class ClassWithInitAndVars: cls_var = 100 - def __init__(self): pass -- -- + + class ClassWithInitAndVarsAndDocstring: """Test class""" - @@ -152,22 +136,17 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: - def __init__(self): pass -- -- - class ClassWithDecoInit: - @deco - def __init__(self): - pass -- -- + +@@ -46,7 +42,6 @@ + class ClassWithDecoInitAndVars: cls_var = 100 - @deco def __init__(self): pass -- -- +@@ -54,9 +49,7 @@ + class ClassWithDecoInitAndVarsAndDocstring: """Test class""" - @@ -176,43 +155,37 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: @deco def __init__(self): pass -- -- - class ClassSimplestWithInner: - class Inner: - pass -- -- +@@ -70,21 +63,18 @@ class ClassSimplestWithInnerWithDocstring: class Inner: """Just a docstring.""" - def __init__(self): pass -- -- + + class ClassWithSingleFieldWithInner: a = 1 - class Inner: pass -- -- + + class ClassWithJustTheDocstringWithInner: """Just a docstring.""" - class Inner: pass -- -- + +@@ -92,29 +82,23 @@ class ClassWithInitWithInner: class Inner: pass - def __init__(self): pass -- -- + + class ClassWithInitAndVarsWithInner: cls_var = 100 - @@ -221,8 +194,8 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: - def __init__(self): pass -- -- + + class ClassWithInitAndVarsAndDocstringWithInner: """Test class""" - @@ -233,8 +206,8 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: - def __init__(self): pass -- -- + +@@ -122,7 +106,6 @@ class ClassWithDecoInitWithInner: class Inner: pass @@ -242,8 +215,8 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: @deco def __init__(self): pass -- -- +@@ -130,10 +113,8 @@ + class ClassWithDecoInitAndVarsWithInner: cls_var = 100 - @@ -253,8 +226,8 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: @deco def __init__(self): pass -- -- +@@ -141,12 +122,9 @@ + class ClassWithDecoInitAndVarsAndDocstringWithInner: """Test class""" - @@ -266,8 +239,8 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: @deco def __init__(self): pass -- -- +@@ -154,12 +132,9 @@ + class ClassWithDecoInitAndVarsAndDocstringWithInner2: """Test class""" - @@ -286,68 +259,100 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: ```py class ClassSimplest: pass + + class ClassWithSingleField: a = 1 + + class ClassWithJustTheDocstring: """Just a docstring.""" + + class ClassWithInit: def __init__(self): pass + + class ClassWithTheDocstringAndInit: """Just a docstring.""" def __init__(self): pass + + class ClassWithInitAndVars: cls_var = 100 def __init__(self): pass + + class ClassWithInitAndVarsAndDocstring: """Test class""" cls_var = 100 def __init__(self): pass + + class ClassWithDecoInit: @deco def __init__(self): pass + + class ClassWithDecoInitAndVars: cls_var = 100 @deco def __init__(self): pass + + class ClassWithDecoInitAndVarsAndDocstring: """Test class""" cls_var = 100 @deco def __init__(self): pass + + class ClassSimplestWithInner: class Inner: pass + + class ClassSimplestWithInnerWithDocstring: class Inner: """Just a docstring.""" def __init__(self): pass + + class ClassWithSingleFieldWithInner: a = 1 class Inner: pass + + class ClassWithJustTheDocstringWithInner: """Just a docstring.""" class Inner: pass + + class ClassWithInitWithInner: class Inner: pass def __init__(self): pass + + class ClassWithInitAndVarsWithInner: cls_var = 100 class Inner: pass def __init__(self): pass + + class ClassWithInitAndVarsAndDocstringWithInner: """Test class""" cls_var = 100 @@ -355,12 +360,16 @@ class ClassWithInitAndVarsAndDocstringWithInner: pass def __init__(self): pass + + class ClassWithDecoInitWithInner: class Inner: pass @deco def __init__(self): pass + + class ClassWithDecoInitAndVarsWithInner: cls_var = 100 class Inner: @@ -368,6 +377,8 @@ class ClassWithDecoInitAndVarsWithInner: @deco def __init__(self): pass + + class ClassWithDecoInitAndVarsAndDocstringWithInner: """Test class""" cls_var = 100 @@ -376,6 +387,8 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner: @deco def __init__(self): pass + + class ClassWithDecoInitAndVarsAndDocstringWithInner2: """Test class""" class Inner: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap index 5daf501dc3..50f5ab1f05 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap @@ -84,9 +84,9 @@ if True: ```diff --- Black +++ Ruff -@@ -1,99 +1,61 @@ +@@ -1,75 +1,49 @@ import core, time, a -- + from . import A, B, C - # keeps existing trailing comma @@ -104,7 +104,7 @@ if True: from foo import ( xyzzy as magic, ) -- + -a = { - 1, - 2, @@ -162,24 +162,22 @@ if True: pass for (x,) in (1,), (2,), (3,): pass -- + -[ - 1, - 2, - 3, -] -- --division_result_tuple = (6 / 2,) +[1, 2, 3,] + +-division_result_tuple = (6 / 2,) +division_result_tuple = (6/2,) print("foo %r", (foo.bar,)) -- + if True: - IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = ( - Config.IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING - | {pylons.controllers.WSGIController} +@@ -79,21 +53,15 @@ ) -- + if True: - ec2client.get_waiter("instance_stopped").wait( + ec2client.get_waiter('instance_stopped').wait( @@ -210,6 +208,7 @@ if True: ```py import core, time, a + from . import A, B, C # keeps existing trailing comma from foo import ( @@ -224,6 +223,7 @@ from foo import ( from foo import ( xyzzy as magic, ) + a = {1,2,3,} b = { 1,2, @@ -249,14 +249,18 @@ for x in (1,): pass for (x,) in (1,), (2,), (3,): pass + [1, 2, 3,] + division_result_tuple = (6/2,) print("foo %r", (foo.bar,)) + if True: IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = ( Config.IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING | {pylons.controllers.WSGIController} ) + if True: ec2client.get_waiter('instance_stopped').wait( InstanceIds=[instance.id], diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comment_after_escaped_newline_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comment_after_escaped_newline_py.snap index 86d56db433..5633eef26a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comment_after_escaped_newline_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comment_after_escaped_newline_py.snap @@ -22,13 +22,13 @@ def bobtwo(): \ ```diff --- Black +++ Ruff -@@ -1,6 +1,7 @@ +@@ -1,6 +1,9 @@ -def bob(): # pylint: disable=W9016 +def bob(): \ + # pylint: disable=W9016 pass -- -- + + -def bobtwo(): # some comment here +def bobtwo(): \ + \ @@ -42,6 +42,8 @@ def bobtwo(): \ def bob(): \ # pylint: disable=W9016 pass + + def bobtwo(): \ \ # some comment here diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap index 808c55756f..cd228ca42b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap @@ -178,7 +178,7 @@ instruction()#comment with bad spacing ```diff --- Black +++ Ruff -@@ -1,39 +1,38 @@ +@@ -1,39 +1,40 @@ from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent, # NOT DRY + MyLovelyCompanyTeamProjectComponent # NOT DRY @@ -187,7 +187,7 @@ instruction()#comment with bad spacing - MyLovelyCompanyTeamProjectComponent as component, # DRY + MyLovelyCompanyTeamProjectComponent as component # DRY ) -- + # Please keep __all__ alphabetized within each category. __all__ = [ @@ -227,7 +227,7 @@ instruction()#comment with bad spacing + 'NamedTuple', # Not really a type. + 'Generator', ] -- + not_shareables = [ # singletons True, @@ -238,11 +238,10 @@ instruction()#comment with bad spacing # builtin types and objects type, object, -@@ -47,21 +46,21 @@ - Cheese("Wensleydale"), +@@ -48,20 +49,23 @@ SubBytes(b"spam"), ] -- + -if "PYTHON" in os.environ: +if 'PYTHON' in os.environ: add_compiler(compiler_from_env()) @@ -252,8 +251,8 @@ instruction()#comment with bad spacing + # add_compiler(compiler) add_compiler(compilers[(7.0, 32)]) - # add_compiler(compilers[(7.1, 64)]) -- -- + + # Comment before function. def inline_comments_in_brackets_ruin_everything(): if typedargslist: @@ -267,7 +266,7 @@ instruction()#comment with bad spacing children[0], body, children[-1], # type: ignore -@@ -73,49 +72,42 @@ +@@ -73,49 +77,42 @@ parameters.children[-1], # )2 ] parameters.children = [parameters.what_if_this_was_actually_long.children[0], body, parameters.children[-1]] # type: ignore @@ -340,7 +339,7 @@ instruction()#comment with bad spacing ] lcomp2 = [ # hello -@@ -127,7 +119,7 @@ +@@ -127,7 +124,7 @@ ] lcomp3 = [ # This one is actually too long to fit in a single line. @@ -349,7 +348,7 @@ instruction()#comment with bad spacing # yup for element in collection.select_elements() # right -@@ -140,28 +132,20 @@ +@@ -140,25 +137,23 @@ # and round and round we go # and round and round we go @@ -363,8 +362,8 @@ instruction()#comment with bad spacing + Leaf(token.NEWLINE, '\n') # FIXME: \r\n? + ], ) -- -- + + -CONFIG_FILES = ( - [ - CONFIG_FILE, @@ -372,20 +371,17 @@ instruction()#comment with bad spacing - + SHARED_CONFIG_FILES - + USER_CONFIG_FILES -) # type: Final -- -- +CONFIG_FILES = [CONFIG_FILE, ] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final + + class Test: def _init_host(self, parsed) -> None: - if parsed.hostname is None or not parsed.hostname.strip(): # type: ignore + if (parsed.hostname is None or # type: ignore + not parsed.hostname.strip()): pass -- -- - ####################### - ### SECTION COMMENT ### - ####################### + + ``` ## Ruff Output @@ -397,6 +393,7 @@ from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( MyLovelyCompanyTeamProjectComponent as component # DRY ) + # Please keep __all__ alphabetized within each category. __all__ = [ @@ -421,6 +418,7 @@ __all__ = [ 'NamedTuple', # Not really a type. 'Generator', ] + not_shareables = [ # singletons True, @@ -439,12 +437,15 @@ not_shareables = [ Cheese("Wensleydale"), SubBytes(b"spam"), ] + if 'PYTHON' in os.environ: add_compiler(compiler_from_env()) else: # for compiler in compilers.values(): # add_compiler(compiler) add_compiler(compilers[(7.0, 32)]) + + # Comment before function. def inline_comments_in_brackets_ruin_everything(): if typedargslist: @@ -533,12 +534,18 @@ short Leaf(token.NEWLINE, '\n') # FIXME: \r\n? ], ) + + CONFIG_FILES = [CONFIG_FILE, ] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final + + class Test: def _init_host(self, parsed) -> None: if (parsed.hostname is None or # type: ignore not parsed.hostname.strip()): pass + + ####################### ### SECTION COMMENT ### ####################### diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap deleted file mode 100644 index 77be147ea1..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap +++ /dev/null @@ -1,338 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments4.py ---- -## Input - -```py -from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent, # NOT DRY -) -from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent as component, # DRY -) - - -class C: - @pytest.mark.parametrize( - ("post_data", "message"), - [ - # metadata_version errors. - ( - {}, - "None is an invalid value for Metadata-Version. Error: This field is" - " required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "-1"}, - "'-1' is an invalid value for Metadata-Version. Error: Unknown Metadata" - " Version see" - " https://packaging.python.org/specifications/core-metadata", - ), - # name errors. - ( - {"metadata_version": "1.2"}, - "'' is an invalid value for Name. Error: This field is required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "1.2", "name": "foo-"}, - "'foo-' is an invalid value for Name. Error: Must start and end with a" - " letter or numeral and contain only ascii numeric and '.', '_' and" - " '-'. see https://packaging.python.org/specifications/core-metadata", - ), - # version errors. - ( - {"metadata_version": "1.2", "name": "example"}, - "'' is an invalid value for Version. Error: This field is required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "1.2", "name": "example", "version": "dog"}, - "'dog' is an invalid value for Version. Error: Must start and end with" - " a letter or numeral and contain only ascii numeric and '.', '_' and" - " '-'. see https://packaging.python.org/specifications/core-metadata", - ), - ], - ) - def test_fails_invalid_post_data( - self, pyramid_config, db_request, post_data, message - ): - pyramid_config.testing_securitypolicy(userid=1) - db_request.POST = MultiDict(post_data) - - -def foo(list_a, list_b): - results = ( - User.query.filter(User.foo == "bar") - .filter( # Because foo. - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - # Another comment about the filtering on is_quux goes here. - .filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))) - .order_by(User.created_at.desc()) - .with_for_update(key_share=True) - .all() - ) - return results - - -def foo2(list_a, list_b): - # Standalone comment reasonably placed. - return ( - User.query.filter(User.foo == "bar") - .filter( - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - ) - - -def foo3(list_a, list_b): - return ( - # Standalone comment but weirdly placed. - User.query.filter(User.foo == "bar") - .filter( - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - ) -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -4,8 +4,6 @@ - from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent as component, # DRY - ) -- -- - class C: - @pytest.mark.parametrize( - ("post_data", "message"), -@@ -54,8 +52,6 @@ - ): - pyramid_config.testing_securitypolicy(userid=1) - db_request.POST = MultiDict(post_data) -- -- - def foo(list_a, list_b): - results = ( - User.query.filter(User.foo == "bar") -@@ -70,8 +66,6 @@ - .all() - ) - return results -- -- - def foo2(list_a, list_b): - # Standalone comment reasonably placed. - return ( -@@ -81,8 +75,6 @@ - ) - .filter(User.xyz.is_(None)) - ) -- -- - def foo3(list_a, list_b): - return ( - # Standalone comment but weirdly placed. -``` - -## Ruff Output - -```py -from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent, # NOT DRY -) -from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent as component, # DRY -) -class C: - @pytest.mark.parametrize( - ("post_data", "message"), - [ - # metadata_version errors. - ( - {}, - "None is an invalid value for Metadata-Version. Error: This field is" - " required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "-1"}, - "'-1' is an invalid value for Metadata-Version. Error: Unknown Metadata" - " Version see" - " https://packaging.python.org/specifications/core-metadata", - ), - # name errors. - ( - {"metadata_version": "1.2"}, - "'' is an invalid value for Name. Error: This field is required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "1.2", "name": "foo-"}, - "'foo-' is an invalid value for Name. Error: Must start and end with a" - " letter or numeral and contain only ascii numeric and '.', '_' and" - " '-'. see https://packaging.python.org/specifications/core-metadata", - ), - # version errors. - ( - {"metadata_version": "1.2", "name": "example"}, - "'' is an invalid value for Version. Error: This field is required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "1.2", "name": "example", "version": "dog"}, - "'dog' is an invalid value for Version. Error: Must start and end with" - " a letter or numeral and contain only ascii numeric and '.', '_' and" - " '-'. see https://packaging.python.org/specifications/core-metadata", - ), - ], - ) - def test_fails_invalid_post_data( - self, pyramid_config, db_request, post_data, message - ): - pyramid_config.testing_securitypolicy(userid=1) - db_request.POST = MultiDict(post_data) -def foo(list_a, list_b): - results = ( - User.query.filter(User.foo == "bar") - .filter( # Because foo. - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - # Another comment about the filtering on is_quux goes here. - .filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))) - .order_by(User.created_at.desc()) - .with_for_update(key_share=True) - .all() - ) - return results -def foo2(list_a, list_b): - # Standalone comment reasonably placed. - return ( - User.query.filter(User.foo == "bar") - .filter( - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - ) -def foo3(list_a, list_b): - return ( - # Standalone comment but weirdly placed. - User.query.filter(User.foo == "bar") - .filter( - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - ) -``` - -## Black Output - -```py -from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent, # NOT DRY -) -from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent as component, # DRY -) - - -class C: - @pytest.mark.parametrize( - ("post_data", "message"), - [ - # metadata_version errors. - ( - {}, - "None is an invalid value for Metadata-Version. Error: This field is" - " required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "-1"}, - "'-1' is an invalid value for Metadata-Version. Error: Unknown Metadata" - " Version see" - " https://packaging.python.org/specifications/core-metadata", - ), - # name errors. - ( - {"metadata_version": "1.2"}, - "'' is an invalid value for Name. Error: This field is required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "1.2", "name": "foo-"}, - "'foo-' is an invalid value for Name. Error: Must start and end with a" - " letter or numeral and contain only ascii numeric and '.', '_' and" - " '-'. see https://packaging.python.org/specifications/core-metadata", - ), - # version errors. - ( - {"metadata_version": "1.2", "name": "example"}, - "'' is an invalid value for Version. Error: This field is required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "1.2", "name": "example", "version": "dog"}, - "'dog' is an invalid value for Version. Error: Must start and end with" - " a letter or numeral and contain only ascii numeric and '.', '_' and" - " '-'. see https://packaging.python.org/specifications/core-metadata", - ), - ], - ) - def test_fails_invalid_post_data( - self, pyramid_config, db_request, post_data, message - ): - pyramid_config.testing_securitypolicy(userid=1) - db_request.POST = MultiDict(post_data) - - -def foo(list_a, list_b): - results = ( - User.query.filter(User.foo == "bar") - .filter( # Because foo. - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - # Another comment about the filtering on is_quux goes here. - .filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))) - .order_by(User.created_at.desc()) - .with_for_update(key_share=True) - .all() - ) - return results - - -def foo2(list_a, list_b): - # Standalone comment reasonably placed. - return ( - User.query.filter(User.foo == "bar") - .filter( - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - ) - - -def foo3(list_a, list_b): - return ( - # Standalone comment but weirdly placed. - User.query.filter(User.foo == "bar") - .filter( - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - ) -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap index dcace90ac2..f310ae68e4 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap @@ -86,7 +86,7 @@ if __name__ == "__main__": ```diff --- Black +++ Ruff -@@ -1,11 +1,9 @@ +@@ -1,7 +1,6 @@ while True: if something.changed: - do.stuff() # trailing comment @@ -95,66 +95,22 @@ if __name__ == "__main__": # This one belongs to the `while` block. # Should this one, too? I guess so. -- - # This one is properly standalone now. - - for i in range(100): -@@ -15,27 +13,18 @@ +@@ -15,7 +14,6 @@ # then we do this print(i) - # and finally we loop around -- + with open(some_temp_file) as f: data = f.read() -- - try: - with open(some_other_file) as w: - w.write(data) - - except OSError: - print("problems") -- - import sys -- -- +@@ -33,7 +31,6 @@ # leading function comment def wat(): ... - # trailing function comment -- -- + + # SECTION COMMENT - - -@@ -47,8 +36,6 @@ - @deco3 - def decorated1(): - ... -- -- - # leading 1 - @deco1 - # leading 2 -@@ -56,18 +43,12 @@ - # leading function comment - def decorated1(): - ... -- -- - # Note: this is fixed in - # Preview.empty_lines_before_class_or_def_with_leading_comments. - # In the current style, the user will have to split those lines by hand. - some_instruction -- -- - # This comment should be split from `some_instruction` by two lines but isn't. - def g(): - ... -- -- - if __name__ == "__main__": - main() ``` ## Ruff Output @@ -166,6 +122,7 @@ while True: # This one belongs to the `while` block. # Should this one, too? I guess so. + # This one is properly standalone now. for i in range(100): @@ -175,18 +132,25 @@ for i in range(100): # then we do this print(i) + with open(some_temp_file) as f: data = f.read() + try: with open(some_other_file) as w: w.write(data) except OSError: print("problems") + import sys + + # leading function comment def wat(): ... + + # SECTION COMMENT @@ -198,6 +162,8 @@ def wat(): @deco3 def decorated1(): ... + + # leading 1 @deco1 # leading 2 @@ -205,13 +171,19 @@ def decorated1(): # leading function comment def decorated1(): ... + + # Note: this is fixed in # Preview.empty_lines_before_class_or_def_with_leading_comments. # In the current style, the user will have to split those lines by hand. some_instruction + + # This comment should be split from `some_instruction` by two lines but isn't. def g(): ... + + if __name__ == "__main__": main() ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap index 6181d6489b..d0727290f2 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap @@ -131,100 +131,35 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite ```diff --- Black +++ Ruff -@@ -1,18 +1,12 @@ - from typing import Any, Tuple -- -- - def f( - a, # type: int - ): - pass -- -- - # test type comments - def f(a, b, c, d, e, f, g, h, i): - # type: (int, int, int, int, int, int, int, int, int) -> None - pass -- -- - def f( - a, # type: int - b, # type: int -@@ -26,8 +20,6 @@ - ): - # type: (...) -> None - pass -- -- - def f( - arg, # type: int - *args, # type: *Any -@@ -36,8 +28,6 @@ - ): - # type: (...) -> None - pass -- -- - def f( - a, # type: int - b, # type: int -@@ -66,23 +56,17 @@ +@@ -66,7 +66,7 @@ + element + another_element + another_element_with_long_name - ) # type: int -- -- + ) + + def f( - x, # not a type comment - y, # type: int - ): - # type: (...) -> None - pass -- -- - def f( - x, # not a type comment - ): # type: (int) -> None - pass -- -- - def func( - a=some_list[0], # type: int - ): # type: () -> int -@@ -102,17 +86,12 @@ - c = call( - "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa" # type: ignore - ) -- -- - result = ( # aaa - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ) -- - AAAAAAAAAAAAA = [AAAAAAAAAAAAA] + SHARED_AAAAAAAAAAAAA + USER_AAAAAAAAAAAAA + AAAAAAAAAAAAA # type: ignore -- - call_to_some_function_asdf( - foo, - [AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, BBBBBBBBBBBB], # type: ignore - ) -- - aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type] ``` ## Ruff Output ```py from typing import Any, Tuple + + def f( a, # type: int ): pass + + # test type comments def f(a, b, c, d, e, f, g, h, i): # type: (int, int, int, int, int, int, int, int, int) -> None pass + + def f( a, # type: int b, # type: int @@ -238,6 +173,8 @@ def f( ): # type: (...) -> None pass + + def f( arg, # type: int *args, # type: *Any @@ -246,6 +183,8 @@ def f( ): # type: (...) -> None pass + + def f( a, # type: int b, # type: int @@ -275,16 +214,22 @@ def f( + another_element + another_element_with_long_name ) + + def f( x, # not a type comment y, # type: int ): # type: (...) -> None pass + + def f( x, # not a type comment ): # type: (int) -> None pass + + def func( a=some_list[0], # type: int ): # type: () -> int @@ -304,14 +249,19 @@ def func( c = call( "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa" # type: ignore ) + + result = ( # aaa "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ) + AAAAAAAAAAAAA = [AAAAAAAAAAAAA] + SHARED_AAAAAAAAAAAAA + USER_AAAAAAAAAAAAA + AAAAAAAAAAAAA # type: ignore + call_to_some_function_asdf( foo, [AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, BBBBBBBBBBBB], # type: ignore ) + aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type] ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap index adc7467642..97ea9a42fb 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap @@ -152,99 +152,19 @@ def bar(): ```diff --- Black +++ Ruff -@@ -1,60 +1,35 @@ - # Test for https://github.com/psf/black/issues/246. +@@ -35,9 +35,10 @@ + some = statement -- -- - # This comment should be split from the statement above by two lines. - def function(): - pass -- -- - some = statement -- -- - # This multiline comments section - # should be split from the statement - # above by two lines. - def function(): - pass -- -- - some = statement -- -- - # This comment should be split from the statement above by two lines. - async def async_function(): - pass -- -- - some = statement -- -- - # This comment should be split from the statement above by two lines. - class MyClass: - pass -- -- - some = statement - # This should be stick to the statement above +-# This should be stick to the statement above -- + ++# This should be stick to the statement above ++ # This should be split from the above by two lines class MyClassWithComplexLeadingComments: pass -- -- - class ClassWithDocstring: - """A docstring.""" -- -- - # Leading comment after a class with just a docstring - class MyClassAfterAnotherClassWithDocstring: - pass -- -- - some = statement -- -- - # leading 1 - @deco1 - # leading 2 -@@ -65,11 +40,7 @@ - # leading 4 - def decorated(): - pass -- -- - some = statement -- -- - # leading 1 - @deco1 - # leading 2 -@@ -80,11 +51,7 @@ - # leading 4 - def decorated_with_split_leading_comments(): - pass -- -- - some = statement -- -- - # leading 1 - @deco1 - # leading 2 -@@ -95,66 +62,44 @@ - # leading 4 that already has an empty line - def decorated_with_split_leading_comments(): - pass -- -- - def main(): - if a: +@@ -102,11 +103,9 @@ # Leading comment before inline function def inline(): pass @@ -256,10 +176,7 @@ def bar(): else: # More leading comments def inline_after_else(): - pass -- -- - if a: +@@ -117,11 +116,9 @@ # Leading comment before "top-level inline" function def top_level_quote_inline(): pass @@ -271,39 +188,24 @@ def bar(): else: # More leading comments def top_level_quote_inline_after_else(): - pass -- -- - class MyClass: - # First method has no empty lines between bare class def. - # More comments. - def first_method(self): - pass -- -- +@@ -138,7 +135,6 @@ # Regression test for https://github.com/psf/black/issues/3454. def foo(): pass - # Trailing comment that belongs to this function -- -- + + @decorator1 - @decorator2 # fmt: skip - def bar(): - pass -- -- +@@ -150,9 +146,6 @@ # Regression test for https://github.com/psf/black/issues/3454. def foo(): pass - # Trailing comment that belongs to this function. - # NOTE this comment only has one empty line below, and the formatter - # should enforce two blank lines. -- -- + + @decorator1 - # A standalone comment - def bar(): ``` ## Ruff Output @@ -312,35 +214,61 @@ def bar(): # Test for https://github.com/psf/black/issues/246. some = statement + + # This comment should be split from the statement above by two lines. def function(): pass + + some = statement + + # This multiline comments section # should be split from the statement # above by two lines. def function(): pass + + some = statement + + # This comment should be split from the statement above by two lines. async def async_function(): pass + + some = statement + + # This comment should be split from the statement above by two lines. class MyClass: pass + + some = statement + + # This should be stick to the statement above # This should be split from the above by two lines class MyClassWithComplexLeadingComments: pass + + class ClassWithDocstring: """A docstring.""" + + # Leading comment after a class with just a docstring class MyClassAfterAnotherClassWithDocstring: pass + + some = statement + + # leading 1 @deco1 # leading 2 @@ -351,7 +279,11 @@ some = statement # leading 4 def decorated(): pass + + some = statement + + # leading 1 @deco1 # leading 2 @@ -362,7 +294,11 @@ some = statement # leading 4 def decorated_with_split_leading_comments(): pass + + some = statement + + # leading 1 @deco1 # leading 2 @@ -373,6 +309,8 @@ some = statement # leading 4 that already has an empty line def decorated_with_split_leading_comments(): pass + + def main(): if a: # Leading comment before inline function @@ -385,6 +323,8 @@ def main(): # More leading comments def inline_after_else(): pass + + if a: # Leading comment before "top-level inline" function def top_level_quote_inline(): @@ -396,21 +336,31 @@ else: # More leading comments def top_level_quote_inline_after_else(): pass + + class MyClass: # First method has no empty lines between bare class def. # More comments. def first_method(self): pass + + # Regression test for https://github.com/psf/black/issues/3454. def foo(): pass + + @decorator1 @decorator2 # fmt: skip def bar(): pass + + # Regression test for https://github.com/psf/black/issues/3454. def foo(): pass + + @decorator1 # A standalone comment def bar(): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap index 306cbb3e56..3709a25f65 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap @@ -32,7 +32,7 @@ def function(a:int=42): ```diff --- Black +++ Ruff -@@ -1,23 +1,16 @@ +@@ -1,23 +1,20 @@ -from .config import ( - ConfigTypeAttributes, - Int, @@ -41,16 +41,16 @@ def function(a:int=42): +from .config import ( ConfigTypeAttributes, Int, Path, # String, + # DEFAULT_TYPE_ATTRIBUTES, ) -- + result = 1 # A simple comment -result = (1,) # Another one -- +result = ( 1, ) # Another one + result = 1 #  type: ignore result = 1 # This comment is talking about type: ignore square = Square(4) #  type: Optional[Square] -- -- + + -def function(a: int = 42): - """This docstring is already formatted - a @@ -72,11 +72,15 @@ def function(a:int=42): from .config import ( ConfigTypeAttributes, Int, Path, # String, # DEFAULT_TYPE_ATTRIBUTES, ) + result = 1 # A simple comment result = ( 1, ) # Another one + result = 1 #  type: ignore result = 1 # This comment is talking about type: ignore square = Square(4) #  type: Optional[Square] + + def function(a:int=42): """ This docstring is already formatted a diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap index 419cf8868b..12a415a057 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap @@ -109,18 +109,7 @@ async def wat(): ```diff --- Black +++ Ruff -@@ -8,27 +8,17 @@ - - Possibly also many, many lines. - """ -- - import os.path - import sys -- - import a - from b.c import X # some noqa comment -- - try: +@@ -19,14 +19,9 @@ import fast except ImportError: import slow as fast @@ -132,36 +121,12 @@ async def wat(): - # some strings - y # type: ignore -) -- -- +# some strings +y # type: ignore + + def function(default=None): - """Docstring comes first. - -@@ -45,12 +35,8 @@ - - # This return is also commented for some reason. - return default -- -- - # Explains why we use global state. - GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} -- -- - # Another comment! - # This time two lines. - -@@ -73,8 +59,6 @@ - - self.spam = 4 - """Docstring for instance attribute spam.""" -- -- - #'

This is pweave!

- - -@@ -93,4 +77,4 @@ +@@ -93,4 +88,4 @@ # Some closing comments. # Maybe Vim or Emacs directives for formatting. @@ -183,10 +148,13 @@ async def wat(): Possibly also many, many lines. """ + import os.path import sys + import a from b.c import X # some noqa comment + try: import fast except ImportError: @@ -194,6 +162,8 @@ except ImportError: y = 1 # some strings y # type: ignore + + def function(default=None): """Docstring comes first. @@ -210,8 +180,12 @@ def function(default=None): # This return is also commented for some reason. return default + + # Explains why we use global state. GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} + + # Another comment! # This time two lines. @@ -234,6 +208,8 @@ class Foo: self.spam = 4 """Docstring for instance attribute spam.""" + + #'

This is pweave!

diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap index 3a06521d11..b95e94cf32 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap @@ -63,57 +63,23 @@ def single_quote_docstring_over_line_limit2(): ```diff --- Black +++ Ruff -@@ -1,48 +1,32 @@ +@@ -1,9 +1,11 @@ def docstring_almost_at_line_limit(): - """long docstring.................................................................""" -- -- + """long docstring................................................................. + """ + + def docstring_almost_at_line_limit_with_prefix(): - f"""long docstring................................................................""" -- -- + f"""long docstring................................................................ + """ + + def mulitline_docstring_almost_at_line_limit(): - """long docstring................................................................. +@@ -45,4 +47,4 @@ - .................................................................................. - """ -- -- - def mulitline_docstring_almost_at_line_limit_with_prefix(): - f"""long docstring................................................................ - .................................................................................. - """ -- -- - def docstring_at_line_limit(): - """long docstring................................................................""" -- -- - def docstring_at_line_limit_with_prefix(): - f"""long docstring...............................................................""" -- -- - def multiline_docstring_at_line_limit(): - """first line----------------------------------------------------------------------- - - second line----------------------------------------------------------------------""" -- -- - def multiline_docstring_at_line_limit_with_prefix(): - f"""first line---------------------------------------------------------------------- - - second line----------------------------------------------------------------------""" -- -- - def single_quote_docstring_over_line_limit(): - "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." -- -- def single_quote_docstring_over_line_limit2(): - "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." + 'We do not want to put the closing quote on a new line as that is invalid (see GH-3141).' @@ -125,33 +91,51 @@ def single_quote_docstring_over_line_limit2(): def docstring_almost_at_line_limit(): """long docstring................................................................. """ + + def docstring_almost_at_line_limit_with_prefix(): f"""long docstring................................................................ """ + + def mulitline_docstring_almost_at_line_limit(): """long docstring................................................................. .................................................................................. """ + + def mulitline_docstring_almost_at_line_limit_with_prefix(): f"""long docstring................................................................ .................................................................................. """ + + def docstring_at_line_limit(): """long docstring................................................................""" + + def docstring_at_line_limit_with_prefix(): f"""long docstring...............................................................""" + + def multiline_docstring_at_line_limit(): """first line----------------------------------------------------------------------- second line----------------------------------------------------------------------""" + + def multiline_docstring_at_line_limit_with_prefix(): f"""first line---------------------------------------------------------------------- second line----------------------------------------------------------------------""" + + def single_quote_docstring_over_line_limit(): "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." + + def single_quote_docstring_over_line_limit2(): 'We do not want to put the closing quote on a new line as that is invalid (see GH-3141).' ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap index 933f6529e7..58b5b5f29c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap @@ -234,7 +234,7 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): ```diff --- Black +++ Ruff -@@ -1,219 +1,157 @@ +@@ -1,83 +1,85 @@ class MyClass: + """ Multiline + class docstring @@ -251,64 +251,64 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): - method docstring - """ - pass -- -- + pass + + def foo(): - """This is a docstring with - some lines of text here - """ - return -- -- + """This is a docstring with + some lines of text here + """ + return + + def bar(): - """This is another docstring - with more lines of text - """ - return -- -- + '''This is another docstring + with more lines of text + ''' + return + + def baz(): - '''"This" is a string with some - embedded "quotes"''' - return -- -- + '''"This" is a string with some + embedded "quotes"''' + return + + def troz(): - """Indentation with tabs - is just as OK - """ - return -- -- + '''Indentation with tabs + is just as OK + ''' + return + + def zort(): - """Another - multiline - docstring - """ - pass -- -- + """Another + multiline + docstring + """ + pass + + def poit(): - """ - Lorem ipsum dolor sit amet. @@ -323,8 +323,6 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): - - aliquip ex ea commodo consequat - """ - pass -- -- + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua @@ -333,16 +331,26 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): + - aliquip ex ea commodo consequat + """ + pass + + def under_indent(): - """ - These lines are indented in a way that does not - make sense. +- """ +- pass + """ + These lines are indented in a way that does not +make sense. + """ + pass -+def over_indent(): + + + def over_indent(): +- """ +- This has a shallow indent +- - But some lines are deeper +- - And the closing quote is too deep + """ + This has a shallow indent + - But some lines are deeper @@ -350,90 +358,78 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): """ - pass + pass -+def single_line(): -+ """But with a newline after it! -- --def over_indent(): -- """ -- This has a shallow indent -- - But some lines are deeper -- - And the closing quote is too deep - """ - pass -- -- --def single_line(): + + def single_line(): - """But with a newline after it!""" -- pass -- -- - def this(): - r""" - 'hey ho' - """ -- -- ++ """But with a newline after it! ++ ++ """ + pass + + +@@ -88,25 +90,30 @@ + + def that(): - """ "hey yah" """ -- -- + """ "hey yah" """ + + def and_that(): - """ - "hey yah" """ -- -- + """ + "hey yah" """ + + def and_this(): +- ''' +- "hey yah"''' + ''' + "hey yah"''' -+def multiline_whitespace(): + + + def multiline_whitespace(): +- """ """ + ''' + + + + - ''' -- "hey yah"''' -- -- --def multiline_whitespace(): -- """ """ -- -- ++ ''' + + def oneline_whitespace(): - """ """ -- -- + ''' ''' + + def empty(): - """""" -- -- +@@ -114,12 +121,11 @@ + + def single_quotes(): - "testing" -- -- ++ 'testing' + + -def believe_it_or_not_this_is_in_the_py_stdlib(): - ''' - "hey yah"''' -- -- -+ 'testing' +def believe_it_or_not_this_is_in_the_py_stdlib(): ''' +"hey yah"''' + + def ignored_docstring(): - """a => \ - b""" -- -- +@@ -128,32 +134,32 @@ + + def single_line_docstring_with_whitespace(): - """This should be stripped""" -- -- + """ This should be stripped """ + + def docstring_with_inline_tabs_and_space_indentation(): """hey @@ -449,8 +445,8 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): + + line ends with some tabs """ -- -- + + def docstring_with_inline_tabs_and_tab_indentation(): - """hey + """hey @@ -463,8 +459,6 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): - line ends with some tabs - """ - pass -- -- + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline @@ -473,69 +467,51 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): + line ends with some tabs + """ + pass + + def backslash_space(): - """\ """ -- -- +@@ -161,14 +167,14 @@ + + def multiline_backslash_1(): - """ + ''' hey\there\ - \ """ -- -- + \ ''' + + def multiline_backslash_2(): - """ - hey there \ """ -- -- + ''' + hey there \ ''' + + # Regression test for #3425 - def multiline_backslash_really_long_dont_crash(): - """ - hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ -- -- +@@ -178,8 +184,8 @@ + + def multiline_backslash_3(): - """ - already escaped \\""" -- -- + ''' + already escaped \\ ''' + + def my_god_its_full_of_stars_1(): - "I'm sorry Dave\u2001" -- -- +@@ -188,7 +194,7 @@ + # the space below is actually a \u2001, removed in output def my_god_its_full_of_stars_2(): - "I'm sorry Dave" -- -- + "I'm sorry Dave " + + def docstring_almost_at_line_limit(): - """long docstring.................................................................""" -- -- - def docstring_almost_at_line_limit2(): - """long docstring................................................................. +@@ -213,7 +219,7 @@ - .................................................................................. - """ -- -- - def docstring_at_line_limit(): - """long docstring................................................................""" -- -- - def multiline_docstring_at_line_limit(): - """first line----------------------------------------------------------------------- - second line----------------------------------------------------------------------""" -- -- def stable_quote_normalization_with_immediate_inner_single_quote(self): - """' + '''' @@ -558,31 +534,43 @@ class MyClass: method docstring """ pass + + def foo(): """This is a docstring with some lines of text here """ return + + def bar(): '''This is another docstring with more lines of text ''' return + + def baz(): '''"This" is a string with some embedded "quotes"''' return + + def troz(): '''Indentation with tabs is just as OK ''' return + + def zort(): """Another multiline docstring """ pass + + def poit(): """ Lorem ipsum dolor sit amet. @@ -595,12 +583,16 @@ def poit(): - aliquip ex ea commodo consequat """ pass + + def under_indent(): """ These lines are indented in a way that does not make sense. """ pass + + def over_indent(): """ This has a shallow indent @@ -608,23 +600,35 @@ def over_indent(): - And the closing quote is too deep """ pass + + def single_line(): """But with a newline after it! """ pass + + def this(): r""" 'hey ho' """ + + def that(): """ "hey yah" """ + + def and_that(): """ "hey yah" """ + + def and_this(): ''' "hey yah"''' + + def multiline_whitespace(): ''' @@ -632,19 +636,33 @@ def multiline_whitespace(): ''' + + def oneline_whitespace(): ''' ''' + + def empty(): """""" + + def single_quotes(): 'testing' + + def believe_it_or_not_this_is_in_the_py_stdlib(): ''' "hey yah"''' + + def ignored_docstring(): """a => \ b""" + + def single_line_docstring_with_whitespace(): """ This should be stripped """ + + def docstring_with_inline_tabs_and_space_indentation(): """hey @@ -655,6 +673,8 @@ def docstring_with_inline_tabs_and_space_indentation(): line ends with some tabs """ + + def docstring_with_inline_tabs_and_tab_indentation(): """hey @@ -666,40 +686,64 @@ def docstring_with_inline_tabs_and_tab_indentation(): line ends with some tabs """ pass + + def backslash_space(): """\ """ + + def multiline_backslash_1(): ''' hey\there\ \ ''' + + def multiline_backslash_2(): ''' hey there \ ''' + + # Regression test for #3425 def multiline_backslash_really_long_dont_crash(): """ hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ + + def multiline_backslash_3(): ''' already escaped \\ ''' + + def my_god_its_full_of_stars_1(): "I'm sorry Dave\u2001" + + # the space below is actually a \u2001, removed in output def my_god_its_full_of_stars_2(): "I'm sorry Dave " + + def docstring_almost_at_line_limit(): """long docstring.................................................................""" + + def docstring_almost_at_line_limit2(): """long docstring................................................................. .................................................................................. """ + + def docstring_at_line_limit(): """long docstring................................................................""" + + def multiline_docstring_at_line_limit(): """first line----------------------------------------------------------------------- second line----------------------------------------------------------------------""" + + def stable_quote_normalization_with_immediate_inner_single_quote(self): '''' diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap index 741cc1757f..9782dc0171 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap @@ -105,10 +105,8 @@ def g(): ```diff --- Black +++ Ruff -@@ -1,11 +1,9 @@ - """Docstring.""" -- -- +@@ -3,9 +3,9 @@ + # leading comment def f(): - NO = "" @@ -120,7 +118,7 @@ def g(): t = leaf.type p = leaf.parent # trailing comment -@@ -16,14 +14,19 @@ +@@ -16,14 +16,19 @@ if t == token.COMMENT: # another trailing comment return DOUBLESPACE @@ -140,17 +138,12 @@ def g(): if prevp.type == token.EQUAL: if prevp.parent and prevp.parent.type in { syms.typedargslist, -@@ -43,17 +46,14 @@ - syms.dictsetmaker, - }: - return NO -- -- +@@ -48,12 +53,11 @@ ############################################################################### # SECTION BECAUSE SECTIONS ############################################################################### - - + def g(): - NO = "" - SPACE = " " @@ -161,7 +154,7 @@ def g(): t = leaf.type p = leaf.parent -@@ -67,7 +67,7 @@ +@@ -67,7 +71,7 @@ return DOUBLESPACE # Another comment because more comments @@ -176,6 +169,8 @@ def g(): ```py """Docstring.""" + + # leading comment def f(): NO = '' @@ -223,6 +218,8 @@ def f(): syms.dictsetmaker, }: return NO + + ############################################################################### # SECTION BECAUSE SECTIONS ############################################################################### diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap index 8dbc397d78..e59d18b772 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap @@ -511,26 +511,22 @@ last_call() Ø = set() authors.łukasz.say_thanks() mapping = { -@@ -233,138 +170,84 @@ - C: 0.1 * (10.0 / 12), - D: 0.1 * (10.0 / 12), - } -- -- +@@ -237,134 +174,86 @@ + def gen(): yield from outside_of_generator - a = yield - b = yield - c = yield -- -- + a = (yield) + b = ((yield)) + c = (((yield))) + + async def f(): await some.complicated[0].call(with_args=(True or (1 is not 1))) -- -- + + -print(*[] or [1]) +print(* [] or [1]) print(**{1: 3} if False else {x: x for x in range(3)}) @@ -860,13 +856,19 @@ mapping = { C: 0.1 * (10.0 / 12), D: 0.1 * (10.0 / 12), } + + def gen(): yield from outside_of_generator a = (yield) b = ((yield)) c = (((yield))) + + async def f(): await some.complicated[0].call(with_args=(True or (1 is not 1))) + + print(* [] or [1]) print(**{1: 3} if False else {x: x for x in range(3)}) print(* lambda x: x) diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap index b7f2c2732c..33388de664 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap @@ -53,32 +53,34 @@ def test_calculate_fades(): ```diff --- Black +++ Ruff -@@ -1,8 +1,6 @@ - import pytest -- +@@ -3,6 +3,7 @@ TmSt = 1 TmEx = 2 -- + ++ # fmt: off # Test data: -@@ -17,19 +15,15 @@ - ]) +@@ -18,18 +19,22 @@ def test_fader(test): pass -- + ++ def check_fader(test): pass -- + ++ def verify_fader(test): # misaligned comment pass -- + ++ def verify_fader(test): """Hey, ho.""" assert test.passed() -- + ++ def test_calculate_fades(): calcs = [ # one is zero/none @@ -88,8 +90,11 @@ def test_calculate_fades(): ```py import pytest + TmSt = 1 TmEx = 2 + + # fmt: off # Test data: @@ -104,15 +109,23 @@ TmEx = 2 ]) def test_fader(test): pass + + def check_fader(test): pass + + def verify_fader(test): # misaligned comment pass + + def verify_fader(test): """Hey, ho.""" assert test.passed() + + def test_calculate_fades(): calcs = [ # one is zero/none diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap index 772a2828ad..a092c47a2a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap @@ -30,8 +30,11 @@ x = [ ```diff --- Black +++ Ruff -@@ -12,4 +12,6 @@ +@@ -10,6 +10,9 @@ + 1, 2, + 3, 4, ] ++ # fmt: on -x = [1, 2, 3, 4] @@ -55,6 +58,7 @@ x = [ 1, 2, 3, 4, ] + # fmt: on x = [ diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap index 6f14577620..4c768c3d7d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap @@ -26,14 +26,15 @@ def f(): pass ```diff --- Black +++ Ruff -@@ -4,17 +4,9 @@ +@@ -4,17 +4,11 @@ 3, 4, ]) # fmt: on -def f(): - pass -- -- ++def f(): pass + + -@test( - [ - 1, @@ -44,7 +45,6 @@ def f(): pass -) -def f(): - pass -+def f(): pass +@test([ + 1, 2, + 3, 4, @@ -62,6 +62,8 @@ def f(): pass ]) # fmt: on def f(): pass + + @test([ 1, 2, 3, 4, diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap index cab9d6960c..db5263f639 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap @@ -106,60 +106,37 @@ elif unformatted: # Regression test for https://github.com/psf/black/issues/2015. run( # fmt: off -@@ -22,8 +20,6 @@ - + path, - check=True, - ) -- -- - # Regression test for https://github.com/psf/black/issues/3026. - def test_func(): - # yapf: disable -@@ -34,8 +30,6 @@ - return True - - return False -- -- - # Regression test for https://github.com/psf/black/issues/2567. - if True: - # fmt: off -@@ -44,9 +38,7 @@ +@@ -44,7 +42,7 @@ print ( "This won't be formatted" ) print ( "This won't be formatted either" ) else: - print("This will be formatted") -- -- + print ( "This will be formatted" ) + + # Regression test for https://github.com/psf/black/issues/3184. - class A: - async def call(param): -@@ -61,27 +53,18 @@ +@@ -61,7 +59,7 @@ elif param[0:4] in ("ZZZZ",): print ( "This won't be formatted either" ) - print("This will be formatted") -- -- + print ( "This will be formatted" ) + + # Regression test for https://github.com/psf/black/issues/2985. - class Named(t.Protocol): - # fmt: off - @property - def this_wont_be_formatted ( self ) -> str: ... -- -- +@@ -72,10 +70,7 @@ + + class Factory(t.Protocol): - def this_will_be_formatted(self, **kwargs) -> Named: - ... - - # fmt: on -- -- + def this_will_be_formatted ( self, **kwargs ) -> Named: ... + + # Regression test for https://github.com/psf/black/issues/3436. - if x: +@@ -83,5 +78,5 @@ return x # fmt: off elif unformatted: @@ -194,6 +171,8 @@ run( + path, check=True, ) + + # Regression test for https://github.com/psf/black/issues/3026. def test_func(): # yapf: disable @@ -204,6 +183,8 @@ def test_func(): return True return False + + # Regression test for https://github.com/psf/black/issues/2567. if True: # fmt: off @@ -213,6 +194,8 @@ if True: print ( "This won't be formatted either" ) else: print ( "This will be formatted" ) + + # Regression test for https://github.com/psf/black/issues/3184. class A: async def call(param): @@ -228,13 +211,19 @@ class A: print ( "This won't be formatted either" ) print ( "This will be formatted" ) + + # Regression test for https://github.com/psf/black/issues/2985. class Named(t.Protocol): # fmt: off @property def this_wont_be_formatted ( self ) -> str: ... + + class Factory(t.Protocol): def this_will_be_formatted ( self, **kwargs ) -> Named: ... + + # Regression test for https://github.com/psf/black/issues/3436. if x: return x diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap index 1766ef2a6b..71c0fceef3 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap @@ -199,13 +199,10 @@ d={'a':1, ```diff --- Black +++ Ruff -@@ -1,20 +1,17 @@ - #!/usr/bin/env python3 - import asyncio - import sys -- +@@ -4,17 +4,18 @@ + from third_party import X, Y, Z -- + -from library import some_connection, some_decorator - +from library import some_connection, \ @@ -216,6 +213,8 @@ d={'a':1, # fmt: on -f"trigger 3.6 mode" +f'trigger 3.6 mode' ++ ++ # Comment 1 # Comment 2 @@ -223,15 +222,37 @@ d={'a':1, # fmt: off def func_no_args(): -@@ -39,72 +36,41 @@ +@@ -26,11 +27,15 @@ + continue + exec('new-style exec', {}, {}) + return None ++ ++ + async def coroutine(arg, exec=False): + 'Single-line docstring. Multiline is harder to reformat.' + async with some_connection() as conn: + await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) + await asyncio.sleep(1) ++ ++ + @asyncio.coroutine + @some_decorator( + with_args=True, +@@ -38,28 +43,19 @@ + ) def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str: return text[number:-1] ++ ++ # fmt: on -def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): - offset = attr.ib(default=attr.Factory(lambda: _r.uniform(1, 2))) - assert task._cancel_stack[: len(old_stack)] == old_stack -- -- ++def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''): ++ offset = attr.ib(default=attr.Factory( lambda: _r.uniform(1, 2))) ++ assert task._cancel_stack[:len(old_stack)] == old_stack + + -def spaces_types( - a: int = 1, - b: tuple = (), @@ -244,53 +265,43 @@ d={'a':1, - i: str = r"", -): - ... -- -- ++def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... + + -def spaces2(result=_core.Value(None)): - ... -- -- -+def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''): -+ offset = attr.ib(default=attr.Factory( lambda: _r.uniform(1, 2))) -+ assert task._cancel_stack[:len(old_stack)] == old_stack -+def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... +def spaces2(result= _core.Value(None)): + ... + + something = { - # fmt: off - key: 'value', - } -- -- - def subscriptlist(): - atom[ - # fmt: off +@@ -74,23 +70,20 @@ 'some big and', 'complex subscript', # fmt: on - goes + here, - andhere, - ] -- -- + goes + here, andhere, + ] + + def import_as_names(): # fmt: off from hello import a, b 'unformatted' - # fmt: on -- -- + + def testlist_star_expr(): # fmt: off a , b = *hello 'unformatted' - # fmt: on -- -- + + def yield_expr(): - # fmt: off +@@ -98,11 +91,10 @@ yield hello 'unformatted' # fmt: on @@ -300,22 +311,18 @@ d={'a':1, ( yield hello ) 'unformatted' - # fmt: on -- -- + + def example(session): - # fmt: off - result = session\ -@@ -113,9 +79,6 @@ +@@ -113,7 +105,6 @@ models.Customer.email == email_address)\ .order_by(models.Customer.id.asc())\ .all() - # fmt: on -- -- - def off_and_on_without_data(): - """All comments here are technically on the same prefix. -@@ -123,12 +86,12 @@ + + def off_and_on_without_data(): +@@ -123,8 +114,10 @@ """ # fmt: off @@ -326,12 +333,8 @@ d={'a':1, + # fmt: on pass -- -- - def on_and_off_broken(): - """Another known limitation.""" - # fmt: on -@@ -137,21 +100,10 @@ + +@@ -137,21 +130,12 @@ and_=indeed . it is not formatted because . the . handling . inside . generate_ignored_nodes() now . considers . multiple . fmt . directives . within . one . prefix @@ -339,8 +342,8 @@ d={'a':1, - # fmt: off - # ...but comments still get reformatted even though they should not be - # fmt: on -- -- + + def long_lines(): if True: typedargslist.extend( @@ -354,22 +357,18 @@ d={'a':1, ) # fmt: off a = ( -@@ -182,24 +134,19 @@ - re.MULTILINE|re.VERBOSE - # fmt: on - ) -- -- +@@ -186,20 +170,19 @@ + def single_literal_yapf_disable(): """Black does not support this.""" - BAZ = {(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)} # yapf: disable -- -- + BAZ = { + (1, 2, 3, 4), + (5, 6, 7, 8), + (9, 10, 11, 12) + } + + cfg.rule( - "Default", - "address", @@ -388,7 +387,7 @@ d={'a':1, # fmt: off xxxxxxx_xxxxxxxxxxxx={ "xxxxxxxx": { -@@ -214,7 +161,7 @@ +@@ -214,7 +197,7 @@ }, }, # fmt: on @@ -405,7 +404,9 @@ d={'a':1, #!/usr/bin/env python3 import asyncio import sys + from third_party import X, Y, Z + from library import some_connection, \ some_decorator # fmt: off @@ -413,6 +414,8 @@ from third_party import (X, Y, Z) # fmt: on f'trigger 3.6 mode' + + # Comment 1 # Comment 2 @@ -427,11 +430,15 @@ def func_no_args(): continue exec('new-style exec', {}, {}) return None + + async def coroutine(arg, exec=False): 'Single-line docstring. Multiline is harder to reformat.' async with some_connection() as conn: await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) await asyncio.sleep(1) + + @asyncio.coroutine @some_decorator( with_args=True, @@ -439,17 +446,27 @@ many_args=[1,2,3] ) def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str: return text[number:-1] + + # fmt: on def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''): offset = attr.ib(default=attr.Factory( lambda: _r.uniform(1, 2))) assert task._cancel_stack[:len(old_stack)] == old_stack + + def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... + + def spaces2(result= _core.Value(None)): ... + + something = { # fmt: off key: 'value', } + + def subscriptlist(): atom[ # fmt: off @@ -458,14 +475,20 @@ def subscriptlist(): # fmt: on goes + here, andhere, ] + + def import_as_names(): # fmt: off from hello import a, b 'unformatted' + + def testlist_star_expr(): # fmt: off a , b = *hello 'unformatted' + + def yield_expr(): # fmt: off yield hello @@ -475,6 +498,8 @@ def yield_expr(): # fmt: off ( yield hello ) 'unformatted' + + def example(session): # fmt: off result = session\ @@ -483,6 +508,8 @@ def example(session): models.Customer.email == email_address)\ .order_by(models.Customer.id.asc())\ .all() + + def off_and_on_without_data(): """All comments here are technically on the same prefix. @@ -496,6 +523,8 @@ def off_and_on_without_data(): # fmt: on pass + + def on_and_off_broken(): """Another known limitation.""" # fmt: on @@ -504,6 +533,8 @@ def on_and_off_broken(): and_=indeed . it is not formatted because . the . handling . inside . generate_ignored_nodes() now . considers . multiple . fmt . directives . within . one . prefix + + def long_lines(): if True: typedargslist.extend( @@ -538,6 +569,8 @@ def long_lines(): re.MULTILINE|re.VERBOSE # fmt: on ) + + def single_literal_yapf_disable(): """Black does not support this.""" BAZ = { @@ -545,6 +578,8 @@ def single_literal_yapf_disable(): (5, 6, 7, 8), (9, 10, 11, 12) } + + cfg.rule( "Default", "address", xxxx_xxxx=["xxx-xxxxxx-xxxxxxxxxx"], diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap index 0c9e4e258b..0ebe890404 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap @@ -75,70 +75,42 @@ async def test_async_with(): ```diff --- Black +++ Ruff -@@ -2,15 +2,10 @@ +@@ -2,7 +2,6 @@ def some_func( unformatted, args ): # fmt: skip print("I am some_func") return 0 - # Make sure this comment is not removed. -- -- + + # Make sure a leading comment is not removed. - async def some_async_func( unformatted, args): # fmt: skip - print("I am some_async_func") - await asyncio.sleep(1) -- -- - # Make sure a leading comment is not removed. - class SomeClass( Unformatted, SuperClasses ): # fmt: skip - def some_method( self, unformatted, args ): # fmt: skip -@@ -20,8 +15,6 @@ - async def some_async_method( self, unformatted, args ): # fmt: skip - print("I am some_async_method") - await asyncio.sleep(1) -- -- - # Make sure a leading comment is not removed. - if unformatted_call( args ): # fmt: skip - print("First branch") -@@ -29,34 +22,22 @@ +@@ -29,15 +28,15 @@ elif another_unformatted_call( args ): # fmt: skip print("Second branch") else : # fmt: skip - print("Last branch") -- -- --while some_condition( unformatted, args ): # fmt: skip + print("Last branch") # fmt: skip + + +-while some_condition( unformatted, args ): # fmt: skip +while some_condition( unformatted, args ): # fmt: skip print("Do something") -- -- + + for i in some_iter( unformatted, args ): # fmt: skip - print("Do something") -- -- + print("Do something") # fmt: skip + + async def test_async_for(): - async for i in some_async_iter( unformatted, args ): # fmt: skip - print("Do something") -- -- - try : # fmt: skip - some_call() - except UnformattedError as ex: # fmt: skip - handle_exception() - finally : # fmt: skip - finally_call() -- -- +@@ -54,7 +53,7 @@ + + with give_me_context( unformatted, args ): # fmt: skip - print("Do something") -- -- + print("Do something") # fmt: skip + + async def test_async_with(): - async with give_me_async_context( unformatted, args ): # fmt: skip - print("Do something") ``` ## Ruff Output @@ -148,10 +120,14 @@ async def test_async_with(): def some_func( unformatted, args ): # fmt: skip print("I am some_func") return 0 + + # Make sure a leading comment is not removed. async def some_async_func( unformatted, args): # fmt: skip print("I am some_async_func") await asyncio.sleep(1) + + # Make sure a leading comment is not removed. class SomeClass( Unformatted, SuperClasses ): # fmt: skip def some_method( self, unformatted, args ): # fmt: skip @@ -161,6 +137,8 @@ class SomeClass( Unformatted, SuperClasses ): # fmt: skip async def some_async_method( self, unformatted, args ): # fmt: skip print("I am some_async_method") await asyncio.sleep(1) + + # Make sure a leading comment is not removed. if unformatted_call( args ): # fmt: skip print("First branch") @@ -169,21 +147,33 @@ elif another_unformatted_call( args ): # fmt: skip print("Second branch") else : # fmt: skip print("Last branch") # fmt: skip + + while some_condition( unformatted, args ): # fmt: skip print("Do something") + + for i in some_iter( unformatted, args ): # fmt: skip print("Do something") # fmt: skip + + async def test_async_for(): async for i in some_async_iter( unformatted, args ): # fmt: skip print("Do something") + + try : # fmt: skip some_call() except UnformattedError as ex: # fmt: skip handle_exception() finally : # fmt: skip finally_call() + + with give_me_context( unformatted, args ): # fmt: skip print("Do something") # fmt: skip + + async def test_async_with(): async with give_me_async_context( unformatted, args ): # fmt: skip print("Do something") diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap index 6af0b38a4e..1a6c83f39e 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap @@ -82,12 +82,8 @@ with hmm_but_this_should_get_two_preceding_newlines(): ) limited.append(-limited.pop()) # negate top return A( -@@ -13,34 +13,22 @@ - very_long_argument_name2=-very.long.value.for_the_argument, - **kwargs, - ) -- -- +@@ -17,30 +17,24 @@ + def g(): "Docstring." - @@ -95,15 +91,15 @@ with hmm_but_this_should_get_two_preceding_newlines(): pass - print("Inner defs should breathe a little.") -- -- + + def h(): def inner(): pass - print("Inner defs should breathe a little.") -- -- + + if os.name == "posix": import termios - @@ -117,7 +113,7 @@ with hmm_but_this_should_get_two_preceding_newlines(): def i_should_be_followed_by_only_one_newline(): pass -@@ -54,12 +42,9 @@ +@@ -54,12 +48,10 @@ class IHopeYouAreHavingALovelyDay: def __call__(self): print("i_should_be_followed_by_only_one_newline") @@ -127,7 +123,7 @@ with hmm_but_this_should_get_two_preceding_newlines(): def foo(): pass - -- + with hmm_but_this_should_get_two_preceding_newlines(): pass ``` @@ -150,15 +146,21 @@ def f( very_long_argument_name2=-very.long.value.for_the_argument, **kwargs, ) + + def g(): "Docstring." def inner(): pass print("Inner defs should breathe a little.") + + def h(): def inner(): pass print("Inner defs should breathe a little.") + + if os.name == "posix": import termios def i_should_be_followed_by_only_one_newline(): @@ -183,6 +185,7 @@ else: def foo(): pass + with hmm_but_this_should_get_two_preceding_newlines(): pass ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap index a1ca74dcab..ceeff81fdc 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap @@ -108,21 +108,18 @@ def __await__(): return (yield) ```diff --- Black +++ Ruff -@@ -1,100 +1,52 @@ - #!/usr/bin/env python3 - import asyncio - import sys -- +@@ -4,97 +4,67 @@ + from third_party import X, Y, Z -- + -from library import some_connection, some_decorator -- --f"trigger 3.6 mode" -- -- +from library import some_connection, \ + some_decorator +f'trigger 3.6 mode' + +-f"trigger 3.6 mode" +- + def func_no_args(): - a - b @@ -136,8 +133,6 @@ def __await__(): return (yield) - continue - exec("new-style exec", {}, {}) - return None -- -- + a; b; c + if True: raise RuntimeError + if False: ... @@ -146,17 +141,19 @@ def __await__(): return (yield) + continue + exec("new-style exec", {}, {}) + return None + + async def coroutine(arg, exec=False): - "Single-line docstring. Multiline is harder to reformat." - async with some_connection() as conn: - await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) - await asyncio.sleep(1) -- -- + "Single-line docstring. Multiline is harder to reformat." + async with some_connection() as conn: + await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) + await asyncio.sleep(1) + + @asyncio.coroutine -@some_decorator(with_args=True, many_args=[1, 2, 3]) -def function_signature_stress_test( @@ -168,13 +165,22 @@ def __await__(): return (yield) - **kwargs, -) -> str: - return text[number:-1] -- -- ++@some_decorator( ++with_args=True, ++many_args=[1,2,3] ++) ++def function_signature_stress_test(number:int,no_annotation=None,text:str="default",* ,debug:bool=False,**kwargs) -> str: ++ return text[number:-1] + + -def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): - offset = attr.ib(default=attr.Factory(lambda: _r.uniform(10000, 200000))) - assert task._cancel_stack[: len(old_stack)] == old_stack -- -- ++def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''): ++ offset = attr.ib(default=attr.Factory( lambda: _r.uniform(10000, 200000))) ++ assert task._cancel_stack[:len(old_stack)] == old_stack + + -def spaces_types( - a: int = 1, - b: tuple = (), @@ -187,24 +193,15 @@ def __await__(): return (yield) - i: str = r"", -): - ... -- -- ++def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... + + -def spaces2(result=_core.Value(None)): - assert fut is self._read_fut, (fut, self._read_fut) -- -- -+@some_decorator( -+with_args=True, -+many_args=[1,2,3] -+) -+def function_signature_stress_test(number:int,no_annotation=None,text:str="default",* ,debug:bool=False,**kwargs) -> str: -+ return text[number:-1] -+def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''): -+ offset = attr.ib(default=attr.Factory( lambda: _r.uniform(10000, 200000))) -+ assert task._cancel_stack[:len(old_stack)] == old_stack -+def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... +def spaces2(result= _core.Value(None)): + assert fut is self._read_fut, (fut, self._read_fut) + + def example(session): - result = ( - session.query(models.Customer.id) @@ -215,14 +212,14 @@ def __await__(): return (yield) - .order_by(models.Customer.id.asc()) - .all() - ) -- -- + result = session.query(models.Customer.id).filter( + models.Customer.account_id == account_id, + models.Customer.email == email_address, + ).order_by( + models.Customer.id.asc() + ).all() + + def long_lines(): if True: typedargslist.extend( @@ -244,7 +241,7 @@ def __await__(): return (yield) # trailing standalone comment ) ) -@@ -117,23 +69,18 @@ +@@ -117,23 +87,22 @@ \n? ) $ @@ -252,8 +249,8 @@ def __await__(): return (yield) - re.MULTILINE | re.VERBOSE, + """, re.MULTILINE | re.VERBOSE ) -- -- + + def trailing_comma(): mapping = { - A: 0.25 * (10.0 / 12), @@ -261,13 +258,13 @@ def __await__(): return (yield) - C: 0.1 * (10.0 / 12), - D: 0.1 * (10.0 / 12), - } -- -- + A: 0.25 * (10.0 / 12), + B: 0.1 * (10.0 / 12), + C: 0.1 * (10.0 / 12), + D: 0.1 * (10.0 / 12), +} + + def f( - a, - **kwargs, @@ -276,12 +273,10 @@ def __await__(): return (yield) ) -> A: return ( yield from A( -@@ -142,7 +89,4 @@ - **kwargs, - ) +@@ -144,5 +113,4 @@ ) -- -- + + -def __await__(): - return (yield) +def __await__(): return (yield) @@ -293,10 +288,14 @@ def __await__(): return (yield) #!/usr/bin/env python3 import asyncio import sys + from third_party import X, Y, Z + from library import some_connection, \ some_decorator f'trigger 3.6 mode' + + def func_no_args(): a; b; c if True: raise RuntimeError @@ -306,11 +305,15 @@ def func_no_args(): continue exec("new-style exec", {}, {}) return None + + async def coroutine(arg, exec=False): "Single-line docstring. Multiline is harder to reformat." async with some_connection() as conn: await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) await asyncio.sleep(1) + + @asyncio.coroutine @some_decorator( with_args=True, @@ -318,12 +321,20 @@ many_args=[1,2,3] ) def function_signature_stress_test(number:int,no_annotation=None,text:str="default",* ,debug:bool=False,**kwargs) -> str: return text[number:-1] + + def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''): offset = attr.ib(default=attr.Factory( lambda: _r.uniform(10000, 200000))) assert task._cancel_stack[:len(old_stack)] == old_stack + + def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... + + def spaces2(result= _core.Value(None)): assert fut is self._read_fut, (fut, self._read_fut) + + def example(session): result = session.query(models.Customer.id).filter( models.Customer.account_id == account_id, @@ -331,6 +342,8 @@ def example(session): ).order_by( models.Customer.id.asc() ).all() + + def long_lines(): if True: typedargslist.extend( @@ -363,6 +376,8 @@ def long_lines(): $ """, re.MULTILINE | re.VERBOSE ) + + def trailing_comma(): mapping = { A: 0.25 * (10.0 / 12), @@ -370,6 +385,8 @@ def trailing_comma(): C: 0.1 * (10.0 / 12), D: 0.1 * (10.0 / 12), } + + def f( a, **kwargs, @@ -381,6 +398,8 @@ def f( **kwargs, ) ) + + def __await__(): return (yield) ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap index d5edb4eca1..6de2f2bfd1 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap @@ -74,7 +74,7 @@ some_module.some_function( ```diff --- Black +++ Ruff -@@ -1,114 +1,46 @@ +@@ -1,69 +1,28 @@ -def f( - a, -): @@ -84,8 +84,8 @@ some_module.some_function( +def f(a,): + d = {'key': 'value',} tup = (1,) -- -- + + -def f2( - a, - b, @@ -98,8 +98,11 @@ some_module.some_function( - 1, - 2, - ) -- -- ++def f2(a,b,): ++ d = {'key': 'value', 'key2': 'value2',} ++ tup = (1,2,) + + -def f( - a: int = 1, -): @@ -111,9 +114,6 @@ some_module.some_function( - call2( - arg=[1, 2, 3], - ) -+def f2(a,b,): -+ d = {'key': 'value', 'key2': 'value2',} -+ tup = (1,2,) +def f(a:int=1,): + call(arg={'explode': 'this',}) + call2(arg=[1,2,3],) @@ -136,8 +136,8 @@ some_module.some_function( - ): + if a == {"a": 1,"b": 2,"c": 3,"d": 4,"e": 5,"f": 6,"g": 7,"h": 8,}["a"]: pass -- -- + + -def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> ( - Set["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] -): @@ -150,20 +150,17 @@ some_module.some_function( - } - } - } -- -- +def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +]: + json = {"k": {"k2": {"k3": [1,]}}} + + # The type annotation shouldn't get a trailing comma since that would change its type. - # Relevant bug report: https://github.com/psf/black/issues/2381. - def some_function_with_a_really_long_name() -> ( - returning_a_deeply_nested_import_of_a_type_i_suppose - ): +@@ -74,24 +33,21 @@ pass -- -- + + -def some_method_with_a_really_long_name( - very_long_parameter_so_yeah: str, another_long_parameter: int -) -> another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not: @@ -171,8 +168,8 @@ some_module.some_function( + another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not +): pass -- -- + + def func() -> ( - also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( - this_shouldn_t_get_a_trailing_comma_too @@ -180,8 +177,8 @@ some_module.some_function( + also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black(this_shouldn_t_get_a_trailing_comma_too) ): pass -- -- + + -def func() -> ( - also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( +def func() -> ((also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( @@ -190,9 +187,8 @@ some_module.some_function( + )) ): pass -- -- - # Make sure inner one-element tuple won't explode + +@@ -100,15 +56,7 @@ some_module.some_function( argument1, (one_element_tuple,), argument4, argument5, argument6 ) @@ -217,9 +213,13 @@ some_module.some_function( def f(a,): d = {'key': 'value',} tup = (1,) + + def f2(a,b,): d = {'key': 'value', 'key2': 'value2',} tup = (1,2,) + + def f(a:int=1,): call(arg={'explode': 'this',}) call2(arg=[1,2,3],) @@ -229,29 +229,41 @@ def f(a:int=1,): }["a"] if a == {"a": 1,"b": 2,"c": 3,"d": 4,"e": 5,"f": 6,"g": 7,"h": 8,}["a"]: pass + + def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ]: json = {"k": {"k2": {"k3": [1,]}}} + + # The type annotation shouldn't get a trailing comma since that would change its type. # Relevant bug report: https://github.com/psf/black/issues/2381. def some_function_with_a_really_long_name() -> ( returning_a_deeply_nested_import_of_a_type_i_suppose ): pass + + def some_method_with_a_really_long_name(very_long_parameter_so_yeah: str, another_long_parameter: int) -> ( another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not ): pass + + def func() -> ( also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black(this_shouldn_t_get_a_trailing_comma_too) ): pass + + def func() -> ((also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( this_shouldn_t_get_a_trailing_comma_too )) ): pass + + # Make sure inner one-element tuple won't explode some_module.some_function( argument1, (one_element_tuple,), argument4, argument5, argument6 diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap index b7cde495e0..352abb2541 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap @@ -62,9 +62,8 @@ __all__ = ( ```diff --- Black +++ Ruff -@@ -1,55 +1,33 @@ - """The asyncio package, tracking PEP 3156.""" -- +@@ -2,12 +2,13 @@ + # flake8: noqa -from logging import WARNING @@ -79,16 +78,9 @@ __all__ = ( # This relies on each of the submodules having an __all__ variable. from .base_events import * from .coroutines import * - from .events import * # comment here -- - from .futures import * - from .locks import * # comment here - from .protocols import * -- - from ..runners import * # comment here - from ..queues import * +@@ -22,33 +23,16 @@ from ..streams import * -- + from some_library import ( - Just, - Enough, @@ -111,7 +103,7 @@ __all__ = ( -) +from name_of_a_company.extremely_long_project_name.component.ttypes import CuteLittleServiceHandlerFactoryyy from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * -- + from .a.b.c.subprocess import * -from . import tasks -from . import A, B, C @@ -119,20 +111,20 @@ __all__ = ( - SomeVeryLongNameAndAllOfItsAdditionalLetters1, - SomeVeryLongNameAndAllOfItsAdditionalLetters2, -) -- +from . import (tasks) +from . import (A, B, C) +from . import SomeVeryLongNameAndAllOfItsAdditionalLetters1, \ + SomeVeryLongNameAndAllOfItsAdditionalLetters2 + __all__ = ( base_events.__all__ - + coroutines.__all__ ``` ## Ruff Output ```py """The asyncio package, tracking PEP 3156.""" + # flake8: noqa from logging import ( @@ -146,22 +138,27 @@ import sys from .base_events import * from .coroutines import * from .events import * # comment here + from .futures import * from .locks import * # comment here from .protocols import * + from ..runners import * # comment here from ..queues import * from ..streams import * + from some_library import ( Just, Enough, Libraries, To, Fit, In, This, Nice, Split, Which, We, No, Longer, Use ) from name_of_a_company.extremely_long_project_name.component.ttypes import CuteLittleServiceHandlerFactoryyy from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * + from .a.b.c.subprocess import * from . import (tasks) from . import (A, B, C) from . import SomeVeryLongNameAndAllOfItsAdditionalLetters1, \ SomeVeryLongNameAndAllOfItsAdditionalLetters2 + __all__ = ( base_events.__all__ + coroutines.__all__ diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap index e2e7b79f7e..4f258ac888 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap @@ -76,34 +76,20 @@ return np.divide( ```diff --- Black +++ Ruff -@@ -1,16 +1,10 @@ +@@ -1,10 +1,10 @@ def function(**kwargs): t = a**2 + b**3 - return t**2 -- -- + return t ** 2 + + def function_replace_spaces(**kwargs): - t = a**2 + b**3 + c**4 -- -- + t = a **2 + b** 3 + c ** 4 + + def function_dont_replace_spaces(): - {**a, **b, **c} -- -- - a = 5**~4 - b = 5 ** f() - c = -(5**2) -@@ -29,7 +23,6 @@ - p = {(k, k**2): v**2 for k, v in pairs} - q = [10**i for i in range(6)] - r = x**y -- - a = 5.0**~4.0 - b = 5.0 ** f() - c = -(5.0**2.0) -@@ -47,8 +40,6 @@ +@@ -47,8 +47,6 @@ o = settings(max_examples=10**6.0) p = {(k, k**2): v**2.0 for k, v in pairs} q = [10.5**i for i in range(6)] @@ -112,14 +98,6 @@ return np.divide( # WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) if hasattr(view, "sum_of_weights"): return np.divide( # type: ignore[no-any-return] -@@ -57,7 +48,6 @@ - out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr] - where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr] - ) -- - return np.divide( - where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore - ) ``` ## Ruff Output @@ -128,10 +106,16 @@ return np.divide( def function(**kwargs): t = a**2 + b**3 return t ** 2 + + def function_replace_spaces(**kwargs): t = a **2 + b** 3 + c ** 4 + + def function_dont_replace_spaces(): {**a, **b, **c} + + a = 5**~4 b = 5 ** f() c = -(5**2) @@ -150,6 +134,7 @@ o = settings(max_examples=10**6) p = {(k, k**2): v**2 for k, v in pairs} q = [10**i for i in range(6)] r = x**y + a = 5.0**~4.0 b = 5.0 ** f() c = -(5.0**2.0) @@ -175,6 +160,7 @@ if hasattr(view, "sum_of_weights"): out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr] where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr] ) + return np.divide( where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore ) diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap index 603cab0375..42f7589dcd 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap @@ -94,56 +94,49 @@ async def main(): ```diff --- Black +++ Ruff -@@ -1,93 +1,64 @@ - import asyncio -- -- - # Control example - async def main(): - await asyncio.sleep(1) -- -- +@@ -8,59 +8,64 @@ + # Remove brackets for short coroutine/task async def main(): - await asyncio.sleep(1) -- -- + await (asyncio.sleep(1)) + + async def main(): - await asyncio.sleep(1) -- -- + await ( + asyncio.sleep(1) + ) + + async def main(): - await asyncio.sleep(1) -- -- + await (asyncio.sleep(1) + ) + + # Check comments async def main(): - await asyncio.sleep(1) # Hello -- -- + await ( # Hello + asyncio.sleep(1) + ) + + async def main(): - await asyncio.sleep(1) # Hello -- -- + await ( + asyncio.sleep(1) # Hello + ) + + async def main(): - await asyncio.sleep(1) # Hello -- -- + await ( + asyncio.sleep(1) + ) + + # Long lines async def main(): - await asyncio.gather( @@ -155,9 +148,9 @@ async def main(): - asyncio.sleep(1), - asyncio.sleep(1), - ) -- -- + await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1)) + + # Same as above but with magic trailing comma in function async def main(): - await asyncio.gather( @@ -169,14 +162,12 @@ async def main(): - asyncio.sleep(1), - asyncio.sleep(1), - ) -- -- + await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1),) + + # Cr@zY Br@ck3Tz async def main(): - await black(1) -- -- + await ( + ((((((((((((( + ((( ((( @@ -190,74 +181,81 @@ async def main(): + ))) ))) + ))))))))))))) + ) + + # Keep brackets around non power operations and nested awaits - async def main(): - await (set_of_tasks | other_set) -- -- - async def main(): - await (await asyncio.sleep(1)) -- -- - # It's awaits all the way down... - async def main(): - await (await x) -- -- - async def main(): - await (yield x) -- -- +@@ -82,11 +87,11 @@ + + async def main(): - await (await asyncio.sleep(1)) -- -- + await (await (asyncio.sleep(1))) + + async def main(): - await (await (await (await (await asyncio.sleep(1))))) -- -- + await (await (await (await (await (asyncio.sleep(1)))))) + + async def main(): - await (yield) ``` ## Ruff Output ```py import asyncio + + # Control example async def main(): await asyncio.sleep(1) + + # Remove brackets for short coroutine/task async def main(): await (asyncio.sleep(1)) + + async def main(): await ( asyncio.sleep(1) ) + + async def main(): await (asyncio.sleep(1) ) + + # Check comments async def main(): await ( # Hello asyncio.sleep(1) ) + + async def main(): await ( asyncio.sleep(1) # Hello ) + + async def main(): await ( asyncio.sleep(1) ) + + # Long lines async def main(): await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1)) + + # Same as above but with magic trailing comma in function async def main(): await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1),) + + # Cr@zY Br@ck3Tz async def main(): await ( @@ -273,20 +271,34 @@ async def main(): ))) ))) ))))))))))))) ) + + # Keep brackets around non power operations and nested awaits async def main(): await (set_of_tasks | other_set) + + async def main(): await (await asyncio.sleep(1)) + + # It's awaits all the way down... async def main(): await (await x) + + async def main(): await (yield x) + + async def main(): await (await (asyncio.sleep(1))) + + async def main(): await (await (await (await (await (asyncio.sleep(1)))))) + + async def main(): await (yield) ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap index f7724c3116..7f435c1e2f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap @@ -48,7 +48,7 @@ except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.ov ```diff --- Black +++ Ruff -@@ -1,42 +1,25 @@ +@@ -1,42 +1,27 @@ # These brackets are redundant, therefore remove. try: a.something @@ -78,7 +78,7 @@ except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.ov -) as err: +except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error) as err: raise err -- + try: a.something -except ( @@ -86,7 +86,7 @@ except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.ov -) as err: +except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error,) as err: raise err -- + try: a.something -except ( @@ -117,10 +117,12 @@ try: a.something except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error) as err: raise err + try: a.something except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error,) as err: raise err + try: a.something except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error) as err: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap index df7d50d6f2..d432c040c7 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap @@ -32,7 +32,7 @@ for (((((k, v))))) in d.items(): ```diff --- Black +++ Ruff -@@ -1,27 +1,15 @@ +@@ -1,27 +1,16 @@ # Only remove tuple brackets after `for` -for k, v in d.items(): +for (k, v) in d.items(): @@ -50,7 +50,7 @@ for (((((k, v))))) in d.items(): -) in d.items(): +for (why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long, i_dont_know_but_we_should_still_check_the_behaviour_if_they_do) in d.items(): print(k, v) -- + -for ( - k, - v, @@ -79,6 +79,7 @@ for module in (core, _unicodefun): # Brackets remain for long for loop lines for (why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long, i_dont_know_but_we_should_still_check_the_behaviour_if_they_do) in d.items(): print(k, v) + for (k, v) in dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items(): print(k, v) # Test deeply nested brackets diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap index fcfe41d5a6..7e1a6486b2 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap @@ -121,101 +121,90 @@ with open("/path/to/file.txt", mode="r") as read_file: ```diff --- Black +++ Ruff -@@ -1,78 +1,68 @@ - import random -+def foo1(): - -+ print("The newline above me should be deleted!") -+def foo2(): - --def foo1(): -- print("The newline above me should be deleted!") +@@ -2,20 +2,26 @@ --def foo2(): + def foo1(): ++ + print("The newline above me should be deleted!") + + + def foo2(): ++ ++ ++ print("All the newlines above me should be deleted!") -- -- + + def foo3(): + print("No newline above me!") print("There is a newline above me, and that's OK!") -- -- + + def foo4(): + # There is a comment here print("The newline above me should not be deleted!") -- -- +@@ -23,27 +29,39 @@ + class Foo: def bar(self): + print("The newline above me should be deleted!") -+for i in range(5): - -+ print(f"{i}) The line above me should be removed!") -+for i in range(5): - --for i in range(5): -- print(f"{i}) The line above me should be removed!") -+ print(f"{i}) The lines above me should be removed!") for i in range(5): -- print(f"{i}) The lines above me should be removed!") ++ + print(f"{i}) The line above me should be removed!") -+ for j in range(7): --for i in range(5): -- for j in range(7): + for i in range(5): ++ ++ ++ + print(f"{i}) The lines above me should be removed!") + + + for i in range(5): ++ + for j in range(7): ++ print(f"{i}) The lines above me should be removed!") -+if random.randint(0, 3) == 0: -- -+ print("The new line above me is about to be removed!") + if random.randint(0, 3) == 0: -- print("The new line above me is about to be removed!") ++ + print("The new line above me is about to be removed!") --if random.randint(0, 3) == 0: -- print("The new lines above me is about to be removed!") - - -+ print("The new lines above me is about to be removed!") if random.randint(0, 3) == 0: - if random.uniform(0, 1) > 0.5: - print("Two lines above me are about to be removed!") -- -- - while True: - print("The newline above me should be deleted!") -- -- - while True: - print("The newlines above me should be deleted!") -- -- - while True: - while False: - print("The newlines above me should be deleted!") -+with open("/path/to/file.txt", mode="w") as file: ++ ++ ++ ++ + print("The new lines above me is about to be removed!") + + +@@ -66,13 +84,19 @@ + -- -+ file.write("The new line above me is about to be removed!") with open("/path/to/file.txt", mode="w") as file: -- file.write("The new line above me is about to be removed!") ++ + file.write("The new line above me is about to be removed!") --with open("/path/to/file.txt", mode="w") as file: + with open("/path/to/file.txt", mode="w") as file: ++ ++ + file.write("The new lines above me is about to be removed!") -+with open("/path/to/file.txt", mode="r") as read_file: -- --with open("/path/to/file.txt", mode="r") as read_file: + + with open("/path/to/file.txt", mode="r") as read_file: ++ with open("/path/to/output_file.txt", mode="w") as write_file: + write_file.writelines(read_file.readlines()) @@ -225,68 +214,102 @@ with open("/path/to/file.txt", mode="r") as read_file: ```py import random + + def foo1(): print("The newline above me should be deleted!") + + def foo2(): print("All the newlines above me should be deleted!") + + def foo3(): print("No newline above me!") print("There is a newline above me, and that's OK!") + + def foo4(): # There is a comment here print("The newline above me should not be deleted!") + + class Foo: def bar(self): print("The newline above me should be deleted!") + + for i in range(5): print(f"{i}) The line above me should be removed!") + + for i in range(5): print(f"{i}) The lines above me should be removed!") + + for i in range(5): for j in range(7): print(f"{i}) The lines above me should be removed!") + + if random.randint(0, 3) == 0: print("The new line above me is about to be removed!") + + if random.randint(0, 3) == 0: print("The new lines above me is about to be removed!") + + if random.randint(0, 3) == 0: if random.uniform(0, 1) > 0.5: print("Two lines above me are about to be removed!") + + while True: print("The newline above me should be deleted!") + + while True: print("The newlines above me should be deleted!") + + while True: while False: print("The newlines above me should be deleted!") + + with open("/path/to/file.txt", mode="w") as file: file.write("The new line above me is about to be removed!") + + with open("/path/to/file.txt", mode="w") as file: file.write("The new lines above me is about to be removed!") + + with open("/path/to/file.txt", mode="r") as read_file: with open("/path/to/output_file.txt", mode="w") as write_file: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap index d06c27e3a0..7b2de231ae 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap @@ -68,19 +68,15 @@ def example8(): ```diff --- Black +++ Ruff -@@ -1,85 +1,34 @@ +@@ -1,5 +1,5 @@ -x = 1 -x = 1.2 -- +x = (1) +x = (1.2) + data = ( "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ).encode() -- -- - async def show_status(): - while True: +@@ -11,75 +11,47 @@ try: if report_host: data = ( @@ -90,52 +86,52 @@ def example8(): + ).encode() except Exception as e: pass -- -- + + def example(): - return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -- -- + return (("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")) + + def example1(): - return 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111 -- -- + return ((1111111111111111111111111111111111111111111111111111111111111111111111111111111111111)) + + def example1point5(): - return 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111 -- -- + return ((((((1111111111111111111111111111111111111111111111111111111111111111111111111111111111111)))))) + + def example2(): - return ( - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ) -- -- + return (("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")) + + def example3(): - return ( - 1111111111111111111111111111111111111111111111111111111111111111111111111111111 - ) -- -- + return ((1111111111111111111111111111111111111111111111111111111111111111111111111111111)) + + def example4(): - return True -- -- + return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((True)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) + + def example5(): - return () -- -- + return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((())))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) + + def example6(): - return {a: a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]} -- -- + return ((((((((({a:a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]}))))))))) + + def example7(): - return { - a: a @@ -162,9 +158,9 @@ def example8(): - 20000000000000000000, - ] - } -- -- + return ((((((((({a:a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20000000000000000000]}))))))))) + + def example8(): - return None + return (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((None))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) @@ -175,9 +171,12 @@ def example8(): ```py x = (1) x = (1.2) + data = ( "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ).encode() + + async def show_status(): while True: try: @@ -187,24 +186,44 @@ async def show_status(): ).encode() except Exception as e: pass + + def example(): return (("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")) + + def example1(): return ((1111111111111111111111111111111111111111111111111111111111111111111111111111111111111)) + + def example1point5(): return ((((((1111111111111111111111111111111111111111111111111111111111111111111111111111111111111)))))) + + def example2(): return (("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")) + + def example3(): return ((1111111111111111111111111111111111111111111111111111111111111111111111111111111)) + + def example4(): return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((True)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) + + def example5(): return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((())))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) + + def example6(): return ((((((((({a:a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]}))))))))) + + def example7(): return ((((((((({a:a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20000000000000000000]}))))))))) + + def example8(): return (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((None))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap index f80e25a7c9..d09d9dfaa7 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap @@ -101,82 +101,80 @@ def foo() -> tuple[int, int, int,]: ```diff --- Black +++ Ruff -@@ -1,120 +1,70 @@ +@@ -1,33 +1,41 @@ # Control def double(a: int) -> int: - return 2 * a -- -- + return 2*a + + # Remove the brackets -def double(a: int) -> int: - return 2 * a -- -- +def double(a: int) -> (int): + return 2*a + + # Some newline variations -def double(a: int) -> int: - return 2 * a -- -- --def double(a: int) -> int: -- return 2 * a -- -- --def double(a: int) -> int: -- return 2 * a -- -- +def double(a: int) -> ( + int): + return 2*a + + +-def double(a: int) -> int: +- return 2 * a +def double(a: int) -> (int +): + return 2*a + + +-def double(a: int) -> int: +- return 2 * a +def double(a: int) -> ( + int +): + return 2*a + + # Don't lose the comments -def double(a: int) -> int: # Hello - return 2 * a -- -- --def double(a: int) -> int: # Hello -- return 2 * a -- -- +def double(a: int) -> ( # Hello + int +): + return 2*a + + +-def double(a: int) -> int: # Hello +- return 2 * a +def double(a: int) -> ( + int # Hello +): + return 2*a + + # Really long annotations - def foo() -> ( - intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds - ): +@@ -37,84 +45,62 @@ return 2 -- -- + + -def foo() -> ( - intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds -): +def foo() -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 -- -- + + -def foo() -> ( - intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds - | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds -): +def foo() -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 -- -- + + -def foo( - a: int, - b: int, @@ -184,8 +182,8 @@ def foo() -> tuple[int, int, int,]: -) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: +def foo(a: int, b: int, c: int,) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 -- -- + + -def foo( - a: int, - b: int, @@ -196,8 +194,8 @@ def foo() -> tuple[int, int, int,]: -): +def foo(a: int, b: int, c: int,) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 -- -- + + # Split args but no need to split return -def foo( - a: int, @@ -206,20 +204,18 @@ def foo() -> tuple[int, int, int,]: -) -> int: +def foo(a: int, b: int, c: int,) -> int: return 2 -- -- + + # Deeply nested brackets # with *interesting* spacing -def double(a: int) -> int: - return 2 * a -- -- --def double(a: int) -> int: -- return 2 * a -- -- +def double(a: int) -> (((((int))))): + return 2*a + + +-def double(a: int) -> int: +- return 2 * a +def double(a: int) -> ( + ( ( + ((int) @@ -228,6 +224,8 @@ def foo() -> tuple[int, int, int,]: + ) + ): + return 2*a + + def foo() -> ( + ( ( intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds @@ -235,26 +233,27 @@ def foo() -> tuple[int, int, int,]: +) +)): return 2 -- -- + + # Return type with commas -def foo() -> tuple[int, int, int]: -- return 2 -- -- - def foo() -> ( ++def foo() -> ( ++ tuple[int, int, int] ++): + return 2 + + +-def foo() -> ( - tuple[ - loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - ] -+ tuple[int, int, int] - ): - return 2 -- -- +-): +def foo() -> tuple[loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong]: -+ return 2 + return 2 + + # Magic trailing comma example -def foo() -> ( - tuple[ @@ -273,49 +272,77 @@ def foo() -> tuple[int, int, int,]: # Control def double(a: int) -> int: return 2*a + + # Remove the brackets def double(a: int) -> (int): return 2*a + + # Some newline variations def double(a: int) -> ( int): return 2*a + + def double(a: int) -> (int ): return 2*a + + def double(a: int) -> ( int ): return 2*a + + # Don't lose the comments def double(a: int) -> ( # Hello int ): return 2*a + + def double(a: int) -> ( int # Hello ): return 2*a + + # Really long annotations def foo() -> ( intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds ): return 2 + + def foo() -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 + + def foo() -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 + + def foo(a: int, b: int, c: int,) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 + + def foo(a: int, b: int, c: int,) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 + + # Split args but no need to split return def foo(a: int, b: int, c: int,) -> int: return 2 + + # Deeply nested brackets # with *interesting* spacing def double(a: int) -> (((((int))))): return 2*a + + def double(a: int) -> ( ( ( ((int) @@ -324,19 +351,27 @@ def double(a: int) -> ( ) ): return 2*a + + def foo() -> ( ( ( intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds ) )): return 2 + + # Return type with commas def foo() -> ( tuple[int, int, int] ): return 2 + + def foo() -> tuple[loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong]: return 2 + + # Magic trailing comma example def foo() -> tuple[int, int, int,]: return 2 diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap index fbac7ed57e..26cfa4faca 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap @@ -60,7 +60,7 @@ func( ```diff --- Black +++ Ruff -@@ -1,25 +1,40 @@ +@@ -1,25 +1,43 @@ # We should not remove the trailing comma in a single-element subscript. a: tuple[int,] b = tuple[int,] @@ -86,12 +86,6 @@ func( - # Trailing commas in multiple chained non-nested parens. -zero(one).two(three).four(five) -- --func1(arg1).func2(arg2).func3(arg3).func4(arg4).func5(arg5) -- --(a, b, c, d) = func1(arg1) and func2(arg2) -- --func(argument1, (one, two), argument4, argument5, argument6) +zero( + one, +).two( @@ -99,7 +93,11 @@ func( +).four( + five, +) + +-func1(arg1).func2(arg2).func3(arg3).func4(arg4).func5(arg5) +func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5) + +-(a, b, c, d) = func1(arg1) and func2(arg2) +( + a, + b, @@ -108,6 +106,8 @@ func( +) = func1( + arg1 +) and func2(arg2) + +-func(argument1, (one, two), argument4, argument5, argument6) +func( + argument1, + ( @@ -144,7 +144,9 @@ zero( ).four( five, ) + func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5) + ( a, b, @@ -153,6 +155,7 @@ func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5) ) = func1( arg1 ) and func2(arg2) + func( argument1, ( diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap index ee4cb215cc..902e4035e2 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap @@ -76,35 +76,7 @@ x[ ```diff --- Black +++ Ruff -@@ -17,19 +17,14 @@ - slice[not so_simple : 1 < val <= 10] - slice[(1 for i in range(42)) : x] - slice[:: [i for i in range(42)]] -- -- - async def f(): - slice[await x : [i async for i in arange(42)] : 42] -- -- - # These are from PEP-8: - ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] - ham[lower:upper], ham[lower:upper:], ham[lower::step] - # ham[lower+offset : upper+offset] - ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] - ham[lower + offset : upper + offset] -- - slice[::, ::] - slice[ - # A -@@ -46,7 +41,6 @@ - # C - 3 - ] -- - slice[ - # A - 1 -@@ -56,4 +50,8 @@ +@@ -56,4 +56,8 @@ # C 4 ] @@ -138,14 +110,19 @@ slice[1 or 2 : True and False] slice[not so_simple : 1 < val <= 10] slice[(1 for i in range(42)) : x] slice[:: [i for i in range(42)]] + + async def f(): slice[await x : [i async for i in arange(42)] : 42] + + # These are from PEP-8: ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] ham[lower + offset : upper + offset] + slice[::, ::] slice[ # A @@ -162,6 +139,7 @@ slice[ # C 3 ] + slice[ # A 1 diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap index be3dbc6901..07e263b90e 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap @@ -33,7 +33,7 @@ def docstring_multiline(): ```diff --- Black +++ Ruff -@@ -1,19 +1,14 @@ +@@ -1,13 +1,13 @@ #!/usr/bin/env python3 name = "Łukasz" @@ -44,20 +44,14 @@ def docstring_multiline(): +(b"", B"") +(u"", U"") (r"", R"") -- + -(rf"", rf"", Rf"", Rf"", rf"", rf"", Rf"", Rf"") -(rb"", rb"", Rb"", Rb"", rb"", rb"", Rb"", Rb"") -- -- +(rf"", fr"", Rf"", fR"", rF"", Fr"", RF"", FR"") +(rb"", br"", Rb"", bR"", rB"", Br"", RB"", BR"") + + def docstring_singleline(): - R"""2020 was one hell of a year. The good news is that we were able to""" -- -- - def docstring_multiline(): - R""" - clear out all of the issues opened in that time :p ``` ## Ruff Output @@ -70,10 +64,15 @@ name = "Łukasz" (b"", B"") (u"", U"") (r"", R"") + (rf"", fr"", Rf"", fR"", rF"", Fr"", RF"", FR"") (rb"", br"", Rb"", bR"", rB"", Br"", RB"", BR"") + + def docstring_singleline(): R"""2020 was one hell of a year. The good news is that we were able to""" + + def docstring_multiline(): R""" clear out all of the issues opened in that time :p diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap index 304ee0a3cb..95a7d91907 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap @@ -42,15 +42,15 @@ assert ( ```diff --- Black +++ Ruff -@@ -1,58 +1,25 @@ +@@ -1,58 +1,33 @@ importA -( - () - << 0 - ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 -) # -- +() << 0 ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 # + assert sort_by_dependency( { - "1": {"2", "3"}, @@ -64,13 +64,13 @@ assert ( + "2a": set(), "2b": set(), "3a": set(), "3b": set() } ) == ["2a", "2b", "2", "3a", "3b", "3", "1"] -- + importA 0 -0 ^ 0 # -- -- +0^0 # + + class A: def foo(self): for _ in range(10): @@ -78,9 +78,9 @@ assert ( + aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc( # pylint: disable=no-member xxxxxxxxxxxx - ) # pylint: disable=no-member -- -- + ) + + def test(self, othr): - return 1 == 2 and ( - name, @@ -101,15 +101,15 @@ assert ( - othr.meta_data, - othr.schedule, - ) -- -- ++ return (1 == 2 and ++ (name, description, self.default, self.selected, self.auto_generated, self.parameters, self.meta_data, self.schedule) == ++ (name, description, othr.default, othr.selected, othr.auto_generated, othr.parameters, othr.meta_data, othr.schedule)) + + -assert a_function( - very_long_arguments_that_surpass_the_limit, - which_is_eighty_eight_in_this_case_plus_a_bit_more, -) == {"x": "this need to pass the line limit as well", "b": "but only by a little bit"} -+ return (1 == 2 and -+ (name, description, self.default, self.selected, self.auto_generated, self.parameters, self.meta_data, self.schedule) == -+ (name, description, othr.default, othr.selected, othr.auto_generated, othr.parameters, othr.meta_data, othr.schedule)) +assert ( + a_function(very_long_arguments_that_surpass_the_limit, which_is_eighty_eight_in_this_case_plus_a_bit_more) + == {"x": "this need to pass the line limit as well", "b": "but only by a little bit"} @@ -121,25 +121,33 @@ assert ( ```py importA () << 0 ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 # + assert sort_by_dependency( { "1": {"2", "3"}, "2": {"2a", "2b"}, "3": {"3a", "3b"}, "2a": set(), "2b": set(), "3a": set(), "3b": set() } ) == ["2a", "2b", "2", "3a", "3b", "3", "1"] + importA 0 0^0 # + + class A: def foo(self): for _ in range(10): aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc( # pylint: disable=no-member xxxxxxxxxxxx ) + + def test(self, othr): return (1 == 2 and (name, description, self.default, self.selected, self.auto_generated, self.parameters, self.meta_data, self.schedule) == (name, description, othr.default, othr.selected, othr.auto_generated, othr.parameters, othr.meta_data, othr.schedule)) + + assert ( a_function(very_long_arguments_that_surpass_the_limit, which_is_eighty_eight_in_this_case_plus_a_bit_more) == {"x": "this need to pass the line limit as well", "b": "but only by a little bit"} diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap index 7fea6d8abe..a6989d984a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap @@ -38,7 +38,7 @@ class A: ```diff --- Black +++ Ruff -@@ -1,29 +1,17 @@ +@@ -1,18 +1,11 @@ -if e1234123412341234.winerror not in ( - _winapi.ERROR_SEM_TIMEOUT, - _winapi.ERROR_PIPE_BUSY, @@ -46,7 +46,7 @@ class A: +if e1234123412341234.winerror not in (_winapi.ERROR_SEM_TIMEOUT, + _winapi.ERROR_PIPE_BUSY) or _check_timeout(t): pass -- + if x: if y: - new_id = ( @@ -56,23 +56,20 @@ class A: - ) - + 1 - ) -- -- + new_id = max(Vegetable.objects.order_by('-id')[0].id, + Mineral.objects.order_by('-id')[0].id) + 1 + + class X: - def get_help_text(self): - return ngettext( +@@ -21,7 +14,7 @@ "Your password must contain at least %(min_length)d character.", "Your password must contain at least %(min_length)d characters.", self.min_length, - ) % {"min_length": self.min_length} -- -- + ) % {'min_length': self.min_length} + + class A: - def b(self): - if self.connection.mysql_is_mariadb and ( ``` ## Ruff Output @@ -81,10 +78,13 @@ class A: if e1234123412341234.winerror not in (_winapi.ERROR_SEM_TIMEOUT, _winapi.ERROR_PIPE_BUSY) or _check_timeout(t): pass + if x: if y: new_id = max(Vegetable.objects.order_by('-id')[0].id, Mineral.objects.order_by('-id')[0].id) + 1 + + class X: def get_help_text(self): return ngettext( @@ -92,6 +92,8 @@ class X: "Your password must contain at least %(min_length)d characters.", self.min_length, ) % {'min_length': self.min_length} + + class A: def b(self): if self.connection.mysql_is_mariadb and ( diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap index 73853e4f0e..eea12aab00 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap @@ -46,7 +46,7 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( ```diff --- Black +++ Ruff -@@ -1,30 +1,8 @@ +@@ -1,28 +1,10 @@ -zero( - one, -).two( @@ -60,12 +60,12 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( -).func3(arg3).func4( - arg4, -).func5(arg5) -- +zero(one,).two(three,).four(five,) + +func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5) # Inner one-element tuple shouldn't explode func1(arg1).func2(arg1, (one_tuple,)).func3(arg3) -- + -( - a, - b, @@ -74,21 +74,11 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( -) = func1( - arg1 -) and func2(arg2) -- -- +(a, b, c, d,) = func1(arg1) and func2(arg2) + + # Example from https://github.com/psf/black/issues/3229 - def refresh_token(self, device_family, refresh_token, api_key): - return self.orchestration.refresh_token( -@@ -33,15 +11,12 @@ - }, - api_key=api_key, - )["extensions"]["sdk"]["token"] -- -- - # Edge case where a bug in a working-in-progress version of - # https://github.com/psf/black/pull/3370 causes an infinite recursion. - assert ( +@@ -41,7 +23,6 @@ long_module.long_class.long_func().another_func() == long_module.long_class.long_func()["some_key"].another_func(arg1) ) @@ -102,10 +92,14 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( ```py zero(one,).two(three,).four(five,) + func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5) # Inner one-element tuple shouldn't explode func1(arg1).func2(arg1, (one_tuple,)).func3(arg3) + (a, b, c, d,) = func1(arg1) and func2(arg2) + + # Example from https://github.com/psf/black/issues/3229 def refresh_token(self, device_family, refresh_token, api_key): return self.orchestration.refresh_token( @@ -114,6 +108,8 @@ def refresh_token(self, device_family, refresh_token, api_key): }, api_key=api_key, )["extensions"]["sdk"]["token"] + + # Edge case where a bug in a working-in-progress version of # https://github.com/psf/black/pull/3370 causes an infinite recursion. assert ( diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tricky_unicode_symbols_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tricky_unicode_symbols_py.snap deleted file mode 100644 index 01c1dbbadd..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tricky_unicode_symbols_py.snap +++ /dev/null @@ -1,61 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/tricky_unicode_symbols.py ---- -## Input - -```py -ä = 1 -µ = 2 -蟒 = 3 -x󠄀 = 4 -មុ = 1 -Q̇_per_meter = 4 - -A᧚ = 3 -A፩ = 8 -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -4,6 +4,5 @@ - x󠄀 = 4 - មុ = 1 - Q̇_per_meter = 4 -- - A᧚ = 3 - A፩ = 8 -``` - -## Ruff Output - -```py -ä = 1 -µ = 2 -蟒 = 3 -x󠄀 = 4 -មុ = 1 -Q̇_per_meter = 4 -A᧚ = 3 -A፩ = 8 -``` - -## Black Output - -```py -ä = 1 -µ = 2 -蟒 = 3 -x󠄀 = 4 -មុ = 1 -Q̇_per_meter = 4 - -A᧚ = 3 -A፩ = 8 -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap index a1a1c9cf2f..084d542640 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap @@ -20,7 +20,7 @@ this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") ```diff --- Black +++ Ruff -@@ -1,12 +1,5 @@ +@@ -1,12 +1,6 @@ # This is a standalone comment. -( - sdfjklsdfsjldkflkjsf, @@ -32,8 +32,8 @@ this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") +sdfjklsdfsjldkflkjsf, sdfjsdfjlksdljkfsdlkf, sdfsdjfklsdfjlksdljkf, sdsfsdfjskdflsfsdf = 1, 2, 3 # This is as well. -(this_will_be_wrapped_in_parens,) = struct.unpack(b"12345678901234567890") -- +this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") + (a,) = call() ``` @@ -44,6 +44,7 @@ this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") sdfjklsdfsjldkflkjsf, sdfjsdfjlksdljkfsdlkf, sdfsdjfklsdfjlksdljkf, sdsfsdfjskdflsfsdf = 1, 2, 3 # This is as well. this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") + (a,) = call() ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__while_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__while_py.snap index 03203dfbc6..9ef6ca4e90 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__while_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__while_py.snap @@ -51,6 +51,8 @@ else: # trailing else comment pass # trailing else body comment + + while ( aVeryLongConditionThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn ): # trailing comment @@ -58,8 +60,11 @@ while ( else: ... + while some_condition(unformatted, args) and anotherCondition or aThirdCondition: # comment print("Do something") + + while ( some_condition(unformatted, args) # trailing some condition and anotherCondition or aThirdCondition # trailing third condition From 209aaa5addb98c3054f845204e64dc2c9d88aa34 Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 5 Jun 2023 11:38:08 +0200 Subject: [PATCH 23/41] Ensure type_ignores for Module are empty (#4861) According to https://docs.python.org/3/library/ast.html#ast-helpers, we expect type_ignores to be always be empty, so this adds a debug assert. Test plan: I confirmed that the assertion holdes for the file below and for all the black tests which include a number of `type: ignore` comments. ```python # type: ignore if 1: print("1") # type: ignore # elsebranch # type: ignore else: # type: ignore print("2") # type: ignore while 1: print() # type: ignore ``` --- crates/ruff_python_formatter/src/module/mod_module.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/ruff_python_formatter/src/module/mod_module.rs b/crates/ruff_python_formatter/src/module/mod_module.rs index fe21ed65fa..0d0fee7c00 100644 --- a/crates/ruff_python_formatter/src/module/mod_module.rs +++ b/crates/ruff_python_formatter/src/module/mod_module.rs @@ -9,10 +9,17 @@ pub struct FormatModModule; impl FormatNodeRule for FormatModModule { fn fmt_fields(&self, item: &ModModule, f: &mut PyFormatter) -> FormatResult<()> { + let ModModule { + range: _, + body, + type_ignores, + } = item; + // https://docs.python.org/3/library/ast.html#ast-helpers + debug_assert!(type_ignores.is_empty()); write!( f, [ - item.body.format().with_options(SuiteLevel::TopLevel), + body.format().with_options(SuiteLevel::TopLevel), // Trailing newline at the end of the file hard_line_break() ] From dc223fd3cab5b7cec1fb3334ba3be8a98b608bfa Mon Sep 17 00:00:00 2001 From: Allison Karlitskaya Date: Mon, 5 Jun 2023 18:44:49 +0200 Subject: [PATCH 24/41] Add some exceptions for FBT003 (#3247) (#4867) --- .../resources/test/fixtures/flake8_boolean_trap/FBT.py | 4 ++++ crates/ruff/src/rules/flake8_boolean_trap/helpers.rs | 3 +++ ..._rules__flake8_boolean_trap__tests__FBT001_FBT.py.snap | 8 ++++---- ..._rules__flake8_boolean_trap__tests__FBT003_FBT.py.snap | 8 ++++++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_boolean_trap/FBT.py b/crates/ruff/resources/test/fixtures/flake8_boolean_trap/FBT.py index eacf248284..581668e932 100644 --- a/crates/ruff/resources/test/fixtures/flake8_boolean_trap/FBT.py +++ b/crates/ruff/resources/test/fixtures/flake8_boolean_trap/FBT.py @@ -57,12 +57,16 @@ dict.fromkeys(("world",), True) {}.deploy(True, False) getattr(someobj, attrname, False) mylist.index(True) +bool(False) int(True) str(int(False)) cfg.get("hello", True) cfg.getint("hello", True) cfg.getfloat("hello", True) cfg.getboolean("hello", True) +os.set_blocking(0, False) +g_action.set_enabled(True) +settings.set_enable_developer_extras(True) class Registry: diff --git a/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs b/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs index 963fd62b89..2c397470b7 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs @@ -10,6 +10,7 @@ pub(super) const FUNC_CALL_NAME_ALLOWLIST: &[&str] = &[ "assertEquals", "assertNotEqual", "assertNotEquals", + "bool", "bytes", "count", "failIfEqual", @@ -27,6 +28,8 @@ pub(super) const FUNC_CALL_NAME_ALLOWLIST: &[&str] = &[ "param", "pop", "remove", + "set_blocking", + "set_enabled", "setattr", "__setattr__", "setdefault", diff --git a/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT001_FBT.py.snap b/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT001_FBT.py.snap index c940e93364..4f54d13871 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT001_FBT.py.snap +++ b/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT001_FBT.py.snap @@ -81,12 +81,12 @@ FBT.py:19:5: FBT001 Boolean positional arg in function definition 23 | kwonly_nonvalued_nohint, | -FBT.py:81:19: FBT001 Boolean positional arg in function definition +FBT.py:85:19: FBT001 Boolean positional arg in function definition | -81 | # FBT001: Boolean positional arg in function definition -82 | def foo(self, value: bool) -> None: +85 | # FBT001: Boolean positional arg in function definition +86 | def foo(self, value: bool) -> None: | ^^^^^^^^^^^ FBT001 -83 | pass +87 | pass | diff --git a/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT003_FBT.py.snap b/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT003_FBT.py.snap index 43058488d3..1aeb008182 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT003_FBT.py.snap +++ b/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT003_FBT.py.snap @@ -28,4 +28,12 @@ FBT.py:57:17: FBT003 Boolean positional value in function call 61 | mylist.index(True) | +FBT.py:69:38: FBT003 Boolean positional value in function call + | +69 | os.set_blocking(0, False) +70 | g_action.set_enabled(True) +71 | settings.set_enable_developer_extras(True) + | ^^^^ FBT003 + | + From f952bef1adea1a8c6d894ba2b14ddbe6fba683cd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 Jun 2023 12:55:28 -0400 Subject: [PATCH 25/41] Mark F523 as "sometimes" fixable (#4868) --- .../resources/test/fixtures/pyflakes/F523.py | 4 ++++ crates/ruff/src/rules/pyflakes/rules/strings.rs | 12 ++++++++---- ...f__rules__pyflakes__tests__F523_F523.py.snap | 17 +++++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F523.py b/crates/ruff/resources/test/fixtures/pyflakes/F523.py index 33283dbd48..6b2985924b 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F523.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F523.py @@ -26,3 +26,7 @@ # With modified indexes "{1}{2}".format(1, 2, 3) # F523, # F524 "{1}{3}".format(1, 2, 3, 4) # F523, # F524 + +# Not fixable +('' +.format(2)) diff --git a/crates/ruff/src/rules/pyflakes/rules/strings.rs b/crates/ruff/src/rules/pyflakes/rules/strings.rs index 7ff92c6867..7771575019 100644 --- a/crates/ruff/src/rules/pyflakes/rules/strings.rs +++ b/crates/ruff/src/rules/pyflakes/rules/strings.rs @@ -4,7 +4,7 @@ use ruff_text_size::TextRange; use rustc_hash::FxHashSet; use rustpython_parser::ast::{self, Constant, Expr, Identifier, Keyword}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Violation}; +use ruff_diagnostics::{AlwaysAutofixableViolation, AutofixKind, Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; @@ -425,7 +425,9 @@ pub struct StringDotFormatExtraPositionalArguments { missing: Vec, } -impl AlwaysAutofixableViolation for StringDotFormatExtraPositionalArguments { +impl Violation for StringDotFormatExtraPositionalArguments { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { let StringDotFormatExtraPositionalArguments { missing } = self; @@ -433,10 +435,12 @@ impl AlwaysAutofixableViolation for StringDotFormatExtraPositionalArguments { format!("`.format` call has unused arguments at position(s): {message}") } - fn autofix_title(&self) -> String { + fn autofix_title(&self) -> Option { let StringDotFormatExtraPositionalArguments { missing } = self; let message = missing.join(", "); - format!("Remove extra positional arguments at position(s): {message}") + Some(format!( + "Remove extra positional arguments at position(s): {message}" + )) } } diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap index 1ec240a042..2b71c0be4e 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap @@ -261,6 +261,8 @@ F523.py:27:1: F523 [*] `.format` call has unused arguments at position(s): 0 27 |-"{1}{2}".format(1, 2, 3) # F523, # F524 27 |+"{0}{1}".format(2, 3) # F523, # F524 28 28 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 +29 29 | +30 30 | # Not fixable F523.py:28:1: F523 [*] `.format` call has unused arguments at position(s): 0, 2 | @@ -268,6 +270,8 @@ F523.py:28:1: F523 [*] `.format` call has unused arguments at position(s): 0, 2 29 | "{1}{2}".format(1, 2, 3) # F523, # F524 30 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 +31 | +32 | # Not fixable | = help: Remove extra positional arguments at position(s): 0, 2 @@ -277,5 +281,18 @@ F523.py:28:1: F523 [*] `.format` call has unused arguments at position(s): 0, 2 27 27 | "{1}{2}".format(1, 2, 3) # F523, # F524 28 |-"{1}{3}".format(1, 2, 3, 4) # F523, # F524 28 |+"{0}{1}".format(2, 4) # F523, # F524 +29 29 | +30 30 | # Not fixable +31 31 | ('' + +F523.py:31:2: F523 `.format` call has unused arguments at position(s): 0 + | +31 | # Not fixable +32 | ('' + | __^ +33 | | .format(2)) + | |__________^ F523 + | + = help: Remove extra positional arguments at position(s): 0 From e6b00f0c4e41885f8be13eeb80069036077cc418 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 Jun 2023 13:32:06 -0400 Subject: [PATCH 26/41] Avoid running RUF100 rules when code contains syntax errors (#4869) --- crates/ruff/src/checkers/noqa.rs | 5 ++--- crates/ruff/src/linter.rs | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ruff/src/checkers/noqa.rs b/crates/ruff/src/checkers/noqa.rs index f54b937d32..52d47182b5 100644 --- a/crates/ruff/src/checkers/noqa.rs +++ b/crates/ruff/src/checkers/noqa.rs @@ -18,10 +18,9 @@ pub(crate) fn check_noqa( locator: &Locator, comment_ranges: &[TextRange], noqa_line_for: &NoqaMapping, + analyze_directives: bool, settings: &Settings, ) -> Vec { - let enforce_noqa = settings.rules.enabled(Rule::UnusedNOQA); - // Identify any codes that are globally exempted (within the current file). let exemption = noqa::file_exemption(locator.contents(), comment_ranges); @@ -93,7 +92,7 @@ pub(crate) fn check_noqa( } // Enforce that the noqa directive was actually used (RUF100). - if enforce_noqa { + if analyze_directives && settings.rules.enabled(Rule::UnusedNOQA) { for line in noqa_directives.lines() { match &line.directive { Directive::All(leading_spaces, noqa_range, trailing_spaces) => { diff --git a/crates/ruff/src/linter.rs b/crates/ruff/src/linter.rs index 79ed8fba70..bd8f713d87 100644 --- a/crates/ruff/src/linter.rs +++ b/crates/ruff/src/linter.rs @@ -214,6 +214,7 @@ pub fn check_path( locator, indexer.comment_ranges(), &directives.noqa_line_for, + error.is_none(), settings, ); if noqa.into() { From 72245960a1c61ab999b6cfa88702c43fd8278050 Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Mon, 5 Jun 2023 10:54:15 -0700 Subject: [PATCH 27/41] implement E307 for pylint invalid str return type (#4854) --- .../pylint/invalid_return_type_str.py | 28 +++++++ crates/ruff/src/checkers/ast/mod.rs | 4 + crates/ruff/src/codes.rs | 1 + .../flake8_annotations/rules/definition.rs | 14 ++-- crates/ruff/src/rules/pylint/mod.rs | 1 + .../rules/pylint/rules/invalid_str_return.rs | 75 +++++++++++++++++++ crates/ruff/src/rules/pylint/rules/mod.rs | 2 + ...s__PLE0307_invalid_return_type_str.py.snap | 44 +++++++++++ crates/ruff_python_ast/src/helpers.rs | 7 +- ruff.schema.json | 1 + 10 files changed, 166 insertions(+), 11 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/pylint/invalid_return_type_str.py create mode 100644 crates/ruff/src/rules/pylint/rules/invalid_str_return.rs create mode 100644 crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE0307_invalid_return_type_str.py.snap diff --git a/crates/ruff/resources/test/fixtures/pylint/invalid_return_type_str.py b/crates/ruff/resources/test/fixtures/pylint/invalid_return_type_str.py new file mode 100644 index 0000000000..a47ed1b306 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/invalid_return_type_str.py @@ -0,0 +1,28 @@ +class Str: + def __str__(self): + return 1 + +class Float: + def __str__(self): + return 3.05 + +class Int: + def __str__(self): + return 0 + +class Bool: + def __str__(self): + return False + +class Str2: + def __str__(self): + x = "ruff" + return x + +# TODO fixme once Ruff has better type checking +def return_int(): + return 3 + +class ComplexReturn: + def __str__(self): + return return_int() \ No newline at end of file diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 7a15483683..fd2b99e1b3 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -382,6 +382,10 @@ where } } + if self.enabled(Rule::InvalidStrReturnType) { + pylint::rules::invalid_str_return(self, name, body); + } + if self.enabled(Rule::InvalidFunctionName) { if let Some(diagnostic) = pep8_naming::rules::invalid_function_name( stmt, diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index e5ab9ae3f1..211a9b215d 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -167,6 +167,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "E0118") => (RuleGroup::Unspecified, rules::pylint::rules::LoadBeforeGlobalDeclaration), (Pylint, "E0241") => (RuleGroup::Unspecified, rules::pylint::rules::DuplicateBases), (Pylint, "E0302") => (RuleGroup::Unspecified, rules::pylint::rules::UnexpectedSpecialMethodSignature), + (Pylint, "E0307") => (RuleGroup::Unspecified, rules::pylint::rules::InvalidStrReturnType), (Pylint, "E0604") => (RuleGroup::Unspecified, rules::pylint::rules::InvalidAllObject), (Pylint, "E0605") => (RuleGroup::Unspecified, rules::pylint::rules::InvalidAllFormat), (Pylint, "E1142") => (RuleGroup::Unspecified, rules::pylint::rules::AwaitOutsideAsync), diff --git a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs index 11bfd64781..492be79613 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs @@ -418,12 +418,14 @@ impl Violation for AnyType { fn is_none_returning(body: &[Stmt]) -> bool { let mut visitor = ReturnStatementVisitor::default(); visitor.visit_body(body); - for expr in visitor.returns.into_iter().flatten() { - if !matches!( - expr, - Expr::Constant(ref constant) if constant.value.is_none() - ) { - return false; + for stmt in visitor.returns { + if let Some(value) = stmt.value.as_deref() { + if !matches!( + value, + Expr::Constant(constant) if constant.value.is_none() + ) { + return false; + } } } true diff --git a/crates/ruff/src/rules/pylint/mod.rs b/crates/ruff/src/rules/pylint/mod.rs index 29145a3b35..fc7fb547ad 100644 --- a/crates/ruff/src/rules/pylint/mod.rs +++ b/crates/ruff/src/rules/pylint/mod.rs @@ -52,6 +52,7 @@ mod tests { #[test_case(Rule::ImportSelf, Path::new("import_self/module.py"))] #[test_case(Rule::InvalidAllFormat, Path::new("invalid_all_format.py"))] #[test_case(Rule::InvalidAllObject, Path::new("invalid_all_object.py"))] + #[test_case(Rule::InvalidStrReturnType, Path::new("invalid_return_type_str.py"))] #[test_case(Rule::DuplicateBases, Path::new("duplicate_bases.py"))] #[test_case(Rule::DuplicateValue, Path::new("duplicate_value.py"))] #[test_case(Rule::InvalidCharacterBackspace, Path::new("invalid_characters.py"))] diff --git a/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs b/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs new file mode 100644 index 0000000000..eeaeb44d2e --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs @@ -0,0 +1,75 @@ +use rustpython_parser::ast::{self, Constant, Expr, Ranged, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::{helpers::ReturnStatementVisitor, statement_visitor::StatementVisitor}; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for `__str__` implementations that return a type other than `str`. +/// +/// ## Why is this bad? +/// The `__str__` method should return a `str` object. Returning a different +/// type may cause unexpected behavior. +#[violation] +pub struct InvalidStrReturnType; + +impl Violation for InvalidStrReturnType { + #[derive_message_formats] + fn message(&self) -> String { + format!("`__str__` does not return `str`") + } +} + +/// E0307 +pub(crate) fn invalid_str_return(checker: &mut Checker, name: &str, body: &[Stmt]) { + if name != "__str__" { + return; + } + + if !checker.semantic_model().scope().kind.is_class() { + return; + } + + let returns = { + let mut visitor = ReturnStatementVisitor::default(); + visitor.visit_body(body); + visitor.returns + }; + + for stmt in returns { + // Disallow implicit `None`. + let Some(value) = stmt.value.as_deref() else { + checker.diagnostics.push(Diagnostic::new(InvalidStrReturnType, stmt.range())); + continue; + }; + + // Disallow other constants. + if matches!( + value, + Expr::List(_) + | Expr::Dict(_) + | Expr::Set(_) + | Expr::ListComp(_) + | Expr::DictComp(_) + | Expr::SetComp(_) + | Expr::GeneratorExp(_) + | Expr::Constant(ast::ExprConstant { + value: Constant::None + | Constant::Bool(_) + | Constant::Bytes(_) + | Constant::Int(_) + | Constant::Tuple(_) + | Constant::Float(_) + | Constant::Complex { .. } + | Constant::Ellipsis, + .. + }) + ) { + checker + .diagnostics + .push(Diagnostic::new(InvalidStrReturnType, value.range())); + } + } +} diff --git a/crates/ruff/src/rules/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index 5a26cdb2eb..a8c45ec0c8 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -17,6 +17,7 @@ pub(crate) use invalid_all_format::{invalid_all_format, InvalidAllFormat}; pub(crate) use invalid_all_object::{invalid_all_object, InvalidAllObject}; pub(crate) use invalid_envvar_default::{invalid_envvar_default, InvalidEnvvarDefault}; pub(crate) use invalid_envvar_value::{invalid_envvar_value, InvalidEnvvarValue}; +pub(crate) use invalid_str_return::{invalid_str_return, InvalidStrReturnType}; pub(crate) use invalid_string_characters::{ invalid_string_characters, InvalidCharacterBackspace, InvalidCharacterEsc, InvalidCharacterNul, InvalidCharacterSub, InvalidCharacterZeroWidthSpace, @@ -73,6 +74,7 @@ mod invalid_all_format; mod invalid_all_object; mod invalid_envvar_default; mod invalid_envvar_value; +mod invalid_str_return; mod invalid_string_characters; mod iteration_over_set; mod load_before_global_declaration; diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE0307_invalid_return_type_str.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE0307_invalid_return_type_str.py.snap new file mode 100644 index 0000000000..69d872d03e --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE0307_invalid_return_type_str.py.snap @@ -0,0 +1,44 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +invalid_return_type_str.py:3:16: PLE0307 `__str__` does not return `str` + | +3 | class Str: +4 | def __str__(self): +5 | return 1 + | ^ PLE0307 +6 | +7 | class Float: + | + +invalid_return_type_str.py:7:16: PLE0307 `__str__` does not return `str` + | + 7 | class Float: + 8 | def __str__(self): + 9 | return 3.05 + | ^^^^ PLE0307 +10 | +11 | class Int: + | + +invalid_return_type_str.py:11:16: PLE0307 `__str__` does not return `str` + | +11 | class Int: +12 | def __str__(self): +13 | return 0 + | ^ PLE0307 +14 | +15 | class Bool: + | + +invalid_return_type_str.py:15:16: PLE0307 `__str__` does not return `str` + | +15 | class Bool: +16 | def __str__(self): +17 | return False + | ^^^^^ PLE0307 +18 | +19 | class Str2: + | + + diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 6b873603ff..c74d96ac12 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -907,7 +907,7 @@ pub fn resolve_imported_module_path<'a>( /// A [`StatementVisitor`] that collects all `return` statements in a function or method. #[derive(Default)] pub struct ReturnStatementVisitor<'a> { - pub returns: Vec>, + pub returns: Vec<&'a ast::StmtReturn>, } impl<'a, 'b> StatementVisitor<'b> for ReturnStatementVisitor<'a> @@ -919,10 +919,7 @@ where Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_) => { // Don't recurse. } - Stmt::Return(ast::StmtReturn { - value, - range: _range, - }) => self.returns.push(value.as_deref()), + Stmt::Return(stmt) => self.returns.push(stmt), _ => walk_stmt(self, stmt), } } diff --git a/ruff.schema.json b/ruff.schema.json index 92f43ef1a3..d5b7fa3b25 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2063,6 +2063,7 @@ "PLE03", "PLE030", "PLE0302", + "PLE0307", "PLE06", "PLE060", "PLE0604", From d31eb87877fc0acd676cb24a9be81d68d36bb1d8 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 Jun 2023 14:23:37 -0400 Subject: [PATCH 28/41] Extract shared simple AST node inference utility (#4871) --- .../pylint/rules/bad_string_format_type.rs | 127 ++++++------------ .../rules/pylint/rules/invalid_str_return.rs | 45 +++---- .../ruff_python_semantic/src/analyze/mod.rs | 1 + .../src/analyze/type_inference.rs | 96 +++++++++++++ .../src/analyze/typing.rs | 2 + 5 files changed, 157 insertions(+), 114 deletions(-) create mode 100644 crates/ruff_python_semantic/src/analyze/type_inference.rs diff --git a/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs b/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs index cbb8b10b4e..137b346cda 100644 --- a/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs +++ b/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs @@ -3,12 +3,13 @@ use std::str::FromStr; use ruff_text_size::TextRange; use rustc_hash::FxHashMap; use rustpython_format::cformat::{CFormatPart, CFormatSpec, CFormatStrOrBytes, CFormatString}; -use rustpython_parser::ast::{self, Constant, Expr, Operator, Ranged}; +use rustpython_parser::ast::{self, Constant, Expr, Ranged}; use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::str::{leading_quote, trailing_quote}; +use ruff_python_semantic::analyze::type_inference::PythonType; use crate::checkers::ast::Checker; @@ -38,87 +39,6 @@ impl Violation for BadStringFormatType { } } -#[derive(Debug, Copy, Clone)] -enum DataType { - String, - Integer, - Float, - Object, - Unknown, -} - -impl From<&Expr> for DataType { - fn from(expr: &Expr) -> Self { - match expr { - Expr::NamedExpr(ast::ExprNamedExpr { value, .. }) => (&**value).into(), - Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => (&**operand).into(), - Expr::Dict(_) => DataType::Object, - Expr::Set(_) => DataType::Object, - Expr::ListComp(_) => DataType::Object, - Expr::SetComp(_) => DataType::Object, - Expr::DictComp(_) => DataType::Object, - Expr::GeneratorExp(_) => DataType::Object, - Expr::JoinedStr(_) => DataType::String, - Expr::BinOp(ast::ExprBinOp { left, op, .. }) => { - // Ex) "a" % "b" - if matches!( - left.as_ref(), - Expr::Constant(ast::ExprConstant { - value: Constant::Str(..), - .. - }) - ) && matches!(op, Operator::Mod) - { - return DataType::String; - } - DataType::Unknown - } - Expr::Constant(ast::ExprConstant { value, .. }) => match value { - Constant::Str(_) => DataType::String, - Constant::Int(_) => DataType::Integer, - Constant::Float(_) => DataType::Float, - _ => DataType::Unknown, - }, - Expr::List(_) => DataType::Object, - Expr::Tuple(_) => DataType::Object, - _ => DataType::Unknown, - } - } -} - -impl DataType { - fn is_compatible_with(self, format: FormatType) -> bool { - match self { - DataType::String => matches!( - format, - FormatType::Unknown | FormatType::String | FormatType::Repr - ), - DataType::Object => matches!( - format, - FormatType::Unknown | FormatType::String | FormatType::Repr - ), - DataType::Integer => matches!( - format, - FormatType::Unknown - | FormatType::String - | FormatType::Repr - | FormatType::Integer - | FormatType::Float - | FormatType::Number - ), - DataType::Float => matches!( - format, - FormatType::Unknown - | FormatType::String - | FormatType::Repr - | FormatType::Float - | FormatType::Number - ), - DataType::Unknown => true, - } - } -} - #[derive(Debug, Copy, Clone)] enum FormatType { Repr, @@ -129,6 +49,45 @@ enum FormatType { Unknown, } +impl FormatType { + fn is_compatible_with(self, data_type: PythonType) -> bool { + match data_type { + PythonType::String + | PythonType::Bytes + | PythonType::List + | PythonType::Dict + | PythonType::Set + | PythonType::Tuple + | PythonType::Generator + | PythonType::Complex + | PythonType::Bool + | PythonType::Ellipsis + | PythonType::None => matches!( + self, + FormatType::Unknown | FormatType::String | FormatType::Repr + ), + PythonType::Integer => matches!( + self, + FormatType::Unknown + | FormatType::String + | FormatType::Repr + | FormatType::Integer + | FormatType::Float + | FormatType::Number + ), + PythonType::Float => matches!( + self, + FormatType::Unknown + | FormatType::String + | FormatType::Repr + | FormatType::Float + | FormatType::Number + ), + PythonType::Unknown => true, + } + } +} + impl From for FormatType { fn from(format: char) -> Self { match format { @@ -159,9 +118,9 @@ fn collect_specs(formats: &[CFormatStrOrBytes]) -> Vec<&CFormatSpec> { /// Return `true` if the format string is equivalent to the constant type fn equivalent(format: &CFormatSpec, value: &Expr) -> bool { - let constant: DataType = value.into(); let format: FormatType = format.format_char.into(); - constant.is_compatible_with(format) + let constant: PythonType = value.into(); + format.is_compatible_with(constant) } /// Return `true` if the [`Constnat`] aligns with the format type. diff --git a/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs b/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs index eeaeb44d2e..486c8a474e 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs @@ -1,8 +1,9 @@ -use rustpython_parser::ast::{self, Constant, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::{helpers::ReturnStatementVisitor, statement_visitor::StatementVisitor}; +use ruff_python_semantic::analyze::type_inference::PythonType; use crate::checkers::ast::Checker; @@ -39,37 +40,21 @@ pub(crate) fn invalid_str_return(checker: &mut Checker, name: &str, body: &[Stmt }; for stmt in returns { - // Disallow implicit `None`. - let Some(value) = stmt.value.as_deref() else { - checker.diagnostics.push(Diagnostic::new(InvalidStrReturnType, stmt.range())); - continue; - }; - - // Disallow other constants. - if matches!( - value, - Expr::List(_) - | Expr::Dict(_) - | Expr::Set(_) - | Expr::ListComp(_) - | Expr::DictComp(_) - | Expr::SetComp(_) - | Expr::GeneratorExp(_) - | Expr::Constant(ast::ExprConstant { - value: Constant::None - | Constant::Bool(_) - | Constant::Bytes(_) - | Constant::Int(_) - | Constant::Tuple(_) - | Constant::Float(_) - | Constant::Complex { .. } - | Constant::Ellipsis, - .. - }) - ) { + if let Some(value) = stmt.value.as_deref() { + // Disallow other, non- + if !matches!( + PythonType::from(value), + PythonType::String | PythonType::Unknown + ) { + checker + .diagnostics + .push(Diagnostic::new(InvalidStrReturnType, value.range())); + } + } else { + // Disallow implicit `None`. checker .diagnostics - .push(Diagnostic::new(InvalidStrReturnType, value.range())); + .push(Diagnostic::new(InvalidStrReturnType, stmt.range())); } } } diff --git a/crates/ruff_python_semantic/src/analyze/mod.rs b/crates/ruff_python_semantic/src/analyze/mod.rs index a4cd2fdf50..f8cb066480 100644 --- a/crates/ruff_python_semantic/src/analyze/mod.rs +++ b/crates/ruff_python_semantic/src/analyze/mod.rs @@ -1,5 +1,6 @@ pub mod branch_detection; pub mod function_type; pub mod logging; +pub mod type_inference; pub mod typing; pub mod visibility; diff --git a/crates/ruff_python_semantic/src/analyze/type_inference.rs b/crates/ruff_python_semantic/src/analyze/type_inference.rs new file mode 100644 index 0000000000..e040fe2805 --- /dev/null +++ b/crates/ruff_python_semantic/src/analyze/type_inference.rs @@ -0,0 +1,96 @@ +//! Analysis rules to perform basic type inference on individual expressions. + +use rustpython_parser::ast; +use rustpython_parser::ast::{Constant, Expr}; + +/// An extremely simple type inference system for individual expressions. +/// +/// This system can only represent and infer the types of simple data types +/// such as strings, integers, floats, and containers. It cannot infer the +/// types of variables or expressions that are not statically known from +/// individual AST nodes alone. +#[derive(Debug, Copy, Clone)] +pub enum PythonType { + /// A string literal, such as `"hello"`. + String, + /// A bytes literal, such as `b"hello"`. + Bytes, + /// An integer literal, such as `1` or `0x1`. + Integer, + /// A floating-point literal, such as `1.0` or `1e10`. + Float, + /// A complex literal, such as `1j` or `1+1j`. + Complex, + /// A boolean literal, such as `True` or `False`. + Bool, + /// A `None` literal, such as `None`. + None, + /// An ellipsis literal, such as `...`. + Ellipsis, + /// A dictionary literal, such as `{}` or `{"a": 1}`. + Dict, + /// A list literal, such as `[]` or `[i for i in range(3)]`. + List, + /// A set literal, such as `set()` or `{i for i in range(3)}`. + Set, + /// A tuple literal, such as `()` or `(1, 2, 3)`. + Tuple, + /// A generator expression, such as `(x for x in range(10))`. + Generator, + /// An unknown type, such as a variable or function call. + Unknown, +} + +impl From<&Expr> for PythonType { + fn from(expr: &Expr) -> Self { + match expr { + Expr::NamedExpr(ast::ExprNamedExpr { value, .. }) => (&**value).into(), + Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => (&**operand).into(), + Expr::Dict(_) => PythonType::Dict, + Expr::DictComp(_) => PythonType::Dict, + Expr::Set(_) => PythonType::Set, + Expr::SetComp(_) => PythonType::Set, + Expr::List(_) => PythonType::List, + Expr::ListComp(_) => PythonType::List, + Expr::Tuple(_) => PythonType::Tuple, + Expr::GeneratorExp(_) => PythonType::Generator, + Expr::JoinedStr(_) => PythonType::String, + Expr::BinOp(ast::ExprBinOp { left, op, .. }) => { + // Ex) "a" % "b" + if op.is_mod() { + if matches!( + left.as_ref(), + Expr::Constant(ast::ExprConstant { + value: Constant::Str(..), + .. + }) + ) { + return PythonType::String; + } + if matches!( + left.as_ref(), + Expr::Constant(ast::ExprConstant { + value: Constant::Bytes(..), + .. + }) + ) { + return PythonType::Bytes; + } + } + PythonType::Unknown + } + Expr::Constant(ast::ExprConstant { value, .. }) => match value { + Constant::Str(_) => PythonType::String, + Constant::Int(_) => PythonType::Integer, + Constant::Float(_) => PythonType::Float, + Constant::Bool(_) => PythonType::Bool, + Constant::Complex { .. } => PythonType::Complex, + Constant::None => PythonType::None, + Constant::Ellipsis => PythonType::Ellipsis, + Constant::Bytes(_) => PythonType::Bytes, + Constant::Tuple(_) => PythonType::Tuple, + }, + _ => PythonType::Unknown, + } + } +} diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 6e3ce7ad19..694e97e650 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -1,3 +1,5 @@ +//! Analysis rules for the `typing` module. + use rustpython_parser::ast::{self, Constant, Expr, Operator}; use num_traits::identities::Zero; From 8a3a269eef85c4a44cbea749686b279e57e62a0f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 Jun 2023 14:31:47 -0400 Subject: [PATCH 29/41] Avoid index-out-of-bands panic for positional placeholders (#4872) --- .../resources/test/fixtures/pyflakes/F523.py | 1 + .../resources/test/fixtures/pyflakes/F524.py | 1 + crates/ruff/src/rules/pyflakes/fixes.rs | 2 +- ..._rules__pyflakes__tests__F523_F523.py.snap | 43 ++++++++++++++----- ..._rules__pyflakes__tests__F524_F524.py.snap | 10 +++++ 5 files changed, 45 insertions(+), 12 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F523.py b/crates/ruff/resources/test/fixtures/pyflakes/F523.py index 6b2985924b..d3dd1b68db 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F523.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F523.py @@ -26,6 +26,7 @@ # With modified indexes "{1}{2}".format(1, 2, 3) # F523, # F524 "{1}{3}".format(1, 2, 3, 4) # F523, # F524 +"{1} {8}".format(0, 1) # F523, # F524 # Not fixable ('' diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F524.py b/crates/ruff/resources/test/fixtures/pyflakes/F524.py index 30cc80ea0e..feffa38af3 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F524.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F524.py @@ -4,3 +4,4 @@ "{0} {bar}".format(1) # F524 "{0} {bar}".format() # F524 "{bar} {0}".format() # F524 +"{1} {8}".format(0, 1) diff --git a/crates/ruff/src/rules/pyflakes/fixes.rs b/crates/ruff/src/rules/pyflakes/fixes.rs index 83cdc6130b..a83d1c8068 100644 --- a/crates/ruff/src/rules/pyflakes/fixes.rs +++ b/crates/ruff/src/rules/pyflakes/fixes.rs @@ -95,7 +95,7 @@ fn update_field_types(format_string: &FormatString, index_map: &[usize]) -> Stri let new_field_name = FieldName::parse(field_name).unwrap(); let mut new_field_name_string = match new_field_name.field_type { FieldType::Auto => String::new(), - FieldType::Index(i) => index_map[i].to_string(), + FieldType::Index(i) => index_map.get(i).unwrap_or(&i).to_string(), FieldType::Keyword(keyword) => keyword, }; for field_name_part in &new_field_name.parts { diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap index 2b71c0be4e..17a1adab1e 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap @@ -251,6 +251,7 @@ F523.py:27:1: F523 [*] `.format` call has unused arguments at position(s): 0 28 | "{1}{2}".format(1, 2, 3) # F523, # F524 | ^^^^^^^^^^^^^^^^^^^^^^^^ F523 29 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 +30 | "{1} {8}".format(0, 1) # F523, # F524 | = help: Remove extra positional arguments at position(s): 0 @@ -261,8 +262,8 @@ F523.py:27:1: F523 [*] `.format` call has unused arguments at position(s): 0 27 |-"{1}{2}".format(1, 2, 3) # F523, # F524 27 |+"{0}{1}".format(2, 3) # F523, # F524 28 28 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 -29 29 | -30 30 | # Not fixable +29 29 | "{1} {8}".format(0, 1) # F523, # F524 +30 30 | F523.py:28:1: F523 [*] `.format` call has unused arguments at position(s): 0, 2 | @@ -270,8 +271,7 @@ F523.py:28:1: F523 [*] `.format` call has unused arguments at position(s): 0, 2 29 | "{1}{2}".format(1, 2, 3) # F523, # F524 30 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 -31 | -32 | # Not fixable +31 | "{1} {8}".format(0, 1) # F523, # F524 | = help: Remove extra positional arguments at position(s): 0, 2 @@ -281,16 +281,37 @@ F523.py:28:1: F523 [*] `.format` call has unused arguments at position(s): 0, 2 27 27 | "{1}{2}".format(1, 2, 3) # F523, # F524 28 |-"{1}{3}".format(1, 2, 3, 4) # F523, # F524 28 |+"{0}{1}".format(2, 4) # F523, # F524 -29 29 | -30 30 | # Not fixable -31 31 | ('' +29 29 | "{1} {8}".format(0, 1) # F523, # F524 +30 30 | +31 31 | # Not fixable -F523.py:31:2: F523 `.format` call has unused arguments at position(s): 0 +F523.py:29:1: F523 [*] `.format` call has unused arguments at position(s): 0 | -31 | # Not fixable -32 | ('' +29 | "{1}{2}".format(1, 2, 3) # F523, # F524 +30 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 +31 | "{1} {8}".format(0, 1) # F523, # F524 + | ^^^^^^^^^^^^^^^^^^^^^^ F523 +32 | +33 | # Not fixable + | + = help: Remove extra positional arguments at position(s): 0 + +ℹ Suggested fix +26 26 | # With modified indexes +27 27 | "{1}{2}".format(1, 2, 3) # F523, # F524 +28 28 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 +29 |-"{1} {8}".format(0, 1) # F523, # F524 + 29 |+"{0} {8}".format(1) # F523, # F524 +30 30 | +31 31 | # Not fixable +32 32 | ('' + +F523.py:32:2: F523 `.format` call has unused arguments at position(s): 0 + | +32 | # Not fixable +33 | ('' | __^ -33 | | .format(2)) +34 | | .format(2)) | |__________^ F523 | = help: Remove extra positional arguments at position(s): 0 diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F524_F524.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F524_F524.py.snap index d4abbf0a61..5d37ffa6ed 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F524_F524.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F524_F524.py.snap @@ -45,6 +45,7 @@ F524.py:5:1: F524 `.format` call is missing argument(s) for placeholder(s): 0, b 7 | "{0} {bar}".format() # F524 | ^^^^^^^^^^^^^^^^^^^^ F524 8 | "{bar} {0}".format() # F524 +9 | "{1} {8}".format(0, 1) | F524.py:6:1: F524 `.format` call is missing argument(s) for placeholder(s): 0, bar @@ -53,6 +54,15 @@ F524.py:6:1: F524 `.format` call is missing argument(s) for placeholder(s): 0, b 7 | "{0} {bar}".format() # F524 8 | "{bar} {0}".format() # F524 | ^^^^^^^^^^^^^^^^^^^^ F524 +9 | "{1} {8}".format(0, 1) + | + +F524.py:7:1: F524 `.format` call is missing argument(s) for placeholder(s): 8 + | +7 | "{0} {bar}".format() # F524 +8 | "{bar} {0}".format() # F524 +9 | "{1} {8}".format(0, 1) + | ^^^^^^^^^^^^^^^^^^^^^^ F524 | From 33434fcb9c40f5d4dd02d313451d351c336c0e6a Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 5 Jun 2023 21:05:42 +0200 Subject: [PATCH 30/41] Add Formatter benchmark (#4860) --- .cargo/config.toml | 2 +- Cargo.lock | 1 + crates/ruff_benchmark/Cargo.toml | 6 ++ crates/ruff_benchmark/benches/formatter.rs | 62 +++++++++++++++++++ .../src/comments/debug.rs | 7 +-- .../ruff_python_formatter/src/comments/mod.rs | 5 +- .../src/comments/visitor.rs | 3 +- 7 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 crates/ruff_benchmark/benches/formatter.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index f88d75d01d..d6ef99b66f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,6 @@ [alias] dev = "run --package ruff_dev --bin ruff_dev" -benchmark = "bench -p ruff_benchmark --" +benchmark = "bench -p ruff_benchmark --bench linter --bench formatter --" [target.'cfg(all())'] rustflags = [ diff --git a/Cargo.lock b/Cargo.lock index 4d3f97ac18..861a87f509 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1796,6 +1796,7 @@ dependencies = [ "once_cell", "ruff", "ruff_python_ast", + "ruff_python_formatter", "rustpython-parser", "serde", "serde_json", diff --git a/crates/ruff_benchmark/Cargo.toml b/crates/ruff_benchmark/Cargo.toml index 21d6f99e05..f3d63336e0 100644 --- a/crates/ruff_benchmark/Cargo.toml +++ b/crates/ruff_benchmark/Cargo.toml @@ -20,6 +20,10 @@ harness = false name = "parser" harness = false +[[bench]] +name = "formatter" +harness = false + [dependencies] once_cell.workspace = true serde.workspace = true @@ -30,6 +34,7 @@ ureq = "2.6.2" [dev-dependencies] ruff.path = "../ruff" ruff_python_ast.path = "../ruff_python_ast" +ruff_python_formatter = { path = "../ruff_python_formatter" } criterion = { version = "0.5.1"} rustpython-parser.workspace = true @@ -38,3 +43,4 @@ mimalloc = "0.1.34" [target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dev-dependencies] tikv-jemallocator = "0.5.0" + diff --git a/crates/ruff_benchmark/benches/formatter.rs b/crates/ruff_benchmark/benches/formatter.rs new file mode 100644 index 0000000000..15698dbba0 --- /dev/null +++ b/crates/ruff_benchmark/benches/formatter.rs @@ -0,0 +1,62 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use ruff_benchmark::{TestCase, TestCaseSpeed, TestFile, TestFileDownloadError}; +use ruff_python_formatter::format_module; +use std::time::Duration; + +#[cfg(target_os = "windows")] +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +#[cfg(all( + not(target_os = "windows"), + not(target_os = "openbsd"), + any( + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "powerpc64" + ) +))] +#[global_allocator] +static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; + +fn create_test_cases() -> Result, TestFileDownloadError> { + Ok(vec![ + TestCase::fast(TestFile::try_download("numpy/globals.py", "https://raw.githubusercontent.com/numpy/numpy/89d64415e349ca75a25250f22b874aa16e5c0973/numpy/_globals.py")?), + TestCase::normal(TestFile::try_download( + "pydantic/types.py", + "https://raw.githubusercontent.com/pydantic/pydantic/83b3c49e99ceb4599d9286a3d793cea44ac36d4b/pydantic/types.py", + )?), + TestCase::normal(TestFile::try_download("numpy/ctypeslib.py", "https://raw.githubusercontent.com/numpy/numpy/e42c9503a14d66adfd41356ef5640c6975c45218/numpy/ctypeslib.py")?), + TestCase::slow(TestFile::try_download( + "large/dataset.py", + "https://raw.githubusercontent.com/DHI/mikeio/b7d26418f4db2909b0aa965253dbe83194d7bb5b/tests/test_dataset.py", + )?), + ]) +} + +fn benchmark_formatter(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("formatter"); + let test_cases = create_test_cases().unwrap(); + + for case in test_cases { + group.throughput(Throughput::Bytes(case.code().len() as u64)); + group.measurement_time(match case.speed() { + TestCaseSpeed::Fast => Duration::from_secs(5), + TestCaseSpeed::Normal => Duration::from_secs(10), + TestCaseSpeed::Slow => Duration::from_secs(20), + }); + + group.bench_with_input( + BenchmarkId::from_parameter(case.name()), + &case, + |b, case| { + b.iter(|| format_module(case.code()).expect("Formatting to succeed")); + }, + ); + } + + group.finish(); +} + +criterion_group!(formatter, benchmark_formatter); +criterion_main!(formatter); diff --git a/crates/ruff_python_formatter/src/comments/debug.rs b/crates/ruff_python_formatter/src/comments/debug.rs index 8583461a44..6e37ac5ff6 100644 --- a/crates/ruff_python_formatter/src/comments/debug.rs +++ b/crates/ruff_python_formatter/src/comments/debug.rs @@ -183,7 +183,6 @@ mod tests { use ruff_python_ast::node::AnyNode; use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::{StmtBreak, StmtContinue}; - use std::cell::Cell; #[test] fn debug() { @@ -210,7 +209,7 @@ break; SourceComment { slice: source_code.slice(TextRange::at(TextSize::new(0), TextSize::new(17))), #[cfg(debug_assertions)] - formatted: Cell::new(false), + formatted: std::cell::Cell::new(false), position: CommentTextPosition::OwnLine, }, ); @@ -220,7 +219,7 @@ break; SourceComment { slice: source_code.slice(TextRange::at(TextSize::new(28), TextSize::new(10))), #[cfg(debug_assertions)] - formatted: Cell::new(false), + formatted: std::cell::Cell::new(false), position: CommentTextPosition::EndOfLine, }, ); @@ -230,7 +229,7 @@ break; SourceComment { slice: source_code.slice(TextRange::at(TextSize::new(39), TextSize::new(15))), #[cfg(debug_assertions)] - formatted: Cell::new(false), + formatted: std::cell::Cell::new(false), position: CommentTextPosition::OwnLine, }, ); diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index 9476ccda45..ae61709e01 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -88,7 +88,6 @@ //! It is possible to add an additional optional label to [`SourceComment`] If ever the need arises to distinguish two *dangling comments* in the formatting logic, use rustpython_parser::ast::Mod; -use std::cell::Cell; use std::fmt::Debug; use std::rc::Rc; @@ -119,7 +118,7 @@ pub(crate) struct SourceComment { /// Whether the comment has been formatted or not. #[cfg(debug_assertions)] - formatted: Cell, + formatted: std::cell::Cell, position: CommentTextPosition, } @@ -137,7 +136,7 @@ impl SourceComment { #[cfg(not(debug_assertions))] #[inline(always)] - pub fn mark_formatted(&self) {} + pub(crate) fn mark_formatted(&self) {} /// Marks the comment as formatted #[cfg(debug_assertions)] diff --git a/crates/ruff_python_formatter/src/comments/visitor.rs b/crates/ruff_python_formatter/src/comments/visitor.rs index 4e2c75ffc6..fa1901a41c 100644 --- a/crates/ruff_python_formatter/src/comments/visitor.rs +++ b/crates/ruff_python_formatter/src/comments/visitor.rs @@ -5,7 +5,6 @@ use ruff_formatter::{SourceCode, SourceCodeSlice}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::prelude::*; use ruff_python_ast::source_code::{CommentRanges, Locator}; -use std::cell::Cell; // The interface is designed to only export the members relevant for iterating nodes in // pre-order. #[allow(clippy::wildcard_imports)] @@ -418,7 +417,7 @@ impl From> for SourceComment { slice: decorated.slice, position: decorated.text_position, #[cfg(debug_assertions)] - formatted: Cell::new(false), + formatted: std::cell::Cell::new(false), } } } From 8938b2d5555a682ef79f1329ca87440e175dd7d2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 Jun 2023 15:06:48 -0400 Subject: [PATCH 31/41] Use `qualified_name` terminology in more structs for consistency (#4873) --- crates/ruff/src/autofix/codemods.rs | 36 +++++------ crates/ruff/src/checkers/ast/mod.rs | 28 +++++---- crates/ruff/src/importer/mod.rs | 6 +- .../runtime_import_in_type_checking_block.rs | 10 +-- .../rules/typing_only_runtime_import.rs | 20 +++--- crates/ruff/src/rules/pandas_vet/helpers.rs | 8 +-- .../pandas_vet/rules/inplace_argument.rs | 2 +- .../src/rules/pyflakes/rules/unused_import.rs | 12 ++-- .../pyupgrade/rules/deprecated_import.rs | 4 +- crates/ruff_python_ast/src/helpers.rs | 12 ++-- crates/ruff_python_semantic/src/binding.rs | 62 ++++++++++--------- crates/ruff_python_semantic/src/model.rs | 21 ++++--- 12 files changed, 116 insertions(+), 105 deletions(-) diff --git a/crates/ruff/src/autofix/codemods.rs b/crates/ruff/src/autofix/codemods.rs index 6d290d81a0..b73b2d5041 100644 --- a/crates/ruff/src/autofix/codemods.rs +++ b/crates/ruff/src/autofix/codemods.rs @@ -57,11 +57,11 @@ pub(crate) fn remove_imports<'a>( // entire statement. let mut found_star = false; for import in imports { - let full_name = match import_body.module.as_ref() { + let qualified_name = match import_body.module.as_ref() { Some(module_name) => format!("{}.*", compose_module_path(module_name)), None => "*".to_string(), }; - if import == full_name { + if import == qualified_name { found_star = true; } else { bail!("Expected \"*\" for unused import (got: \"{}\")", import); @@ -83,26 +83,26 @@ pub(crate) fn remove_imports<'a>( for import in imports { let alias_index = aliases.iter().position(|alias| { - let full_name = match import_module { + let qualified_name = match import_module { Some((relative, module)) => { let module = module.map(compose_module_path); let member = compose_module_path(&alias.name); - let mut full_name = String::with_capacity( + let mut qualified_name = String::with_capacity( relative.len() + module.as_ref().map_or(0, String::len) + member.len() + 1, ); for _ in 0..relative.len() { - full_name.push('.'); + qualified_name.push('.'); } if let Some(module) = module { - full_name.push_str(&module); - full_name.push('.'); + qualified_name.push_str(&module); + qualified_name.push('.'); } - full_name.push_str(&member); - full_name + qualified_name.push_str(&member); + qualified_name } None => compose_module_path(&alias.name), }; - full_name == import + qualified_name == import }); if let Some(index) = alias_index { @@ -170,26 +170,26 @@ pub(crate) fn retain_imports( aliases.retain(|alias| { imports.iter().any(|import| { - let full_name = match import_module { + let qualified_name = match import_module { Some((relative, module)) => { let module = module.map(compose_module_path); let member = compose_module_path(&alias.name); - let mut full_name = String::with_capacity( + let mut qualified_name = String::with_capacity( relative.len() + module.as_ref().map_or(0, String::len) + member.len() + 1, ); for _ in 0..relative.len() { - full_name.push('.'); + qualified_name.push('.'); } if let Some(module) = module { - full_name.push_str(&module); - full_name.push('.'); + qualified_name.push_str(&module); + qualified_name.push('.'); } - full_name.push_str(&member); - full_name + qualified_name.push_str(&member); + qualified_name } None => compose_module_path(&alias.name), }; - full_name == *import + qualified_name == *import }) }); diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index fd2b99e1b3..80066088ee 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -848,23 +848,25 @@ where } } } else if alias.name.contains('.') && alias.asname.is_none() { - // Given `import foo.bar`, `name` would be "foo", and `full_name` would be + // Given `import foo.bar`, `name` would be "foo", and `qualified_name` would be // "foo.bar". let name = alias.name.split('.').next().unwrap(); - let full_name = &alias.name; + let qualified_name = &alias.name; self.add_binding( name, alias.range(), - BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }), + BindingKind::SubmoduleImportation(SubmoduleImportation { + qualified_name, + }), BindingFlags::empty(), ); } else { let name = alias.asname.as_ref().unwrap_or(&alias.name); - let full_name = &alias.name; + let qualified_name = &alias.name; self.add_binding( name, alias.range(), - BindingKind::Importation(Importation { full_name }), + BindingKind::Importation(Importation { qualified_name }), if alias .asname .as_ref() @@ -1150,16 +1152,16 @@ where } } - // Given `from foo import bar`, `name` would be "bar" and `full_name` would + // Given `from foo import bar`, `name` would be "bar" and `qualified_name` would // be "foo.bar". Given `from foo import bar as baz`, `name` would be "baz" - // and `full_name` would be "foo.bar". + // and `qualified_name` would be "foo.bar". let name = alias.asname.as_ref().unwrap_or(&alias.name); - let full_name = + let qualified_name = helpers::format_import_from_member(level, module, &alias.name); self.add_binding( name, alias.range(), - BindingKind::FromImportation(FromImportation { full_name }), + BindingKind::FromImportation(FromImportation { qualified_name }), if alias .asname .as_ref() @@ -1195,12 +1197,12 @@ where } if self.enabled(Rule::UnconventionalImportAlias) { - let full_name = + let qualified_name = helpers::format_import_from_member(level, module, &alias.name); if let Some(diagnostic) = flake8_import_conventions::rules::conventional_import_alias( stmt, - &full_name, + &qualified_name, alias.asname.as_deref(), &self.settings.flake8_import_conventions.aliases, ) @@ -1211,12 +1213,12 @@ where if self.enabled(Rule::BannedImportAlias) { if let Some(asname) = &alias.asname { - let full_name = + let qualified_name = helpers::format_import_from_member(level, module, &alias.name); if let Some(diagnostic) = flake8_import_conventions::rules::banned_import_alias( stmt, - &full_name, + &qualified_name, asname, &self.settings.flake8_import_conventions.banned_aliases, ) diff --git a/crates/ruff/src/importer/mod.rs b/crates/ruff/src/importer/mod.rs index 9df741641f..ba923aed3f 100644 --- a/crates/ruff/src/importer/mod.rs +++ b/crates/ruff/src/importer/mod.rs @@ -88,7 +88,7 @@ impl<'a> Importer<'a> { ) -> Result { // Generate the modified import statement. let content = autofix::codemods::retain_imports( - &[import.full_name], + &[import.qualified_name], import.stmt, self.locator, self.stylist, @@ -120,7 +120,7 @@ impl<'a> Importer<'a> { ) -> Result { // Generate the modified import statement. let content = autofix::codemods::retain_imports( - &[import.full_name], + &[import.qualified_name], import.stmt, self.locator, self.stylist, @@ -447,7 +447,7 @@ pub(crate) struct StmtImport<'a> { /// The import statement. pub(crate) stmt: &'a Stmt, /// The "full name" of the imported module or member. - pub(crate) full_name: &'a str, + pub(crate) qualified_name: &'a str, } /// The result of an [`Importer::get_or_import_symbol`] call. diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index ec51578b43..a6c0497596 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -39,7 +39,7 @@ use crate::registry::AsRule; /// - [PEP 535](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[violation] pub struct RuntimeImportInTypeCheckingBlock { - full_name: String, + qualified_name: String, } impl Violation for RuntimeImportInTypeCheckingBlock { @@ -47,9 +47,9 @@ impl Violation for RuntimeImportInTypeCheckingBlock { #[derive_message_formats] fn message(&self) -> String { - let RuntimeImportInTypeCheckingBlock { full_name } = self; + let RuntimeImportInTypeCheckingBlock { qualified_name } = self; format!( - "Move import `{full_name}` out of type-checking block. Import is used for more than type hinting." + "Move import `{qualified_name}` out of type-checking block. Import is used for more than type hinting." ) } @@ -84,7 +84,7 @@ pub(crate) fn runtime_import_in_type_checking_block( { let mut diagnostic = Diagnostic::new( RuntimeImportInTypeCheckingBlock { - full_name: qualified_name.to_string(), + qualified_name: qualified_name.to_string(), }, binding.range, ); @@ -110,7 +110,7 @@ pub(crate) fn runtime_import_in_type_checking_block( let add_import_edit = checker.importer.runtime_import_edit( &StmtImport { stmt, - full_name: qualified_name, + qualified_name, }, reference.range().start(), )?; diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 2f74e14aee..a9186c91db 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -45,7 +45,7 @@ use crate::rules::isort::{categorize, ImportSection, ImportType}; /// - [PEP 536](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[violation] pub struct TypingOnlyFirstPartyImport { - full_name: String, + qualified_name: String, } impl Violation for TypingOnlyFirstPartyImport { @@ -55,7 +55,7 @@ impl Violation for TypingOnlyFirstPartyImport { fn message(&self) -> String { format!( "Move application import `{}` into a type-checking block", - self.full_name + self.qualified_name ) } @@ -101,7 +101,7 @@ impl Violation for TypingOnlyFirstPartyImport { /// - [PEP 536](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[violation] pub struct TypingOnlyThirdPartyImport { - full_name: String, + qualified_name: String, } impl Violation for TypingOnlyThirdPartyImport { @@ -111,7 +111,7 @@ impl Violation for TypingOnlyThirdPartyImport { fn message(&self) -> String { format!( "Move third-party import `{}` into a type-checking block", - self.full_name + self.qualified_name ) } @@ -157,7 +157,7 @@ impl Violation for TypingOnlyThirdPartyImport { /// - [PEP 536](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[violation] pub struct TypingOnlyStandardLibraryImport { - full_name: String, + qualified_name: String, } impl Violation for TypingOnlyStandardLibraryImport { @@ -167,7 +167,7 @@ impl Violation for TypingOnlyStandardLibraryImport { fn message(&self) -> String { format!( "Move standard library import `{}` into a type-checking block", - self.full_name + self.qualified_name ) } @@ -274,7 +274,7 @@ pub(crate) fn typing_only_runtime_import( ImportSection::Known(ImportType::LocalFolder | ImportType::FirstParty) => { Diagnostic::new( TypingOnlyFirstPartyImport { - full_name: qualified_name.to_string(), + qualified_name: qualified_name.to_string(), }, binding.range, ) @@ -282,14 +282,14 @@ pub(crate) fn typing_only_runtime_import( ImportSection::Known(ImportType::ThirdParty) | ImportSection::UserDefined(_) => { Diagnostic::new( TypingOnlyThirdPartyImport { - full_name: qualified_name.to_string(), + qualified_name: qualified_name.to_string(), }, binding.range, ) } ImportSection::Known(ImportType::StandardLibrary) => Diagnostic::new( TypingOnlyStandardLibraryImport { - full_name: qualified_name.to_string(), + qualified_name: qualified_name.to_string(), }, binding.range, ), @@ -319,7 +319,7 @@ pub(crate) fn typing_only_runtime_import( let add_import_edit = checker.importer.typing_import_edit( &StmtImport { stmt, - full_name: qualified_name, + qualified_name, }, reference.range().start(), checker.semantic_model(), diff --git a/crates/ruff/src/rules/pandas_vet/helpers.rs b/crates/ruff/src/rules/pandas_vet/helpers.rs index 1ec93a6d80..9622571434 100644 --- a/crates/ruff/src/rules/pandas_vet/helpers.rs +++ b/crates/ruff/src/rules/pandas_vet/helpers.rs @@ -40,11 +40,9 @@ pub(crate) fn test_expression(expr: &Expr, model: &SemanticModel) -> Resolution | BindingKind::LoopVar | BindingKind::Global | BindingKind::Nonlocal => Resolution::RelevantLocal, - BindingKind::Importation(Importation { full_name: module }) - if module == "pandas" => - { - Resolution::PandasModule - } + BindingKind::Importation(Importation { + qualified_name: module, + }) if module == "pandas" => Resolution::PandasModule, _ => Resolution::IrrelevantBinding, } }) diff --git a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs index 9295bd6b62..ebb56710ce 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs @@ -71,7 +71,7 @@ pub(crate) fn inplace_argument( matches!( binding.kind, BindingKind::Importation(Importation { - full_name: "pandas" + qualified_name: "pandas" }) ) }); diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs index c204dfc0dd..be08c5a1c4 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs @@ -165,7 +165,9 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut let fix = if !in_init && !in_except_handler && checker.patch(Rule::UnusedImport) { autofix::edits::remove_unused_imports( - unused_imports.iter().map(|(full_name, _)| *full_name), + unused_imports + .iter() + .map(|(qualified_name, _)| *qualified_name), stmt, parent, checker.locator, @@ -177,10 +179,10 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut None }; - for (full_name, range) in unused_imports { + for (qualified_name, range) in unused_imports { let mut diagnostic = Diagnostic::new( UnusedImport { - name: full_name.to_string(), + name: qualified_name.to_string(), context: if in_except_handler { Some(UnusedImportContext::ExceptHandler) } else if in_init { @@ -217,10 +219,10 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut let multiple = unused_imports.len() > 1; let in_except_handler = exceptions.intersects(Exceptions::MODULE_NOT_FOUND_ERROR | Exceptions::IMPORT_ERROR); - for (full_name, range) in unused_imports { + for (qualified_name, range) in unused_imports { let mut diagnostic = Diagnostic::new( UnusedImport { - name: full_name.to_string(), + name: qualified_name.to_string(), context: if in_except_handler { Some(UnusedImportContext::ExceptHandler) } else if in_init { diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs index ffc4ed6ddc..b2c5c62394 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs @@ -493,14 +493,14 @@ impl<'a> ImportReplacer<'a> { fn format_import_from(names: &[&Alias], module: &str) -> String { // Construct the whitespace strings. // Generate the formatted names. - let full_names: String = names + let qualified_names: String = names .iter() .map(|name| match &name.asname { Some(asname) => format!("{} as {}", name.name, asname), None => format!("{}", name.name), }) .join(", "); - format!("from {module} import {full_names}") + format!("from {module} import {qualified_names}") } } diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index c74d96ac12..0f3340c1f7 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -800,7 +800,7 @@ pub fn format_import_from(level: Option, module: Option<&str>) -> String { /// assert_eq!(format_import_from_member(Some(1), Some("foo"), "bar"), ".foo.bar".to_string()); /// ``` pub fn format_import_from_member(level: Option, module: Option<&str>, member: &str) -> String { - let mut full_name = String::with_capacity( + let mut qualified_name = String::with_capacity( (level.unwrap_or(0) as usize) + module.as_ref().map_or(0, |module| module.len()) + 1 @@ -808,15 +808,15 @@ pub fn format_import_from_member(level: Option, module: Option<&str>, membe ); if let Some(level) = level { for _ in 0..level { - full_name.push('.'); + qualified_name.push('.'); } } if let Some(module) = module { - full_name.push_str(module); - full_name.push('.'); + qualified_name.push_str(module); + qualified_name.push('.'); } - full_name.push_str(member); - full_name + qualified_name.push_str(member); + qualified_name } /// Create a module path from a (package, path) pair. diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 61a864e09c..dd862e8ce6 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -48,36 +48,36 @@ impl<'a> Binding<'a> { /// Return `true` if this binding redefines the given binding. pub fn redefines(&self, existing: &'a Binding) -> bool { match &self.kind { - BindingKind::Importation(Importation { full_name }) => { + BindingKind::Importation(Importation { qualified_name }) => { if let BindingKind::SubmoduleImportation(SubmoduleImportation { - full_name: existing, + qualified_name: existing, }) = &existing.kind { - return full_name == existing; + return qualified_name == existing; } } - BindingKind::FromImportation(FromImportation { full_name }) => { + BindingKind::FromImportation(FromImportation { qualified_name }) => { if let BindingKind::SubmoduleImportation(SubmoduleImportation { - full_name: existing, + qualified_name: existing, }) = &existing.kind { - return full_name == existing; + return qualified_name == existing; } } - BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }) => { + BindingKind::SubmoduleImportation(SubmoduleImportation { qualified_name }) => { match &existing.kind { BindingKind::Importation(Importation { - full_name: existing, + qualified_name: existing, }) | BindingKind::SubmoduleImportation(SubmoduleImportation { - full_name: existing, + qualified_name: existing, }) => { - return full_name == existing; + return qualified_name == existing; } BindingKind::FromImportation(FromImportation { - full_name: existing, + qualified_name: existing, }) => { - return full_name == existing; + return qualified_name == existing; } _ => {} } @@ -104,10 +104,12 @@ impl<'a> Binding<'a> { /// Returns the fully-qualified symbol name, if this symbol was imported from another module. pub fn qualified_name(&self) -> Option<&str> { match &self.kind { - BindingKind::Importation(Importation { full_name }) => Some(full_name), - BindingKind::FromImportation(FromImportation { full_name }) => Some(full_name), - BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }) => { - Some(full_name) + BindingKind::Importation(Importation { qualified_name }) => Some(qualified_name), + BindingKind::FromImportation(FromImportation { qualified_name }) => { + Some(qualified_name) + } + BindingKind::SubmoduleImportation(SubmoduleImportation { qualified_name }) => { + Some(qualified_name) } _ => None, } @@ -117,14 +119,14 @@ impl<'a> Binding<'a> { /// symbol was imported from another module. pub fn module_name(&self) -> Option<&str> { match &self.kind { - BindingKind::Importation(Importation { full_name }) - | BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }) => { - Some(full_name.split('.').next().unwrap_or(full_name)) + BindingKind::Importation(Importation { qualified_name }) + | BindingKind::SubmoduleImportation(SubmoduleImportation { qualified_name }) => { + Some(qualified_name.split('.').next().unwrap_or(qualified_name)) } - BindingKind::FromImportation(FromImportation { full_name }) => Some( - full_name + BindingKind::FromImportation(FromImportation { qualified_name }) => Some( + qualified_name .rsplit_once('.') - .map_or(full_name, |(module, _)| module), + .map_or(qualified_name, |(module, _)| module), ), _ => None, } @@ -241,9 +243,9 @@ pub struct Export<'a> { #[derive(Clone, Debug)] pub struct Importation<'a> { /// The full name of the module being imported. - /// Ex) Given `import foo`, `full_name` would be "foo". - /// Ex) Given `import foo as bar`, `full_name` would be "foo". - pub full_name: &'a str, + /// Ex) Given `import foo`, `qualified_name` would be "foo". + /// Ex) Given `import foo as bar`, `qualified_name` would be "foo". + pub qualified_name: &'a str, } /// A binding for a member imported from a module, keyed on the name to which the member is bound. @@ -252,9 +254,9 @@ pub struct Importation<'a> { #[derive(Clone, Debug)] pub struct FromImportation { /// The full name of the member being imported. - /// Ex) Given `from foo import bar`, `full_name` would be "foo.bar". - /// Ex) Given `from foo import bar as baz`, `full_name` would be "foo.bar". - pub full_name: String, + /// Ex) Given `from foo import bar`, `qualified_name` would be "foo.bar". + /// Ex) Given `from foo import bar as baz`, `qualified_name` would be "foo.bar". + pub qualified_name: String, } /// A binding for a submodule imported from a module, keyed on the name of the parent module. @@ -262,8 +264,8 @@ pub struct FromImportation { #[derive(Clone, Debug)] pub struct SubmoduleImportation<'a> { /// The full name of the submodule being imported. - /// Ex) Given `import foo.bar`, `full_name` would be "foo.bar". - pub full_name: &'a str, + /// Ex) Given `import foo.bar`, `qualified_name` would be "foo.bar". + pub qualified_name: &'a str, } #[derive(Clone, Debug, is_macro::Is)] diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 589e06624c..7b23c81b6a 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -327,7 +327,9 @@ impl<'a> SemanticModel<'a> { let head = call_path.first()?; let binding = self.find_binding(head)?; match &binding.kind { - BindingKind::Importation(Importation { full_name: name }) => { + BindingKind::Importation(Importation { + qualified_name: name, + }) => { if name.starts_with('.') { let mut source_path = from_relative_import(self.module_path?, name); if source_path.is_empty() { @@ -342,13 +344,17 @@ impl<'a> SemanticModel<'a> { Some(source_path) } } - BindingKind::SubmoduleImportation(SubmoduleImportation { full_name: name }) => { + BindingKind::SubmoduleImportation(SubmoduleImportation { + qualified_name: name, + }) => { let name = name.split('.').next().unwrap_or(name); let mut source_path: CallPath = from_unqualified_name(name); source_path.extend(call_path.into_iter().skip(1)); Some(source_path) } - BindingKind::FromImportation(FromImportation { full_name: name }) => { + BindingKind::FromImportation(FromImportation { + qualified_name: name, + }) => { if name.starts_with('.') { let mut source_path = from_relative_import(self.module_path?, name); if source_path.is_empty() { @@ -397,8 +403,8 @@ impl<'a> SemanticModel<'a> { // Ex) Given `module="sys"` and `object="exit"`: // `import sys` -> `sys.exit` // `import sys as sys2` -> `sys2.exit` - BindingKind::Importation(Importation { full_name }) => { - if full_name == &module { + BindingKind::Importation(Importation { qualified_name }) => { + if qualified_name == &module { if let Some(source) = binding.source { // Verify that `sys` isn't bound in an inner scope. if self @@ -418,8 +424,9 @@ impl<'a> SemanticModel<'a> { // Ex) Given `module="os.path"` and `object="join"`: // `from os.path import join` -> `join` // `from os.path import join as join2` -> `join2` - BindingKind::FromImportation(FromImportation { full_name }) => { - if let Some((target_module, target_member)) = full_name.split_once('.') { + BindingKind::FromImportation(FromImportation { qualified_name }) => { + if let Some((target_module, target_member)) = qualified_name.split_once('.') + { if target_module == module && target_member == member { if let Some(source) = binding.source { // Verify that `join` isn't bound in an inner scope. From 79ae1840af51784f8d2c8831bb5ed55ccd7c8f0b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 Jun 2023 15:09:27 -0400 Subject: [PATCH 32/41] Remove unused lifetime from UnusedImport type alias (#4874) --- .../ruff/src/rules/pyflakes/rules/unused_import.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs index be08c5a1c4..a8f25b8809 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs @@ -100,8 +100,8 @@ impl Violation for UnusedImport { } } -type SpannedName<'a> = (&'a str, &'a TextRange); -type BindingContext<'a> = (NodeId, Option, Exceptions); +type SpannedName<'a> = (&'a str, TextRange); +type BindingContext = (NodeId, Option, Exceptions); pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut Vec) { // Collect all unused imports by statement. @@ -139,12 +139,12 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut ignored .entry((stmt_id, parent_id, exceptions)) .or_default() - .push((qualified_name, &binding.range)); + .push((qualified_name, binding.range)); } else { unused .entry((stmt_id, parent_id, exceptions)) .or_default() - .push((qualified_name, &binding.range)); + .push((qualified_name, binding.range)); } } @@ -192,7 +192,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut }, multiple, }, - *range, + range, ); if stmt.is_import_from_stmt() { diagnostic.set_parent(stmt.start()); @@ -232,7 +232,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut }, multiple, }, - *range, + range, ); if stmt.is_import_from_stmt() { diagnostic.set_parent(stmt.start()); From f9e82f2578d900de94236c94ed8bbe112cc2d65f Mon Sep 17 00:00:00 2001 From: Justin Prieto Date: Mon, 5 Jun 2023 15:21:16 -0400 Subject: [PATCH 33/41] [`flake8-pyi`] Implement PYI029 (#4851) --- .../test/fixtures/flake8_pyi/PYI029.py | 57 +++++++++ .../test/fixtures/flake8_pyi/PYI029.pyi | 28 +++++ crates/ruff/src/checkers/ast/mod.rs | 3 + crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/flake8_pyi/mod.rs | 2 + crates/ruff/src/rules/flake8_pyi/rules/mod.rs | 2 + .../rules/str_or_repr_defined_in_stub.rs | 110 ++++++++++++++++++ ...__flake8_pyi__tests__PYI029_PYI029.py.snap | 4 + ..._flake8_pyi__tests__PYI029_PYI029.pyi.snap | 62 ++++++++++ ruff.schema.json | 1 + 10 files changed, 270 insertions(+) create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI029.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI029.pyi create mode 100644 crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI029_PYI029.py.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI029_PYI029.pyi.snap diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI029.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI029.py new file mode 100644 index 0000000000..20cc7d6ae5 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI029.py @@ -0,0 +1,57 @@ +import builtins +from abc import abstractmethod + + +def __repr__(self) -> str: + ... + + +def __str__(self) -> builtins.str: + ... + + +def __repr__(self, /, foo) -> str: + ... + + +def __repr__(self, *, foo) -> str: + ... + + +class ShouldRemoveSingle: + def __str__(self) -> builtins.str: + ... + + +class ShouldRemove: + def __repr__(self) -> str: + ... + + def __str__(self) -> builtins.str: + ... + + +class NoReturnSpecified: + def __str__(self): + ... + + def __repr__(self): + ... + + +class NonMatchingArgs: + def __str__(self, *, extra) -> builtins.str: + ... + + def __repr__(self, /, extra) -> str: + ... + + +class MatchingArgsButAbstract: + @abstractmethod + def __str__(self) -> builtins.str: + ... + + @abstractmethod + def __repr__(self) -> str: + ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI029.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI029.pyi new file mode 100644 index 0000000000..a082a733a2 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI029.pyi @@ -0,0 +1,28 @@ +import builtins +from abc import abstractmethod + +def __repr__(self) -> str: ... +def __str__(self) -> builtins.str: ... +def __repr__(self, /, foo) -> str: ... +def __repr__(self, *, foo) -> str: ... + +class ShouldRemoveSingle: + def __str__(self) -> builtins.str: ... # Error: PYI029 + +class ShouldRemove: + def __repr__(self) -> str: ... # Error: PYI029 + def __str__(self) -> builtins.str: ... # Error: PYI029 + +class NoReturnSpecified: + def __str__(self): ... + def __repr__(self): ... + +class NonMatchingArgs: + def __str__(self, *, extra) -> builtins.str: ... + def __repr__(self, /, extra) -> str: ... + +class MatchingArgsButAbstract: + @abstractmethod + def __str__(self) -> builtins.str: ... + @abstractmethod + def __repr__(self) -> str: ... diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 80066088ee..f5e130f11a 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -451,6 +451,9 @@ where stmt.is_async_function_def_stmt(), ); } + if self.enabled(Rule::StrOrReprDefinedInStub) { + flake8_pyi::rules::str_or_repr_defined_in_stub(self, stmt); + } } if self.enabled(Rule::DunderFunctionName) { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 211a9b215d..0f5effd416 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -605,6 +605,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Pyi, "021") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::DocstringInStub), (Flake8Pyi, "024") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::CollectionsNamedTuple), (Flake8Pyi, "025") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnaliasedCollectionsAbcSetImport), + (Flake8Pyi, "029") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::StrOrReprDefinedInStub), (Flake8Pyi, "032") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::AnyEqNeAnnotation), (Flake8Pyi, "033") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TypeCommentInStub), (Flake8Pyi, "034") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NonSelfReturnType), diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index e92e12e003..7e27c598a5 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -46,6 +46,8 @@ mod tests { #[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.pyi"))] #[test_case(Rule::UnassignedSpecialVariableInStub, Path::new("PYI035.py"))] #[test_case(Rule::UnassignedSpecialVariableInStub, Path::new("PYI035.pyi"))] + #[test_case(Rule::StrOrReprDefinedInStub, Path::new("PYI029.py"))] + #[test_case(Rule::StrOrReprDefinedInStub, Path::new("PYI029.pyi"))] #[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.py"))] #[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.pyi"))] #[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.py"))] diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index 4f20a04c76..8b897e7ad5 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -24,6 +24,7 @@ pub(crate) use simple_defaults::{ unassigned_special_variable_in_stub, ArgumentDefaultInStub, AssignmentDefaultInStub, TypedArgumentDefaultInStub, UnannotatedAssignmentInStub, UnassignedSpecialVariableInStub, }; +pub(crate) use str_or_repr_defined_in_stub::{str_or_repr_defined_in_stub, StrOrReprDefinedInStub}; pub(crate) use string_or_bytes_too_long::{string_or_bytes_too_long, StringOrBytesTooLong}; pub(crate) use stub_body_multiple_statements::{ stub_body_multiple_statements, StubBodyMultipleStatements, @@ -54,6 +55,7 @@ mod pass_statement_stub_body; mod prefix_type_params; mod quoted_annotation_in_stub; mod simple_defaults; +mod str_or_repr_defined_in_stub; mod string_or_bytes_too_long; mod stub_body_multiple_statements; mod type_alias_naming; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs new file mode 100644 index 0000000000..88f204fcc3 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs @@ -0,0 +1,110 @@ +use rustpython_parser::ast; +use rustpython_parser::ast::Stmt; + +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::identifier_range; +use ruff_python_semantic::analyze::visibility::is_abstract; + +use crate::autofix::edits::delete_stmt; +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for redundant definitions of `__str__` or `__repr__` in stubs. +/// +/// ## Why is this bad? +/// Defining `__str__` or `__repr__` in a stub is almost always redundant, +/// as the signatures are almost always identical to those of the default +/// equivalent, `object.__str__` and `object.__repr__`, respectively. +/// +/// ## Example +/// ```python +/// class Foo: +/// def __repr__(self) -> str: +/// ... +/// ``` +#[violation] +pub struct StrOrReprDefinedInStub { + name: String, +} + +impl AlwaysAutofixableViolation for StrOrReprDefinedInStub { + #[derive_message_formats] + fn message(&self) -> String { + let StrOrReprDefinedInStub { name } = self; + format!("Defining `{name}` in a stub is almost always redundant") + } + + fn autofix_title(&self) -> String { + let StrOrReprDefinedInStub { name } = self; + format!("Remove definition of `{name}`") + } +} + +/// PYI029 +pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { + let Stmt::FunctionDef(ast::StmtFunctionDef { + name, + decorator_list, + returns, + args, + .. + }) = stmt else { + return + }; + + let Some(returns) = returns else { + return; + }; + + if !matches!(name.as_str(), "__str__" | "__repr__") { + return; + } + + if !checker.semantic_model().scope().kind.is_class() { + return; + } + + // It is a violation only if the method signature matches that of `object.__str__` + // or `object.__repr__` exactly and the method is not decorated as abstract. + if !args.kwonlyargs.is_empty() || (args.args.len() + args.posonlyargs.len()) > 1 { + return; + } + + if is_abstract(checker.semantic_model(), decorator_list) { + return; + } + + if checker + .semantic_model() + .resolve_call_path(returns) + .map_or(true, |call_path| { + !matches!(call_path.as_slice(), ["" | "builtins", "str"]) + }) + { + return; + } + + let mut diagnostic = Diagnostic::new( + StrOrReprDefinedInStub { + name: name.to_string(), + }, + identifier_range(stmt, checker.locator), + ); + if checker.patch(diagnostic.kind.rule()) { + let stmt = checker.semantic_model().stmt(); + let parent = checker.semantic_model().stmt_parent(); + let edit = delete_stmt( + stmt, + parent, + checker.locator, + checker.indexer, + checker.stylist, + ); + diagnostic.set_fix( + Fix::automatic(edit).isolate(checker.isolation(checker.semantic_model().stmt_parent())), + ); + } + checker.diagnostics.push(diagnostic); +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI029_PYI029.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI029_PYI029.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI029_PYI029.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI029_PYI029.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI029_PYI029.pyi.snap new file mode 100644 index 0000000000..b1c616ccb4 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI029_PYI029.pyi.snap @@ -0,0 +1,62 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI029.pyi:10:9: PYI029 [*] Defining `__str__` in a stub is almost always redundant + | +10 | class ShouldRemoveSingle: +11 | def __str__(self) -> builtins.str: ... # Error: PYI029 + | ^^^^^^^ PYI029 +12 | +13 | class ShouldRemove: + | + = help: Remove definition of `str` + +ℹ Fix +7 7 | def __repr__(self, *, foo) -> str: ... +8 8 | +9 9 | class ShouldRemoveSingle: +10 |- def __str__(self) -> builtins.str: ... # Error: PYI029 + 10 |+ pass # Error: PYI029 +11 11 | +12 12 | class ShouldRemove: +13 13 | def __repr__(self) -> str: ... # Error: PYI029 + +PYI029.pyi:13:9: PYI029 [*] Defining `__repr__` in a stub is almost always redundant + | +13 | class ShouldRemove: +14 | def __repr__(self) -> str: ... # Error: PYI029 + | ^^^^^^^^ PYI029 +15 | def __str__(self) -> builtins.str: ... # Error: PYI029 + | + = help: Remove definition of `repr` + +ℹ Fix +10 10 | def __str__(self) -> builtins.str: ... # Error: PYI029 +11 11 | +12 12 | class ShouldRemove: +13 |- def __repr__(self) -> str: ... # Error: PYI029 +14 13 | def __str__(self) -> builtins.str: ... # Error: PYI029 +15 14 | +16 15 | class NoReturnSpecified: + +PYI029.pyi:14:9: PYI029 [*] Defining `__str__` in a stub is almost always redundant + | +14 | class ShouldRemove: +15 | def __repr__(self) -> str: ... # Error: PYI029 +16 | def __str__(self) -> builtins.str: ... # Error: PYI029 + | ^^^^^^^ PYI029 +17 | +18 | class NoReturnSpecified: + | + = help: Remove definition of `str` + +ℹ Fix +11 11 | +12 12 | class ShouldRemove: +13 13 | def __repr__(self) -> str: ... # Error: PYI029 +14 |- def __str__(self) -> builtins.str: ... # Error: PYI029 +15 14 | +16 15 | class NoReturnSpecified: +17 16 | def __str__(self): ... + + diff --git a/ruff.schema.json b/ruff.schema.json index d5b7fa3b25..f99ffe9dee 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2242,6 +2242,7 @@ "PYI021", "PYI024", "PYI025", + "PYI029", "PYI03", "PYI032", "PYI033", From 913b9d1fcf1aa00466662492796c394fd5ca25e3 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 5 Jun 2023 21:30:28 +0200 Subject: [PATCH 34/41] Normalize newlines in `verbatim_text` (#4850) --- crates/ruff_python_formatter/src/lib.rs | 28 ++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index dcbf68acc9..75de5ac303 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -3,12 +3,15 @@ use ruff_text_size::TextRange; use rustpython_parser::ast::Mod; use rustpython_parser::lexer::lex; use rustpython_parser::{parse_tokens, Mode}; +use std::borrow::Cow; use ruff_formatter::format_element::tag::VerbatimKind; use ruff_formatter::formatter::Formatter; -use ruff_formatter::prelude::{source_position, source_text_slice, ContainsNewlines, Tag}; +use ruff_formatter::prelude::{ + dynamic_text, source_position, source_text_slice, ContainsNewlines, Tag, +}; use ruff_formatter::{ - format, write, Buffer, Format, FormatContext, FormatElement, FormatResult, Formatted, + format, normalize_newlines, write, Buffer, Format, FormatElement, FormatResult, Formatted, IndentStyle, Printed, SimpleFormatOptions, SourceCode, }; use ruff_python_ast::node::AstNode; @@ -145,14 +148,29 @@ pub(crate) const fn verbatim_text(range: TextRange) -> VerbatimText { VerbatimText(range) } -impl Format for VerbatimText { - fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { +impl Format> for VerbatimText { + fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { f.write_element(FormatElement::Tag(Tag::StartVerbatim( VerbatimKind::Verbatim { length: self.0.len(), }, )))?; - write!(f, [source_text_slice(self.0, ContainsNewlines::Detect)])?; + + match normalize_newlines(f.context().locator().slice(self.0), ['\r']) { + Cow::Borrowed(_) => { + write!(f, [source_text_slice(self.0, ContainsNewlines::Detect)])?; + } + Cow::Owned(cleaned) => { + write!( + f, + [ + dynamic_text(&cleaned, Some(self.0.start())), + source_position(self.0.end()) + ] + )?; + } + } + f.write_element(FormatElement::Tag(Tag::EndVerbatim))?; Ok(()) } From d1b8fe6af297036bd2160cf548411151be38986d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 Jun 2023 16:13:08 -0400 Subject: [PATCH 35/41] Fix round-tripping of nested functions (#4875) --- .../src/source_code/generator.rs | 60 +++++++++++++------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/crates/ruff_python_ast/src/source_code/generator.rs b/crates/ruff_python_ast/src/source_code/generator.rs index ec7e92886d..7c3e1efde4 100644 --- a/crates/ruff_python_ast/src/source_code/generator.rs +++ b/crates/ruff_python_ast/src/source_code/generator.rs @@ -209,14 +209,13 @@ impl<'a> Generator<'a> { .. }) => { self.newlines(if self.indent_depth == 0 { 2 } else { 1 }); + for decorator in decorator_list { + statement!({ + self.p("@"); + self.unparse_expr(decorator, precedence::MAX); + }); + } statement!({ - for decorator in decorator_list { - statement!({ - self.p("@"); - self.unparse_expr(decorator, precedence::MAX); - }); - } - self.newline(); self.p("def "); self.p_id(name); self.p("("); @@ -242,13 +241,13 @@ impl<'a> Generator<'a> { .. }) => { self.newlines(if self.indent_depth == 0 { 2 } else { 1 }); + for decorator in decorator_list { + statement!({ + self.p("@"); + self.unparse_expr(decorator, precedence::MAX); + }); + } statement!({ - for decorator in decorator_list { - statement!({ - self.unparse_expr(decorator, precedence::MAX); - }); - } - self.newline(); self.p("async def "); self.p_id(name); self.p("("); @@ -274,13 +273,13 @@ impl<'a> Generator<'a> { range: _range, }) => { self.newlines(if self.indent_depth == 0 { 2 } else { 1 }); + for decorator in decorator_list { + statement!({ + self.p("@"); + self.unparse_expr(decorator, precedence::MAX); + }); + } statement!({ - for decorator in decorator_list { - statement!({ - self.unparse_expr(decorator, precedence::MAX); - }); - } - self.newline(); self.p("class "); self.p_id(name); let mut first = true; @@ -1614,6 +1613,29 @@ except* Exception as e: ); assert_eq!(round_trip(r#"x = (1, 2, 3)"#), r#"x = 1, 2, 3"#); assert_eq!(round_trip(r#"-(1) + ~(2) + +(3)"#), r#"-1 + ~2 + +3"#); + assert_round_trip!( + r#"def f(): + + def f(): + pass"# + ); + assert_round_trip!( + r#"@foo +def f(): + + @foo + def f(): + pass"# + ); + + assert_round_trip!( + r#"@foo +class Foo: + + @foo + def f(): + pass"# + ); } #[test] From a70afa7de7492047ab2255236f6de64035ca91fd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 Jun 2023 17:11:19 -0400 Subject: [PATCH 36/41] Remove `ToString` prefixes (#4881) --- crates/ruff/src/rules/flake8_bandit/settings.rs | 2 +- crates/ruff/src/rules/flake8_pytest_style/settings.rs | 2 +- crates/ruff/src/rules/pyflakes/rules/strings.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ruff/src/rules/flake8_bandit/settings.rs b/crates/ruff/src/rules/flake8_bandit/settings.rs index 76f363edc4..d43bd48700 100644 --- a/crates/ruff/src/rules/flake8_bandit/settings.rs +++ b/crates/ruff/src/rules/flake8_bandit/settings.rs @@ -6,7 +6,7 @@ use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; fn default_tmp_dirs() -> Vec { ["/tmp", "/var/tmp", "/dev/shm"] - .map(std::string::ToString::to_string) + .map(ToString::to_string) .to_vec() } diff --git a/crates/ruff/src/rules/flake8_pytest_style/settings.rs b/crates/ruff/src/rules/flake8_pytest_style/settings.rs index e70545c6d4..d00a3d8ebc 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/settings.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/settings.rs @@ -16,7 +16,7 @@ fn default_broad_exceptions() -> Vec { "EnvironmentError", "socket.error", ] - .map(std::string::ToString::to_string) + .map(ToString::to_string) .to_vec() } diff --git a/crates/ruff/src/rules/pyflakes/rules/strings.rs b/crates/ruff/src/rules/pyflakes/rules/strings.rs index 7771575019..70a0151f2f 100644 --- a/crates/ruff/src/rules/pyflakes/rules/strings.rs +++ b/crates/ruff/src/rules/pyflakes/rules/strings.rs @@ -809,7 +809,7 @@ pub(crate) fn string_dot_format_extra_positional_arguments( StringDotFormatExtraPositionalArguments { missing: missing .iter() - .map(std::string::ToString::to_string) + .map(ToString::to_string) .collect::>(), }, location, From c67029ded9b3d6378bd41ae68038df398b2386f3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 Jun 2023 17:43:47 -0400 Subject: [PATCH 37/41] Move duplicate-value rule to flake8-bugbear (#4882) --- .../B033.py} | 0 crates/ruff/src/checkers/ast/mod.rs | 2 +- crates/ruff/src/codes.rs | 2 +- crates/ruff/src/rule_redirects.rs | 1 + crates/ruff/src/rules/flake8_bugbear/mod.rs | 53 ++++++++--------- .../flake8_bugbear/rules/duplicate_value.rs | 57 +++++++++++++++++++ .../src/rules/flake8_bugbear/rules/mod.rs | 2 + ...__flake8_bugbear__tests__B033_B033.py.snap | 23 ++++++++ crates/ruff/src/rules/pylint/mod.rs | 1 - crates/ruff/src/rules/pylint/rules/mod.rs | 2 - ...nt__tests__PLW0130_duplicate_value.py.snap | 23 -------- ruff.schema.json | 2 +- 12 files changed, 113 insertions(+), 55 deletions(-) rename crates/ruff/resources/test/fixtures/{pylint/duplicate_value.py => flake8_bugbear/B033.py} (100%) create mode 100644 crates/ruff/src/rules/flake8_bugbear/rules/duplicate_value.rs create mode 100644 crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B033_B033.py.snap delete mode 100644 crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0130_duplicate_value.py.snap diff --git a/crates/ruff/resources/test/fixtures/pylint/duplicate_value.py b/crates/ruff/resources/test/fixtures/flake8_bugbear/B033.py similarity index 100% rename from crates/ruff/resources/test/fixtures/pylint/duplicate_value.py rename to crates/ruff/resources/test/fixtures/flake8_bugbear/B033.py diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index f5e130f11a..24a0c8b177 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -3060,7 +3060,7 @@ where } Expr::Set(ast::ExprSet { elts, range: _ }) => { if self.enabled(Rule::DuplicateValue) { - pylint::rules::duplicate_value(self, elts); + flake8_bugbear::rules::duplicate_value(self, elts); } } Expr::Yield(_) => { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 0f5effd416..d0eb6f83f5 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -197,7 +197,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf), (Pylint, "W0120") => (RuleGroup::Unspecified, rules::pylint::rules::UselessElseOnLoop), (Pylint, "W0129") => (RuleGroup::Unspecified, rules::pylint::rules::AssertOnStringLiteral), - (Pylint, "W0130") => (RuleGroup::Unspecified, rules::pylint::rules::DuplicateValue), (Pylint, "W0131") => (RuleGroup::Unspecified, rules::pylint::rules::NamedExprWithoutContext), (Pylint, "W0406") => (RuleGroup::Unspecified, rules::pylint::rules::ImportSelf), (Pylint, "W0602") => (RuleGroup::Unspecified, rules::pylint::rules::GlobalVariableNotAssigned), @@ -249,6 +248,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Bugbear, "030") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::ExceptWithNonExceptionClasses), (Flake8Bugbear, "031") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::ReuseOfGroupbyGenerator), (Flake8Bugbear, "032") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::UnintentionalTypeAnnotation), + (Flake8Bugbear, "033") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::DuplicateValue), (Flake8Bugbear, "904") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::RaiseWithoutFromInsideExcept), (Flake8Bugbear, "905") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict), diff --git a/crates/ruff/src/rule_redirects.rs b/crates/ruff/src/rule_redirects.rs index 4749b64cca..ebf63dddd0 100644 --- a/crates/ruff/src/rule_redirects.rs +++ b/crates/ruff/src/rule_redirects.rs @@ -93,5 +93,6 @@ static REDIRECTS: Lazy> = Lazy::new(|| { // TODO(charlie): Remove by 2023-06-01. ("RUF004", "B026"), ("PIE802", "C419"), + ("PLW0130", "B033"), ]) }); diff --git a/crates/ruff/src/rules/flake8_bugbear/mod.rs b/crates/ruff/src/rules/flake8_bugbear/mod.rs index 896b977adf..2bf5676306 100644 --- a/crates/ruff/src/rules/flake8_bugbear/mod.rs +++ b/crates/ruff/src/rules/flake8_bugbear/mod.rs @@ -14,39 +14,40 @@ mod tests { use crate::settings::Settings; use crate::test::test_path; - #[test_case(Rule::UnaryPrefixIncrement, Path::new("B002.py"))] - #[test_case(Rule::AssignmentToOsEnviron, Path::new("B003.py"))] - #[test_case(Rule::UnreliableCallableCheck, Path::new("B004.py"))] - #[test_case(Rule::StripWithMultiCharacters, Path::new("B005.py"))] - #[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"))] - #[test_case(Rule::UnusedLoopControlVariable, Path::new("B007.py"))] - #[test_case(Rule::FunctionCallInDefaultArgument, Path::new("B006_B008.py"))] - #[test_case(Rule::GetAttrWithConstant, Path::new("B009_B010.py"))] - #[test_case(Rule::SetAttrWithConstant, Path::new("B009_B010.py"))] - #[test_case(Rule::AssertFalse, Path::new("B011.py"))] - #[test_case(Rule::JumpStatementInFinally, Path::new("B012.py"))] - #[test_case(Rule::RedundantTupleInExceptionHandler, Path::new("B013.py"))] - #[test_case(Rule::DuplicateHandlerException, Path::new("B014.py"))] - #[test_case(Rule::UselessComparison, Path::new("B015.py"))] - #[test_case(Rule::CannotRaiseLiteral, Path::new("B016.py"))] - #[test_case(Rule::AssertRaisesException, Path::new("B017.py"))] - #[test_case(Rule::UselessExpression, Path::new("B018.py"))] - #[test_case(Rule::CachedInstanceMethod, Path::new("B019.py"))] - #[test_case(Rule::LoopVariableOverridesIterator, Path::new("B020.py"))] - #[test_case(Rule::FStringDocstring, Path::new("B021.py"))] - #[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"))] - #[test_case(Rule::FunctionUsesLoopVariable, Path::new("B023.py"))] #[test_case(Rule::AbstractBaseClassWithoutAbstractMethod, Path::new("B024.py"))] + #[test_case(Rule::AssertFalse, Path::new("B011.py"))] + #[test_case(Rule::AssertRaisesException, Path::new("B017.py"))] + #[test_case(Rule::AssignmentToOsEnviron, Path::new("B003.py"))] + #[test_case(Rule::CachedInstanceMethod, Path::new("B019.py"))] + #[test_case(Rule::CannotRaiseLiteral, Path::new("B016.py"))] + #[test_case(Rule::DuplicateHandlerException, Path::new("B014.py"))] #[test_case(Rule::DuplicateTryBlockException, Path::new("B025.py"))] - #[test_case(Rule::StarArgUnpackingAfterKeywordArg, Path::new("B026.py"))] + #[test_case(Rule::DuplicateValue, Path::new("B033.py"))] #[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027.py"))] #[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027.pyi"))] - #[test_case(Rule::NoExplicitStacklevel, Path::new("B028.py"))] #[test_case(Rule::ExceptWithEmptyTuple, Path::new("B029.py"))] #[test_case(Rule::ExceptWithNonExceptionClasses, Path::new("B030.py"))] - #[test_case(Rule::ReuseOfGroupbyGenerator, Path::new("B031.py"))] - #[test_case(Rule::UnintentionalTypeAnnotation, Path::new("B032.py"))] + #[test_case(Rule::FStringDocstring, Path::new("B021.py"))] + #[test_case(Rule::FunctionCallInDefaultArgument, Path::new("B006_B008.py"))] + #[test_case(Rule::FunctionUsesLoopVariable, Path::new("B023.py"))] + #[test_case(Rule::GetAttrWithConstant, Path::new("B009_B010.py"))] + #[test_case(Rule::JumpStatementInFinally, Path::new("B012.py"))] + #[test_case(Rule::LoopVariableOverridesIterator, Path::new("B020.py"))] + #[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"))] + #[test_case(Rule::NoExplicitStacklevel, Path::new("B028.py"))] #[test_case(Rule::RaiseWithoutFromInsideExcept, Path::new("B904.py"))] + #[test_case(Rule::RedundantTupleInExceptionHandler, Path::new("B013.py"))] + #[test_case(Rule::ReuseOfGroupbyGenerator, Path::new("B031.py"))] + #[test_case(Rule::SetAttrWithConstant, Path::new("B009_B010.py"))] + #[test_case(Rule::StarArgUnpackingAfterKeywordArg, Path::new("B026.py"))] + #[test_case(Rule::StripWithMultiCharacters, Path::new("B005.py"))] + #[test_case(Rule::UnaryPrefixIncrement, Path::new("B002.py"))] + #[test_case(Rule::UnintentionalTypeAnnotation, Path::new("B032.py"))] + #[test_case(Rule::UnreliableCallableCheck, Path::new("B004.py"))] + #[test_case(Rule::UnusedLoopControlVariable, Path::new("B007.py"))] + #[test_case(Rule::UselessComparison, Path::new("B015.py"))] + #[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"))] + #[test_case(Rule::UselessExpression, Path::new("B018.py"))] #[test_case(Rule::ZipWithoutExplicitStrict, Path::new("B905.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_value.rs b/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_value.rs new file mode 100644 index 0000000000..e66da4623c --- /dev/null +++ b/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_value.rs @@ -0,0 +1,57 @@ +use rustc_hash::FxHashSet; +use rustpython_parser::ast::{self, Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::comparable::ComparableExpr; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for set literals that contain duplicate items. +/// +/// ## Why is this bad? +/// In Python, sets are unordered collections of unique elements. Including a +/// duplicate item in a set literal is redundant, as the duplicate item will be +/// replaced with a single item at runtime. +/// +/// ## Example +/// ```python +/// {1, 2, 3, 1} +/// ``` +/// +/// Use instead: +/// ```python +/// {1, 2, 3} +/// ``` +#[violation] +pub struct DuplicateValue { + value: String, +} + +impl Violation for DuplicateValue { + #[derive_message_formats] + fn message(&self) -> String { + let DuplicateValue { value } = self; + format!("Sets should not contain duplicate item `{value}`") + } +} + +/// B033 +pub(crate) fn duplicate_value(checker: &mut Checker, elts: &Vec) { + let mut seen_values: FxHashSet = FxHashSet::default(); + for elt in elts { + if let Expr::Constant(ast::ExprConstant { value, .. }) = elt { + let comparable_value: ComparableExpr = elt.into(); + + if !seen_values.insert(comparable_value) { + checker.diagnostics.push(Diagnostic::new( + DuplicateValue { + value: checker.generator().constant(value), + }, + elt.range(), + )); + } + }; + } +} diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs b/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs index 77714559fb..f350160a57 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs @@ -10,6 +10,7 @@ pub(crate) use cannot_raise_literal::{cannot_raise_literal, CannotRaiseLiteral}; pub(crate) use duplicate_exceptions::{ duplicate_exceptions, DuplicateHandlerException, DuplicateTryBlockException, }; +pub(crate) use duplicate_value::{duplicate_value, DuplicateValue}; pub(crate) use except_with_empty_tuple::{except_with_empty_tuple, ExceptWithEmptyTuple}; pub(crate) use except_with_non_exception_classes::{ except_with_non_exception_classes, ExceptWithNonExceptionClasses, @@ -66,6 +67,7 @@ mod assignment_to_os_environ; mod cached_instance_method; mod cannot_raise_literal; mod duplicate_exceptions; +mod duplicate_value; mod except_with_empty_tuple; mod except_with_non_exception_classes; mod f_string_docstring; diff --git a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B033_B033.py.snap b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B033_B033.py.snap new file mode 100644 index 0000000000..a71540f42f --- /dev/null +++ b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B033_B033.py.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff/src/rules/flake8_bugbear/mod.rs +--- +B033.py:4:35: B033 Sets should not contain duplicate item `"value1"` + | +4 | # Errors. +5 | ### +6 | incorrect_set = {"value1", 23, 5, "value1"} + | ^^^^^^^^ B033 +7 | incorrect_set = {1, 1} + | + +B033.py:5:21: B033 Sets should not contain duplicate item `1` + | +5 | ### +6 | incorrect_set = {"value1", 23, 5, "value1"} +7 | incorrect_set = {1, 1} + | ^ B033 +8 | +9 | ### + | + + diff --git a/crates/ruff/src/rules/pylint/mod.rs b/crates/ruff/src/rules/pylint/mod.rs index fc7fb547ad..c7536d1922 100644 --- a/crates/ruff/src/rules/pylint/mod.rs +++ b/crates/ruff/src/rules/pylint/mod.rs @@ -54,7 +54,6 @@ mod tests { #[test_case(Rule::InvalidAllObject, Path::new("invalid_all_object.py"))] #[test_case(Rule::InvalidStrReturnType, Path::new("invalid_return_type_str.py"))] #[test_case(Rule::DuplicateBases, Path::new("duplicate_bases.py"))] - #[test_case(Rule::DuplicateValue, Path::new("duplicate_value.py"))] #[test_case(Rule::InvalidCharacterBackspace, Path::new("invalid_characters.py"))] #[test_case(Rule::InvalidCharacterEsc, Path::new("invalid_characters.py"))] #[test_case(Rule::InvalidCharacterNul, Path::new("invalid_characters.py"))] diff --git a/crates/ruff/src/rules/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index a8c45ec0c8..70f088b313 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -9,7 +9,6 @@ pub(crate) use compare_to_empty_string::{compare_to_empty_string, CompareToEmpty pub(crate) use comparison_of_constant::{comparison_of_constant, ComparisonOfConstant}; pub(crate) use continue_in_finally::{continue_in_finally, ContinueInFinally}; pub(crate) use duplicate_bases::{duplicate_bases, DuplicateBases}; -pub(crate) use duplicate_value::{duplicate_value, DuplicateValue}; pub(crate) use global_statement::{global_statement, GlobalStatement}; pub(crate) use global_variable_not_assigned::GlobalVariableNotAssigned; pub(crate) use import_self::{import_from_self, import_self, ImportSelf}; @@ -66,7 +65,6 @@ mod compare_to_empty_string; mod comparison_of_constant; mod continue_in_finally; mod duplicate_bases; -mod duplicate_value; mod global_statement; mod global_variable_not_assigned; mod import_self; diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0130_duplicate_value.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0130_duplicate_value.py.snap deleted file mode 100644 index 6fdf6149d7..0000000000 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0130_duplicate_value.py.snap +++ /dev/null @@ -1,23 +0,0 @@ ---- -source: crates/ruff/src/rules/pylint/mod.rs ---- -duplicate_value.py:4:35: PLW0130 Duplicate value `"value1"` in set - | -4 | # Errors. -5 | ### -6 | incorrect_set = {"value1", 23, 5, "value1"} - | ^^^^^^^^ PLW0130 -7 | incorrect_set = {1, 1} - | - -duplicate_value.py:5:21: PLW0130 Duplicate value `1` in set - | -5 | ### -6 | incorrect_set = {"value1", 23, 5, "value1"} -7 | incorrect_set = {1, 1} - | ^ PLW0130 -8 | -9 | ### - | - - diff --git a/ruff.schema.json b/ruff.schema.json index f99ffe9dee..29b6d3f32e 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1639,6 +1639,7 @@ "B030", "B031", "B032", + "B033", "B9", "B90", "B904", @@ -2137,7 +2138,6 @@ "PLW0120", "PLW0129", "PLW013", - "PLW0130", "PLW0131", "PLW04", "PLW040", From 0c7ea800aff5294516bd7e11585a21e4670994e8 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 Jun 2023 20:44:30 -0400 Subject: [PATCH 38/41] Remove destructive fixes for F523 (#4883) --- crates/ruff/src/cst/matchers.rs | 12 +- crates/ruff/src/rules/pyflakes/fixes.rs | 122 +----------------- crates/ruff/src/rules/pyflakes/format.rs | 2 - .../ruff/src/rules/pyflakes/rules/strings.rs | 64 ++++++--- ..._rules__pyflakes__tests__F504_F504.py.snap | 6 +- ..._rules__pyflakes__tests__F504_F50x.py.snap | 2 +- ..._rules__pyflakes__tests__F522_F522.py.snap | 6 +- ..._rules__pyflakes__tests__F523_F523.py.snap | 89 +++---------- 8 files changed, 73 insertions(+), 230 deletions(-) diff --git a/crates/ruff/src/cst/matchers.rs b/crates/ruff/src/cst/matchers.rs index 0df0590fcc..23b6382d14 100644 --- a/crates/ruff/src/cst/matchers.rs +++ b/crates/ruff/src/cst/matchers.rs @@ -3,7 +3,7 @@ use libcst_native::{ Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FormattedString, FormattedStringContent, FormattedStringExpression, FunctionDef, GeneratorExp, If, Import, ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda, ListComp, Module, Name, - SimpleString, SmallStatement, Statement, Suite, Tuple, With, + SmallStatement, Statement, Suite, Tuple, With, }; pub(crate) fn match_module(module_text: &str) -> Result { @@ -109,16 +109,6 @@ pub(crate) fn match_attribute<'a, 'b>( } } -pub(crate) fn match_simple_string<'a, 'b>( - expression: &'a mut Expression<'b>, -) -> Result<&'a mut SimpleString<'b>> { - if let Expression::SimpleString(simple_string) = expression { - Ok(simple_string) - } else { - bail!("Expected Expression::SimpleString") - } -} - pub(crate) fn match_formatted_string<'a, 'b>( expression: &'a mut Expression<'b>, ) -> Result<&'a mut FormattedString<'b>> { diff --git a/crates/ruff/src/rules/pyflakes/fixes.rs b/crates/ruff/src/rules/pyflakes/fixes.rs index a83d1c8068..56d2347cc4 100644 --- a/crates/ruff/src/rules/pyflakes/fixes.rs +++ b/crates/ruff/src/rules/pyflakes/fixes.rs @@ -1,20 +1,15 @@ -use anyhow::{anyhow, bail, Ok, Result}; +use anyhow::{bail, Ok, Result}; use libcst_native::{DictElement, Expression}; use ruff_text_size::TextRange; -use rustpython_format::{ - FieldName, FieldNamePart, FieldType, FormatPart, FormatString, FromTemplate, -}; use rustpython_parser::ast::{Excepthandler, Expr, Ranged}; use rustpython_parser::{lexer, Mode, Tok}; -use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_ast::str::{leading_quote, raw_contents, trailing_quote}; +use ruff_python_ast::str::raw_contents; -use crate::cst::matchers::{ - match_attribute, match_call_mut, match_dict, match_expression, match_simple_string, -}; +use crate::autofix::codemods::CodegenStylist; +use crate::cst::matchers::{match_call_mut, match_dict, match_expression}; /// Generate a [`Edit`] to remove unused keys from format dict. pub(crate) fn remove_unused_format_arguments_from_dict( @@ -60,132 +55,25 @@ pub(crate) fn remove_unused_keyword_arguments_from_format_call( )) } -fn unparse_format_part(format_part: FormatPart) -> String { - match format_part { - FormatPart::Literal(literal) => literal, - FormatPart::Field { - field_name, - conversion_spec, - format_spec, - } => { - let mut field_name = field_name; - if let Some(conversion) = conversion_spec { - field_name.push_str(&format!("!{conversion}")); - } - if !format_spec.is_empty() { - field_name.push_str(&format!(":{format_spec}")); - } - format!("{{{field_name}}}") - } - } -} - -fn update_field_types(format_string: &FormatString, index_map: &[usize]) -> String { - format_string - .format_parts - .iter() - .map(|part| match part { - FormatPart::Literal(literal) => FormatPart::Literal(literal.to_string()), - FormatPart::Field { - field_name, - conversion_spec, - format_spec, - } => { - // SAFETY: We've already parsed this string before. - let new_field_name = FieldName::parse(field_name).unwrap(); - let mut new_field_name_string = match new_field_name.field_type { - FieldType::Auto => String::new(), - FieldType::Index(i) => index_map.get(i).unwrap_or(&i).to_string(), - FieldType::Keyword(keyword) => keyword, - }; - for field_name_part in &new_field_name.parts { - let field_name_part_string = match field_name_part { - FieldNamePart::Attribute(attribute) => format!(".{attribute}"), - FieldNamePart::Index(i) => format!("[{i}]"), - FieldNamePart::StringIndex(s) => format!("[{s}]"), - }; - new_field_name_string.push_str(&field_name_part_string); - } - - // SAFETY: We've already parsed this string before. - let new_format_spec = FormatString::from_str(format_spec).unwrap(); - let new_format_spec_string = update_field_types(&new_format_spec, index_map); - FormatPart::Field { - field_name: new_field_name_string, - conversion_spec: *conversion_spec, - format_spec: new_format_spec_string, - } - } - }) - .map(unparse_format_part) - .collect() -} - /// Generate a [`Edit`] to remove unused positional arguments from a `format` call. pub(crate) fn remove_unused_positional_arguments_from_format_call( unused_arguments: &[usize], location: TextRange, locator: &Locator, stylist: &Stylist, - format_string: &FormatString, ) -> Result { let module_text = locator.slice(location); let mut tree = match_expression(module_text)?; let call = match_call_mut(&mut tree)?; - // Remove any unused arguments, and generate a map from previous index to new index. + // Remove any unused arguments. let mut index = 0; - let mut offset = 0; - let mut index_map = Vec::with_capacity(call.args.len()); call.args.retain(|_| { - index_map.push(index - offset); let is_unused = unused_arguments.contains(&index); index += 1; - if is_unused { - offset += 1; - } !is_unused }); - // If we removed an argument, we may need to rewrite the positional themselves. - // Ex) `"{1}{2}".format(a, b, c)` to `"{0}{1}".format(b, c)` - let rewrite_arguments = index_map - .iter() - .enumerate() - .filter(|&(prev_index, _)| !unused_arguments.contains(&prev_index)) - .any(|(prev_index, &new_index)| prev_index != new_index); - - let new_format_string; - if rewrite_arguments { - // Extract the format string verbatim. - let func = match_attribute(&mut call.func)?; - let simple_string = match_simple_string(&mut func.value)?; - - // Extract existing quotes from the format string. - let leading_quote = leading_quote(simple_string.value).ok_or_else(|| { - anyhow!( - "Could not find leading quote for format string: {}", - simple_string.value - ) - })?; - let trailing_quote = trailing_quote(simple_string.value).ok_or_else(|| { - anyhow!( - "Could not find trailing quote for format string: {}", - simple_string.value - ) - })?; - - // Update the format string, preserving the quotes. - new_format_string = format!( - "{}{}{}", - leading_quote, - update_field_types(format_string, &index_map), - trailing_quote - ); - - simple_string.value = new_format_string.as_str(); - } - Ok(Edit::range_replacement( tree.codegen_stylist(stylist), location, diff --git a/crates/ruff/src/rules/pyflakes/format.rs b/crates/ruff/src/rules/pyflakes/format.rs index 2bd1bc9b62..03d1f30f95 100644 --- a/crates/ruff/src/rules/pyflakes/format.rs +++ b/crates/ruff/src/rules/pyflakes/format.rs @@ -26,7 +26,6 @@ pub(crate) struct FormatSummary { pub(crate) indices: Vec, pub(crate) keywords: Vec, pub(crate) has_nested_parts: bool, - pub(crate) format_string: FormatString, } impl TryFrom<&str> for FormatSummary { @@ -75,7 +74,6 @@ impl TryFrom<&str> for FormatSummary { indices, keywords, has_nested_parts, - format_string, }) } } diff --git a/crates/ruff/src/rules/pyflakes/rules/strings.rs b/crates/ruff/src/rules/pyflakes/rules/strings.rs index 70a0151f2f..5afec5c441 100644 --- a/crates/ruff/src/rules/pyflakes/rules/strings.rs +++ b/crates/ruff/src/rules/pyflakes/rules/strings.rs @@ -4,7 +4,7 @@ use ruff_text_size::TextRange; use rustc_hash::FxHashSet; use rustpython_parser::ast::{self, Constant, Expr, Identifier, Keyword}; -use ruff_diagnostics::{AlwaysAutofixableViolation, AutofixKind, Diagnostic, Violation}; +use ruff_diagnostics::{AlwaysAutofixableViolation, AutofixKind, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; @@ -604,14 +604,14 @@ pub(crate) fn percent_format_extra_named_arguments( location, ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - remove_unused_format_arguments_from_dict( + diagnostic.try_set_fix(|| { + let edit = remove_unused_format_arguments_from_dict( &missing, right, checker.locator, checker.stylist, - ) + )?; + Ok(Fix::automatic(edit)) }); } checker.diagnostics.push(diagnostic); @@ -770,14 +770,14 @@ pub(crate) fn string_dot_format_extra_named_arguments( location, ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - remove_unused_keyword_arguments_from_format_call( + diagnostic.try_set_fix(|| { + let edit = remove_unused_keyword_arguments_from_format_call( &missing, location, checker.locator, checker.stylist, - ) + )?; + Ok(Fix::automatic(edit)) }); } checker.diagnostics.push(diagnostic); @@ -815,16 +815,42 @@ pub(crate) fn string_dot_format_extra_positional_arguments( location, ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - remove_unused_positional_arguments_from_format_call( - &missing, - location, - checker.locator, - checker.stylist, - &summary.format_string, - ) - }); + // We can only fix if the positional arguments we're removing don't require re-indexing + // the format string itself. For example, we can't fix `"{1}{2}".format(0, 1, 2)"`, since + // this requires changing the format string to `"{0}{1}"`. But we can fix + // `"{0}{1}".format(0, 1, 2)`, since this only requires modifying the call arguments. + fn is_contiguous_from_end(indexes: &[usize], target: &[T]) -> bool { + if indexes.is_empty() { + return true; + } + + let mut expected_index = target.len() - 1; + for &index in indexes.iter().rev() { + if index != expected_index { + return false; + } + + if expected_index == 0 { + break; + } + + expected_index -= 1; + } + + true + } + + if is_contiguous_from_end(&missing, args) { + diagnostic.try_set_fix(|| { + let edit = remove_unused_positional_arguments_from_format_call( + &missing, + location, + checker.locator, + checker.stylist, + )?; + Ok(Fix::automatic(edit)) + }); + } } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F504.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F504.py.snap index 8ab0155101..a8f8bc10aa 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F504.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F504.py.snap @@ -12,7 +12,7 @@ F504.py:3:1: F504 [*] `%`-format string has unused named argument(s): b | = help: Remove extra named arguments: b -ℹ Suggested fix +ℹ Fix 1 1 | # Ruff has no way of knowing if the following are F505s 2 2 | a = "wrong" 3 |-"%(a)s %(c)s" % {a: "?", "b": "!"} # F504 ("b" not used) @@ -31,7 +31,7 @@ F504.py:8:1: F504 [*] `%`-format string has unused named argument(s): b | = help: Remove extra named arguments: b -ℹ Suggested fix +ℹ Fix 5 5 | hidden = {"a": "!"} 6 6 | "%(a)s %(c)s" % {"x": 1, **hidden} # Ok (cannot see through splat) 7 7 | @@ -47,7 +47,7 @@ F504.py:9:1: F504 [*] `%`-format string has unused named argument(s): b | = help: Remove extra named arguments: b -ℹ Suggested fix +ℹ Fix 6 6 | "%(a)s %(c)s" % {"x": 1, **hidden} # Ok (cannot see through splat) 7 7 | 8 8 | "%(a)s" % {"a": 1, r"b": "!"} # F504 ("b" not used) diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F50x.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F50x.py.snap index 94960c3067..7c0f848338 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F50x.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F50x.py.snap @@ -12,7 +12,7 @@ F50x.py:8:1: F504 [*] `%`-format string has unused named argument(s): baz | = help: Remove extra named arguments: baz -ℹ Suggested fix +ℹ Fix 5 5 | '%s %s' % (1,) # F507 6 6 | '%s %s' % (1, 2, 3) # F507 7 7 | '%(bar)s' % {} # F505 diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F522_F522.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F522_F522.py.snap index 2779b22c72..ca5ac6ec6e 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F522_F522.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F522_F522.py.snap @@ -10,7 +10,7 @@ F522.py:1:1: F522 [*] `.format` call has unused named argument(s): bar | = help: Remove extra named arguments: bar -ℹ Suggested fix +ℹ Fix 1 |-"{}".format(1, bar=2) # F522 1 |+"{}".format(1, ) # F522 2 2 | "{bar}{}".format(1, bar=2, spam=3) # F522 @@ -27,7 +27,7 @@ F522.py:2:1: F522 [*] `.format` call has unused named argument(s): spam | = help: Remove extra named arguments: spam -ℹ Suggested fix +ℹ Fix 1 1 | "{}".format(1, bar=2) # F522 2 |-"{bar}{}".format(1, bar=2, spam=3) # F522 2 |+"{bar}{}".format(1, bar=2, ) # F522 @@ -43,7 +43,7 @@ F522.py:4:1: F522 [*] `.format` call has unused named argument(s): eggs, ham | = help: Remove extra named arguments: eggs, ham -ℹ Suggested fix +ℹ Fix 1 1 | "{}".format(1, bar=2) # F522 2 2 | "{bar}{}".format(1, bar=2, spam=3) # F522 3 3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap index 17a1adab1e..42d7590315 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap @@ -11,7 +11,7 @@ F523.py:2:1: F523 [*] `.format` call has unused arguments at position(s): 1 | = help: Remove extra positional arguments at position(s): 1 -ℹ Suggested fix +ℹ Fix 1 1 | # With indexes 2 |-"{0}".format(1, 2) # F523 2 |+"{0}".format(1, ) # F523 @@ -19,7 +19,7 @@ F523.py:2:1: F523 [*] `.format` call has unused arguments at position(s): 1 4 4 | "{1:{0}}".format(1, 2) # No issues 5 5 | "{1:{0}}".format(1, 2, 3) # F523 -F523.py:3:1: F523 [*] `.format` call has unused arguments at position(s): 0, 2 +F523.py:3:1: F523 `.format` call has unused arguments at position(s): 0, 2 | 3 | # With indexes 4 | "{0}".format(1, 2) # F523 @@ -30,15 +30,6 @@ F523.py:3:1: F523 [*] `.format` call has unused arguments at position(s): 0, 2 | = help: Remove extra positional arguments at position(s): 0, 2 -ℹ Suggested fix -1 1 | # With indexes -2 2 | "{0}".format(1, 2) # F523 -3 |-"{1}".format(1, 2, 3) # F523 - 3 |+"{0}".format(2, ) # F523 -4 4 | "{1:{0}}".format(1, 2) # No issues -5 5 | "{1:{0}}".format(1, 2, 3) # F523 -6 6 | "{0}{2}".format(1, 2) # F523, # F524 - F523.py:5:1: F523 [*] `.format` call has unused arguments at position(s): 2 | 5 | "{1}".format(1, 2, 3) # F523 @@ -50,7 +41,7 @@ F523.py:5:1: F523 [*] `.format` call has unused arguments at position(s): 2 | = help: Remove extra positional arguments at position(s): 2 -ℹ Suggested fix +ℹ Fix 2 2 | "{0}".format(1, 2) # F523 3 3 | "{1}".format(1, 2, 3) # F523 4 4 | "{1:{0}}".format(1, 2) # No issues @@ -70,7 +61,7 @@ F523.py:6:1: F523 [*] `.format` call has unused arguments at position(s): 1 | = help: Remove extra positional arguments at position(s): 1 -ℹ Suggested fix +ℹ Fix 3 3 | "{1}".format(1, 2, 3) # F523 4 4 | "{1:{0}}".format(1, 2) # No issues 5 5 | "{1:{0}}".format(1, 2, 3) # F523 @@ -80,7 +71,7 @@ F523.py:6:1: F523 [*] `.format` call has unused arguments at position(s): 1 8 8 | 9 9 | # With no indexes -F523.py:7:1: F523 [*] `.format` call has unused arguments at position(s): 0, 3 +F523.py:7:1: F523 `.format` call has unused arguments at position(s): 0, 3 | 7 | "{1:{0}}".format(1, 2, 3) # F523 8 | "{0}{2}".format(1, 2) # F523, # F524 @@ -91,16 +82,6 @@ F523.py:7:1: F523 [*] `.format` call has unused arguments at position(s): 0, 3 | = help: Remove extra positional arguments at position(s): 0, 3 -ℹ Suggested fix -4 4 | "{1:{0}}".format(1, 2) # No issues -5 5 | "{1:{0}}".format(1, 2, 3) # F523 -6 6 | "{0}{2}".format(1, 2) # F523, # F524 -7 |-"{1.arg[1]!r:0{2['arg']}{1}}".format(1, 2, 3, 4) # F523 - 7 |+"{0.arg[1]!r:0{1['arg']}{0}}".format(2, 3, ) # F523 -8 8 | -9 9 | # With no indexes -10 10 | "{}".format(1, 2) # F523 - F523.py:10:1: F523 [*] `.format` call has unused arguments at position(s): 1 | 10 | # With no indexes @@ -111,7 +92,7 @@ F523.py:10:1: F523 [*] `.format` call has unused arguments at position(s): 1 | = help: Remove extra positional arguments at position(s): 1 -ℹ Suggested fix +ℹ Fix 7 7 | "{1.arg[1]!r:0{2['arg']}{1}}".format(1, 2, 3, 4) # F523 8 8 | 9 9 | # With no indexes @@ -132,7 +113,7 @@ F523.py:11:1: F523 [*] `.format` call has unused arguments at position(s): 1, 2 | = help: Remove extra positional arguments at position(s): 1, 2 -ℹ Suggested fix +ℹ Fix 8 8 | 9 9 | # With no indexes 10 10 | "{}".format(1, 2) # F523 @@ -153,7 +134,7 @@ F523.py:13:1: F523 [*] `.format` call has unused arguments at position(s): 2 | = help: Remove extra positional arguments at position(s): 2 -ℹ Suggested fix +ℹ Fix 10 10 | "{}".format(1, 2) # F523 11 11 | "{}".format(1, 2, 3) # F523 12 12 | "{:{}}".format(1, 2) # No issues @@ -163,7 +144,7 @@ F523.py:13:1: F523 [*] `.format` call has unused arguments at position(s): 2 15 15 | # With *args 16 16 | "{0}{1}".format(*args) # No issues -F523.py:19:1: F523 [*] `.format` call has unused arguments at position(s): 2 +F523.py:19:1: F523 `.format` call has unused arguments at position(s): 2 | 19 | "{0}{1}".format(1, *args) # No issues 20 | "{0}{1}".format(1, 2, *args) # No issues @@ -174,16 +155,6 @@ F523.py:19:1: F523 [*] `.format` call has unused arguments at position(s): 2 | = help: Remove extra positional arguments at position(s): 2 -ℹ Suggested fix -16 16 | "{0}{1}".format(*args) # No issues -17 17 | "{0}{1}".format(1, *args) # No issues -18 18 | "{0}{1}".format(1, 2, *args) # No issues -19 |-"{0}{1}".format(1, 2, 3, *args) # F523 - 19 |+"{0}{1}".format(1, 2, *args) # F523 -20 20 | -21 21 | # With nested quotes -22 22 | "''1{0}".format(1, 2, 3) # F523 - F523.py:22:1: F523 [*] `.format` call has unused arguments at position(s): 1, 2 | 22 | # With nested quotes @@ -194,7 +165,7 @@ F523.py:22:1: F523 [*] `.format` call has unused arguments at position(s): 1, 2 | = help: Remove extra positional arguments at position(s): 1, 2 -ℹ Suggested fix +ℹ Fix 19 19 | "{0}{1}".format(1, 2, 3, *args) # F523 20 20 | 21 21 | # With nested quotes @@ -214,7 +185,7 @@ F523.py:23:1: F523 [*] `.format` call has unused arguments at position(s): 2 | = help: Remove extra positional arguments at position(s): 2 -ℹ Suggested fix +ℹ Fix 20 20 | 21 21 | # With nested quotes 22 22 | "''1{0}".format(1, 2, 3) # F523 @@ -235,7 +206,7 @@ F523.py:24:1: F523 [*] `.format` call has unused arguments at position(s): 2 | = help: Remove extra positional arguments at position(s): 2 -ℹ Suggested fix +ℹ Fix 21 21 | # With nested quotes 22 22 | "''1{0}".format(1, 2, 3) # F523 23 23 | "\"\"{1}{0}".format(1, 2, 3) # F523 @@ -245,7 +216,7 @@ F523.py:24:1: F523 [*] `.format` call has unused arguments at position(s): 2 26 26 | # With modified indexes 27 27 | "{1}{2}".format(1, 2, 3) # F523, # F524 -F523.py:27:1: F523 [*] `.format` call has unused arguments at position(s): 0 +F523.py:27:1: F523 `.format` call has unused arguments at position(s): 0 | 27 | # With modified indexes 28 | "{1}{2}".format(1, 2, 3) # F523, # F524 @@ -255,17 +226,7 @@ F523.py:27:1: F523 [*] `.format` call has unused arguments at position(s): 0 | = help: Remove extra positional arguments at position(s): 0 -ℹ Suggested fix -24 24 | '""{1}{0}'.format(1, 2, 3) # F523 -25 25 | -26 26 | # With modified indexes -27 |-"{1}{2}".format(1, 2, 3) # F523, # F524 - 27 |+"{0}{1}".format(2, 3) # F523, # F524 -28 28 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 -29 29 | "{1} {8}".format(0, 1) # F523, # F524 -30 30 | - -F523.py:28:1: F523 [*] `.format` call has unused arguments at position(s): 0, 2 +F523.py:28:1: F523 `.format` call has unused arguments at position(s): 0, 2 | 28 | # With modified indexes 29 | "{1}{2}".format(1, 2, 3) # F523, # F524 @@ -275,17 +236,7 @@ F523.py:28:1: F523 [*] `.format` call has unused arguments at position(s): 0, 2 | = help: Remove extra positional arguments at position(s): 0, 2 -ℹ Suggested fix -25 25 | -26 26 | # With modified indexes -27 27 | "{1}{2}".format(1, 2, 3) # F523, # F524 -28 |-"{1}{3}".format(1, 2, 3, 4) # F523, # F524 - 28 |+"{0}{1}".format(2, 4) # F523, # F524 -29 29 | "{1} {8}".format(0, 1) # F523, # F524 -30 30 | -31 31 | # Not fixable - -F523.py:29:1: F523 [*] `.format` call has unused arguments at position(s): 0 +F523.py:29:1: F523 `.format` call has unused arguments at position(s): 0 | 29 | "{1}{2}".format(1, 2, 3) # F523, # F524 30 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 @@ -296,16 +247,6 @@ F523.py:29:1: F523 [*] `.format` call has unused arguments at position(s): 0 | = help: Remove extra positional arguments at position(s): 0 -ℹ Suggested fix -26 26 | # With modified indexes -27 27 | "{1}{2}".format(1, 2, 3) # F523, # F524 -28 28 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 -29 |-"{1} {8}".format(0, 1) # F523, # F524 - 29 |+"{0} {8}".format(1) # F523, # F524 -30 30 | -31 31 | # Not fixable -32 32 | ('' - F523.py:32:2: F523 `.format` call has unused arguments at position(s): 0 | 32 | # Not fixable From 805b2eb0b7e04c1a6d214c45719192e501a5f0c7 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 Jun 2023 20:48:53 -0400 Subject: [PATCH 39/41] Respect shadowed exports in `__all__` (#4885) --- .../test/fixtures/pyflakes/F401_16.py | 15 ++ crates/ruff/src/checkers/ast/mod.rs | 158 +++++++----------- crates/ruff/src/rules/pyflakes/mod.rs | 1 + .../rules/pyflakes/rules/undefined_export.rs | 18 +- ...les__pyflakes__tests__F401_F401_16.py.snap | 4 + 5 files changed, 86 insertions(+), 110 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/pyflakes/F401_16.py create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_16.py.snap diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F401_16.py b/crates/ruff/resources/test/fixtures/pyflakes/F401_16.py new file mode 100644 index 0000000000..dd815bb900 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pyflakes/F401_16.py @@ -0,0 +1,15 @@ +"""Test that `__all__` exports are respected even with multiple declarations.""" + +import random + + +def some_dependency_check(): + return random.uniform(0.0, 1.0) > 0.49999 + + +if some_dependency_check(): + import math + + __all__ = ["math"] +else: + __all__ = [] diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 24a0c8b177..08f9508c85 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -4616,14 +4616,8 @@ impl<'a> Checker<'a> { let scope = self.semantic_model.scope(); - if id == "__all__" - && scope.kind.is_module() - && matches!( - parent, - Stmt::Assign(_) | Stmt::AugAssign(_) | Stmt::AnnAssign(_) - ) - { - if match parent { + if scope.kind.is_module() + && match parent { Stmt::Assign(ast::StmtAssign { targets, .. }) => { if let Some(Expr::Name(ast::ExprName { id, .. })) = targets.first() { id == "__all__" @@ -4646,47 +4640,32 @@ impl<'a> Checker<'a> { } } _ => false, - } { - let (all_names, all_names_flags) = { - let (mut names, flags) = - extract_all_names(parent, |name| self.semantic_model.is_builtin(name)); - - // Grab the existing bound __all__ values. - if let Stmt::AugAssign(_) = parent { - if let Some(binding_id) = scope.get("__all__") { - if let BindingKind::Export(Export { names: existing }) = - &self.semantic_model.bindings[binding_id].kind - { - names.extend_from_slice(existing); - } - } - } - - (names, flags) - }; - - if self.enabled(Rule::InvalidAllFormat) { - if matches!(all_names_flags, AllNamesFlags::INVALID_FORMAT) { - self.diagnostics - .push(pylint::rules::invalid_all_format(expr)); - } - } - - if self.enabled(Rule::InvalidAllObject) { - if matches!(all_names_flags, AllNamesFlags::INVALID_OBJECT) { - self.diagnostics - .push(pylint::rules::invalid_all_object(expr)); - } - } - - self.add_binding( - id, - expr.range(), - BindingKind::Export(Export { names: all_names }), - BindingFlags::empty(), - ); - return; } + { + let (names, flags) = + extract_all_names(parent, |name| self.semantic_model.is_builtin(name)); + + if self.enabled(Rule::InvalidAllFormat) { + if matches!(flags, AllNamesFlags::INVALID_FORMAT) { + self.diagnostics + .push(pylint::rules::invalid_all_format(expr)); + } + } + + if self.enabled(Rule::InvalidAllObject) { + if matches!(flags, AllNamesFlags::INVALID_OBJECT) { + self.diagnostics + .push(pylint::rules::invalid_all_object(expr)); + } + } + + self.add_binding( + id, + expr.range(), + BindingKind::Export(Export { names }), + BindingFlags::empty(), + ); + return; } if self @@ -4920,50 +4899,31 @@ impl<'a> Checker<'a> { } // Mark anything referenced in `__all__` as used. - let all_bindings: Option<(Vec, TextRange)> = { + let exports: Vec<(&str, TextRange)> = { let global_scope = self.semantic_model.global_scope(); - let all_names: Option<(&[&str], TextRange)> = global_scope - .get("__all__") + global_scope + .bindings_for_name("__all__") .map(|binding_id| &self.semantic_model.bindings[binding_id]) - .and_then(|binding| match &binding.kind { + .filter_map(|binding| match &binding.kind { BindingKind::Export(Export { names }) => { - Some((names.as_slice(), binding.range)) + Some(names.iter().map(|name| (*name, binding.range))) } _ => None, - }); - - all_names.map(|(names, range)| { - ( - names - .iter() - .filter_map(|name| global_scope.get(name)) - .collect(), - range, - ) - }) + }) + .flatten() + .collect() }; - if let Some((bindings, range)) = all_bindings { - for binding_id in bindings { + for (name, range) in &exports { + if let Some(binding_id) = self.semantic_model.global_scope().get(name) { self.semantic_model.add_global_reference( binding_id, - range, + *range, ExecutionContext::Runtime, ); } } - // Extract `__all__` names from the global scope. - let all_names: Option<(&[&str], TextRange)> = self - .semantic_model - .global_scope() - .get("__all__") - .map(|binding_id| &self.semantic_model.bindings[binding_id]) - .and_then(|binding| match &binding.kind { - BindingKind::Export(Export { names }) => Some((names.as_slice(), binding.range)), - _ => None, - }); - // Identify any valid runtime imports. If a module is imported at runtime, and // used at runtime, then by default, we avoid flagging any other // imports from that model as typing-only. @@ -5000,35 +4960,33 @@ impl<'a> Checker<'a> { // F822 if self.enabled(Rule::UndefinedExport) { if !self.path.ends_with("__init__.py") { - if let Some((names, range)) = all_names { + for (name, range) in &exports { diagnostics - .extend(pyflakes::rules::undefined_export(names, range, scope)); + .extend(pyflakes::rules::undefined_export(name, *range, scope)); } } } // F405 if self.enabled(Rule::UndefinedLocalWithImportStarUsage) { - if let Some((names, range)) = &all_names { - let sources: Vec = scope - .star_imports() - .map(|StarImportation { level, module }| { - helpers::format_import_from(*level, *module) - }) - .sorted() - .dedup() - .collect(); - if !sources.is_empty() { - for name in names.iter() { - if !scope.defines(name) { - diagnostics.push(Diagnostic::new( - pyflakes::rules::UndefinedLocalWithImportStarUsage { - name: (*name).to_string(), - sources: sources.clone(), - }, - *range, - )); - } + let sources: Vec = scope + .star_imports() + .map(|StarImportation { level, module }| { + helpers::format_import_from(*level, *module) + }) + .sorted() + .dedup() + .collect(); + if !sources.is_empty() { + for (name, range) in &exports { + if !scope.defines(name) { + diagnostics.push(Diagnostic::new( + pyflakes::rules::UndefinedLocalWithImportStarUsage { + name: (*name).to_string(), + sources: sources.clone(), + }, + *range, + )); } } } diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index bcaea72222..fe890582f6 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -40,6 +40,7 @@ mod tests { #[test_case(Rule::UnusedImport, Path::new("F401_13.py"))] #[test_case(Rule::UnusedImport, Path::new("F401_14.py"))] #[test_case(Rule::UnusedImport, Path::new("F401_15.py"))] + #[test_case(Rule::UnusedImport, Path::new("F401_16.py"))] #[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))] #[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))] #[test_case(Rule::LateFutureImport, Path::new("F404.py"))] diff --git a/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs b/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs index c7c36956c7..07ed283517 100644 --- a/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs +++ b/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs @@ -48,18 +48,16 @@ impl Violation for UndefinedExport { } /// F822 -pub(crate) fn undefined_export(names: &[&str], range: TextRange, scope: &Scope) -> Vec { +pub(crate) fn undefined_export(name: &str, range: TextRange, scope: &Scope) -> Vec { let mut diagnostics = Vec::new(); if !scope.uses_star_imports() { - for name in names { - if !scope.defines(name) { - diagnostics.push(Diagnostic::new( - UndefinedExport { - name: (*name).to_string(), - }, - range, - )); - } + if !scope.defines(name) { + diagnostics.push(Diagnostic::new( + UndefinedExport { + name: (*name).to_string(), + }, + range, + )); } } diagnostics diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_16.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_16.py.snap new file mode 100644 index 0000000000..1976c4331d --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_16.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- + From c2a3e97b7fca2fc843f7e7309c1a756db7c5111d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 Jun 2023 20:52:11 -0400 Subject: [PATCH 40/41] Avoid early-exit in explicit-f-string-type-conversion (#4886) --- .../resources/test/fixtures/ruff/RUF010.py | 2 + .../explicit_f_string_type_conversion.rs | 13 ++--- ..._rules__ruff__tests__RUF010_RUF010.py.snap | 54 ++++++++++++++++--- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF010.py b/crates/ruff/resources/test/fixtures/ruff/RUF010.py index 2d2604f9dc..f5850115de 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF010.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF010.py @@ -12,6 +12,8 @@ f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 +f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 + f"{foo(bla)}" # OK f"{str(bla, 'ascii')}, {str(bla, encoding='cp1255')}" # OK diff --git a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs index 9ed8091258..18e93ab3c6 100644 --- a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs +++ b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs @@ -97,9 +97,10 @@ pub(crate) fn explicit_f_string_type_conversion( }) = &formatted_value else { continue; }; + // Skip if there's already a conversion flag. if !conversion.is_none() { - return; + continue; } let Expr::Call(ast::ExprCall { @@ -108,24 +109,24 @@ pub(crate) fn explicit_f_string_type_conversion( keywords, .. }) = value.as_ref() else { - return; + continue; }; // Can't be a conversion otherwise. if args.len() != 1 || !keywords.is_empty() { - return; + continue; } let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else { - return; + continue; }; if !matches!(id.as_str(), "str" | "repr" | "ascii") { - return; + continue; }; if !checker.semantic_model().is_builtin(id) { - return; + continue; } let mut diagnostic = Diagnostic::new(ExplicitFStringTypeConversion, value.range()); diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap index df895039e7..57f269e8dc 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap @@ -128,7 +128,7 @@ RUF010.py:13:5: RUF010 [*] Use conversion in f-string 15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | ^^^^^^^^ RUF010 16 | -17 | f"{foo(bla)}" # OK +17 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | = help: Replace f-string function call with conversion @@ -139,7 +139,7 @@ RUF010.py:13:5: RUF010 [*] Use conversion in f-string 13 |-f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 13 |+f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 14 14 | -15 15 | f"{foo(bla)}" # OK +15 15 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 16 16 | RUF010.py:13:19: RUF010 [*] Use conversion in f-string @@ -149,7 +149,7 @@ RUF010.py:13:19: RUF010 [*] Use conversion in f-string 15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | ^^^^^^^^^ RUF010 16 | -17 | f"{foo(bla)}" # OK +17 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | = help: Replace f-string function call with conversion @@ -160,7 +160,7 @@ RUF010.py:13:19: RUF010 [*] Use conversion in f-string 13 |-f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 13 |+f"{(str(bla))}, {bla!r}, {(ascii(bla))}" # RUF010 14 14 | -15 15 | f"{foo(bla)}" # OK +15 15 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 16 16 | RUF010.py:13:34: RUF010 [*] Use conversion in f-string @@ -170,7 +170,7 @@ RUF010.py:13:34: RUF010 [*] Use conversion in f-string 15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | ^^^^^^^^^^ RUF010 16 | -17 | f"{foo(bla)}" # OK +17 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | = help: Replace f-string function call with conversion @@ -181,7 +181,49 @@ RUF010.py:13:34: RUF010 [*] Use conversion in f-string 13 |-f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 13 |+f"{(str(bla))}, {(repr(bla))}, {bla!a}" # RUF010 14 14 | -15 15 | f"{foo(bla)}" # OK +15 15 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 16 16 | +RUF010.py:15:14: RUF010 [*] Use conversion in f-string + | +15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 +16 | +17 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 + | ^^^^^^^^^ RUF010 +18 | +19 | f"{foo(bla)}" # OK + | + = help: Replace f-string function call with conversion + +ℹ Fix +12 12 | +13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 +14 14 | +15 |-f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 + 15 |+f"{bla!s}, {bla!r}, {(ascii(bla))}" # RUF010 +16 16 | +17 17 | f"{foo(bla)}" # OK +18 18 | + +RUF010.py:15:29: RUF010 [*] Use conversion in f-string + | +15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 +16 | +17 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 + | ^^^^^^^^^^ RUF010 +18 | +19 | f"{foo(bla)}" # OK + | + = help: Replace f-string function call with conversion + +ℹ Fix +12 12 | +13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 +14 14 | +15 |-f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 + 15 |+f"{bla!s}, {(repr(bla))}, {bla!a}" # RUF010 +16 16 | +17 17 | f"{foo(bla)}" # OK +18 18 | + From 7b0fb1a3b4abae2351cc9e562fc24b07a9efc36c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 Jun 2023 22:37:07 -0400 Subject: [PATCH 41/41] Respect noqa directives on `ImportFrom` parents for type-checking rules (#4889) --- .../fixtures/flake8_type_checking/TCH002.py | 14 ++++++ crates/ruff/src/checkers/ast/mod.rs | 18 ++------ .../runtime_import_in_type_checking_block.rs | 5 ++- .../rules/typing_only_runtime_import.rs | 43 ++++++++++--------- crates/ruff_python_semantic/src/binding.rs | 14 ++++++ 5 files changed, 59 insertions(+), 35 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py b/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py index 7b082da74e..82d6d2f10b 100644 --- a/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py +++ b/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py @@ -150,3 +150,17 @@ def f(): def f(): import pandas as pd + + +def f(): + from pandas import DataFrame # noqa: TCH002 + + x: DataFrame = 2 + + +def f(): + from pandas import ( # noqa: TCH002 + DataFrame, + ) + + x: DataFrame = 2 diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 08f9508c85..2eda953a01 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -4410,13 +4410,8 @@ impl<'a> Checker<'a> { }, binding.trimmed_range(&self.semantic_model, self.locator), ); - if let Some(parent) = binding.source { - let parent = self.semantic_model.stmts[parent]; - if parent.is_import_from_stmt() - && parent.range().contains_range(binding.range) - { - diagnostic.set_parent(parent.start()); - } + if let Some(range) = binding.parent_range(&self.semantic_model) { + diagnostic.set_parent(range.start()); } self.diagnostics.push(diagnostic); } @@ -5044,13 +5039,8 @@ impl<'a> Checker<'a> { }, binding.trimmed_range(&self.semantic_model, self.locator), ); - if let Some(parent) = binding - .source - .map(|source| &self.semantic_model.stmts[source]) - { - if parent.is_import_from_stmt() { - diagnostic.set_parent(parent.start()); - } + if let Some(range) = binding.parent_range(&self.semantic_model) { + diagnostic.set_parent(range.start()); } diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index a6c0497596..a2da8d1c98 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -86,8 +86,11 @@ pub(crate) fn runtime_import_in_type_checking_block( RuntimeImportInTypeCheckingBlock { qualified_name: qualified_name.to_string(), }, - binding.range, + binding.trimmed_range(checker.semantic_model(), checker.locator), ); + if let Some(range) = binding.parent_range(checker.semantic_model()) { + diagnostic.set_parent(range.start()); + } if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index a9186c91db..085c565271 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -1,4 +1,4 @@ -use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; +use ruff_diagnostics::{AutofixKind, Diagnostic, DiagnosticKind, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_semantic::binding::Binding; @@ -263,7 +263,7 @@ pub(crate) fn typing_only_runtime_import( .unwrap(); // Categorize the import. - let mut diagnostic = match categorize( + let kind: DiagnosticKind = match categorize( qualified_name, Some(level), &checker.settings.src, @@ -272,32 +272,35 @@ pub(crate) fn typing_only_runtime_import( checker.settings.target_version, ) { ImportSection::Known(ImportType::LocalFolder | ImportType::FirstParty) => { - Diagnostic::new( - TypingOnlyFirstPartyImport { - qualified_name: qualified_name.to_string(), - }, - binding.range, - ) + TypingOnlyFirstPartyImport { + qualified_name: qualified_name.to_string(), + } + .into() } ImportSection::Known(ImportType::ThirdParty) | ImportSection::UserDefined(_) => { - Diagnostic::new( - TypingOnlyThirdPartyImport { - qualified_name: qualified_name.to_string(), - }, - binding.range, - ) - } - ImportSection::Known(ImportType::StandardLibrary) => Diagnostic::new( - TypingOnlyStandardLibraryImport { + TypingOnlyThirdPartyImport { qualified_name: qualified_name.to_string(), - }, - binding.range, - ), + } + .into() + } + ImportSection::Known(ImportType::StandardLibrary) => TypingOnlyStandardLibraryImport { + qualified_name: qualified_name.to_string(), + } + .into(), + ImportSection::Known(ImportType::Future) => { unreachable!("`__future__` imports should be marked as used") } }; + let mut diagnostic = Diagnostic::new( + kind, + binding.trimmed_range(checker.semantic_model(), checker.locator), + ); + if let Some(range) = binding.parent_range(checker.semantic_model()) { + diagnostic.set_parent(range.start()); + } + if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { // Step 1) Remove the import. diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index dd862e8ce6..6d30793aa4 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -2,6 +2,7 @@ use std::ops::{Deref, DerefMut}; use bitflags::bitflags; use ruff_text_size::TextRange; +use rustpython_parser::ast::Ranged; use ruff_index::{newtype_index, IndexSlice, IndexVec}; use ruff_python_ast::helpers; @@ -143,6 +144,19 @@ impl<'a> Binding<'a> { _ => self.range, } } + + /// Returns the range of the binding's parent. + pub fn parent_range(&self, semantic_model: &SemanticModel) -> Option { + self.source + .map(|node_id| semantic_model.stmts[node_id]) + .and_then(|parent| { + if parent.is_import_from_stmt() { + Some(parent.range()) + } else { + None + } + }) + } } bitflags! {