diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py index b91681ace8..594f1feed1 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py @@ -27,3 +27,7 @@ def g(a,): pass x1 = lambda y: 1 x2 = lambda y,: 1 + +# Ignore trailing comma. +with (a,): # magic trailing comma + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/with.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/with.py index fb213a6c21..03ab7a0a3b 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/with.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/with.py @@ -67,6 +67,24 @@ with ( a as b ): ... +with ( + a as b + # trailing comment +): ... + +with ( + a as ( + # leading comment + b + ) +): ... + +with ( + a as ( + b + # trailing comment + ) +): ... with (a # trailing same line comment # trailing own line comment @@ -78,7 +96,6 @@ with ( as b ): ... - with (a # trailing same line comment # trailing own line comment ) as b: ... @@ -158,3 +175,82 @@ with ( # outer comment CtxManager2(), ) as example: ... + +# Breaking of with items. +with (test # bar + as # foo + ( + # test + foo)): + pass + +with test as ( + # test + foo): + pass + +with (test # bar + as # foo + ( # baz + # test + foo)): + pass + +with (a as b, c as d): + pass + +with ( + a as b, + # foo + c as d +): + pass + +with ( + a as ( # foo + b + ) +): + pass + +with ( + f(a, ) as b + +): + pass + +with (x := 1) as d: + pass + +with (x[1, 2,] as d): + pass + +with (f(a, ) as b, c as d): + pass + +with f(a, ) as b, c as d: + pass + +with ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) as b: + pass + +with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b: + pass + +with ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) as b, c as d: + pass + +with ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b, c as d): + pass + +with ( + (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb) as b, c as d): + pass + +with (foo() as bar, baz() as bop): + pass diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 943b06efd8..56f2936b7c 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -1,11 +1,10 @@ use ruff_formatter::{format_args, write, Argument, Arguments}; use ruff_python_ast::Ranged; -use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{TextRange, TextSize}; use crate::context::{NodeLevel, WithNodeLevel}; +use crate::other::commas::has_magic_trailing_comma; use crate::prelude::*; -use crate::MagicTrailingComma; /// Adds parentheses and indents `content` if it doesn't fit on a line. pub(crate) fn parenthesize_if_expands<'ast, T>(content: &T) -> ParenthesizeIfExpands<'_, 'ast> @@ -194,26 +193,11 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { pub(crate) fn finish(&mut self) -> FormatResult<()> { self.result.and_then(|_| { if let Some(last_end) = self.entries.position() { - let magic_trailing_comma = match self.fmt.options().magic_trailing_comma() { - MagicTrailingComma::Respect => { - let first_token = SimpleTokenizer::new( - self.fmt.context().source(), - TextRange::new(last_end, self.sequence_end), - ) - .skip_trivia() - // Skip over any closing parentheses belonging to the expression - .find(|token| token.kind() != SimpleTokenKind::RParen); - - matches!( - first_token, - Some(SimpleToken { - kind: SimpleTokenKind::Comma, - .. - }) - ) - } - MagicTrailingComma::Ignore => false, - }; + let magic_trailing_comma = has_magic_trailing_comma( + TextRange::new(last_end, self.sequence_end), + self.fmt.options(), + self.fmt.context(), + ); // If there is a single entry, only keep the magic trailing comma, don't add it if // it wasn't there -- unless the trailing comma behavior is set to one-or-more. diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index 902521b94b..11de444cb5 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -247,12 +247,11 @@ pub(crate) struct FormatDanglingOpenParenthesisComments<'a> { impl Format> for FormatDanglingOpenParenthesisComments<'_> { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { - let mut comments = self + for comment in self .comments .iter() - .filter(|comment| comment.is_unformatted()); - - if let Some(comment) = comments.next() { + .filter(|comment| comment.is_unformatted()) + { debug_assert!( comment.line_position().is_end_of_line(), "Expected dangling comment to be at the end of the line" @@ -261,16 +260,11 @@ impl Format> for FormatDanglingOpenParenthesisComments<'_> { write!( f, [ - line_suffix(&format_args![space(), space(), format_comment(comment)]), + line_suffix(&format_args!(space(), space(), format_comment(comment))), expand_parent() ] )?; comment.mark_formatted(); - - debug_assert!( - comments.next().is_none(), - "Expected at most one dangling comment" - ); } Ok(()) diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index c67647227b..13691a334d 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -1130,9 +1130,8 @@ fn handle_with_item_comment<'a>( if comment.end() < as_token.start() { // If before the `as` keyword, then it must be a trailing comment of the context expression. CommentPlacement::trailing(context_expr, comment) - } - // Trailing end of line comment coming after the `as` keyword`. - else if comment.line_position().is_end_of_line() { + } else if comment.line_position().is_end_of_line() { + // Trailing end of line comment coming after the `as` keyword`. CommentPlacement::dangling(comment.enclosing_node(), comment) } else { CommentPlacement::leading(optional_vars, comment) diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs index 0a5693e83c..ee427fd312 100644 --- a/crates/ruff_python_formatter/src/options.rs +++ b/crates/ruff_python_formatter/src/options.rs @@ -11,7 +11,7 @@ use std::str::FromStr; serde(default) )] pub struct PyFormatOptions { - /// Whether we're in a `.py` file or `.pyi` file, which have different rules + /// Whether we're in a `.py` file or `.pyi` file, which have different rules. source_type: PySourceType, /// Specifies the indent style: @@ -27,7 +27,7 @@ pub struct PyFormatOptions { /// The preferred quote style to use (single vs double quotes). quote_style: QuoteStyle, - /// Whether to expand lists or elements if they have a trailing comma such as `(a, b,)` + /// Whether to expand lists or elements if they have a trailing comma such as `(a, b,)`. magic_trailing_comma: MagicTrailingComma, } diff --git a/crates/ruff_python_formatter/src/other/commas.rs b/crates/ruff_python_formatter/src/other/commas.rs new file mode 100644 index 0000000000..326daba425 --- /dev/null +++ b/crates/ruff_python_formatter/src/other/commas.rs @@ -0,0 +1,31 @@ +use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer}; +use ruff_text_size::TextRange; + +use crate::prelude::*; +use crate::{MagicTrailingComma, PyFormatOptions}; + +/// Returns `true` if the range ends with a magic trailing comma (and the magic trailing comma +/// should be respected). +pub(crate) fn has_magic_trailing_comma( + range: TextRange, + options: &PyFormatOptions, + context: &PyFormatContext, +) -> bool { + match options.magic_trailing_comma() { + MagicTrailingComma::Respect => { + let first_token = SimpleTokenizer::new(context.source(), range) + .skip_trivia() + // Skip over any closing parentheses belonging to the expression + .find(|token| token.kind() != SimpleTokenKind::RParen); + + matches!( + first_token, + Some(SimpleToken { + kind: SimpleTokenKind::Comma, + .. + }) + ) + } + MagicTrailingComma::Ignore => false, + } +} diff --git a/crates/ruff_python_formatter/src/other/mod.rs b/crates/ruff_python_formatter/src/other/mod.rs index 4f9e3a52bb..e7eb28ae7f 100644 --- a/crates/ruff_python_formatter/src/other/mod.rs +++ b/crates/ruff_python_formatter/src/other/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod alias; pub(crate) mod arguments; +pub(crate) mod commas; pub(crate) mod comprehension; pub(crate) mod decorator; pub(crate) mod elif_else_clause; diff --git a/crates/ruff_python_formatter/src/other/with_item.rs b/crates/ruff_python_formatter/src/other/with_item.rs index 2bd2d90ba8..a193d9e137 100644 --- a/crates/ruff_python_formatter/src/other/with_item.rs +++ b/crates/ruff_python_formatter/src/other/with_item.rs @@ -1,10 +1,9 @@ +use ruff_formatter::{write, Buffer, FormatResult}; use ruff_python_ast::WithItem; -use ruff_formatter::{write, Buffer, FormatResult}; - -use crate::comments::{leading_comments, trailing_comments, SourceComment}; +use crate::comments::SourceComment; use crate::expression::maybe_parenthesize_expression; -use crate::expression::parentheses::Parenthesize; +use crate::expression::parentheses::{parenthesized, Parentheses, Parenthesize}; use crate::prelude::*; use crate::{FormatNodeRule, PyFormatter}; @@ -22,28 +21,33 @@ impl FormatNodeRule for FormatWithItem { let comments = f.context().comments().clone(); let trailing_as_comments = comments.dangling_comments(item); - maybe_parenthesize_expression(context_expr, item, Parenthesize::IfRequired).fmt(f)?; + write!( + f, + [maybe_parenthesize_expression( + context_expr, + item, + Parenthesize::IfRequired + )] + )?; if let Some(optional_vars) = optional_vars { - write!( - f, - [space(), text("as"), trailing_comments(trailing_as_comments)] - )?; - let leading_var_comments = comments.leading_comments(optional_vars.as_ref()); - if leading_var_comments.is_empty() { - write!(f, [space(), optional_vars.format()])?; + write!(f, [space(), text("as"), space()])?; + + if trailing_as_comments.is_empty() { + write!(f, [optional_vars.format()])?; } else { write!( f, - [ - // Otherwise the comment would end up on the same line as the `as` - hard_line_break(), - leading_comments(leading_var_comments), - optional_vars.format() - ] + [parenthesized( + "(", + &optional_vars.format().with_options(Parentheses::Never), + ")", + ) + .with_dangling_comments(trailing_as_comments)] )?; } } + Ok(()) } diff --git a/crates/ruff_python_formatter/src/statement/stmt_with.rs b/crates/ruff_python_formatter/src/statement/stmt_with.rs index b96e4f7535..c3f555abd2 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_with.rs @@ -4,13 +4,15 @@ use ruff_python_ast::{Ranged, StmtWith}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::TextRange; +use crate::builders::parenthesize_if_expands; use crate::comments::SourceComment; use crate::expression::parentheses::{ in_parentheses_only_soft_line_break_or_space, optional_parentheses, parenthesized, }; +use crate::other::commas; use crate::prelude::*; use crate::statement::clause::{clause_header, ClauseHeader}; -use crate::FormatNodeRule; +use crate::{FormatNodeRule, PyFormatOptions}; #[derive(Default)] pub struct FormatStmtWith; @@ -66,8 +68,8 @@ impl FormatNodeRule for FormatStmtWith { parenthesized("(", &joined, ")") .with_dangling_comments(parenthesized_comments) .fmt(f)?; - } else if are_with_items_parenthesized(item, f.context())? { - optional_parentheses(&format_with(|f| { + } else if should_parenthesize(item, f.options(), f.context())? { + parenthesize_if_expands(&format_with(|f| { let mut joiner = f.join_comma_separated(item.body.first().unwrap().start()); @@ -81,6 +83,16 @@ impl FormatNodeRule for FormatStmtWith { joiner.finish() })) .fmt(f)?; + } else if let [item] = item.items.as_slice() { + // This is similar to `maybe_parenthesize_expression`, but we're not dealing with an + // expression here, it's a `WithItem`. + if comments.has_leading_comments(item) + || comments.has_trailing_own_line_comments(item) + { + optional_parentheses(&item.format()).fmt(f)?; + } else { + item.format().fmt(f)?; + } } else { f.join_with(format_args![text(","), space()]) .entries(item.items.iter().formatted()) @@ -105,14 +117,50 @@ impl FormatNodeRule for FormatStmtWith { } } -fn are_with_items_parenthesized(with: &StmtWith, context: &PyFormatContext) -> FormatResult { - let first_with_item = with - .items - .first() - .ok_or(FormatError::syntax_error("Expected at least one with item"))?; - let before_first_with_item = TextRange::new(with.start(), first_with_item.start()); +/// Returns `true` if the `with` items should be parenthesized, if at least one item expands. +/// +/// Black parenthesizes `with` items if there's more than one item and they're already +/// parenthesized, _or_ there's a single item with a trailing comma. +fn should_parenthesize( + with: &StmtWith, + options: &PyFormatOptions, + context: &PyFormatContext, +) -> FormatResult { + if has_magic_trailing_comma(with, options, context) { + return Ok(true); + } - let mut tokenizer = SimpleTokenizer::new(context.source(), before_first_with_item) + if are_with_items_parenthesized(with, context)? { + return Ok(true); + } + + Ok(false) +} + +fn has_magic_trailing_comma( + with: &StmtWith, + options: &PyFormatOptions, + context: &PyFormatContext, +) -> bool { + let Some(last_item) = with.items.last() else { + return false; + }; + + commas::has_magic_trailing_comma( + TextRange::new(last_item.end(), with.end()), + options, + context, + ) +} + +fn are_with_items_parenthesized(with: &StmtWith, context: &PyFormatContext) -> FormatResult { + let [first_item, _, ..] = with.items.as_slice() else { + return Ok(false); + }; + + let before_first_item = TextRange::new(with.start(), first_item.start()); + + let mut tokenizer = SimpleTokenizer::new(context.source(), before_first_item) .skip_trivia() .skip_while(|t| t.kind() == SimpleTokenKind::Async); diff --git a/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap index f0beff4915..a799ac9bc6 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap @@ -33,6 +33,10 @@ def g(a,): pass x1 = lambda y: 1 x2 = lambda y,: 1 + +# Ignore trailing comma. +with (a,): # magic trailing comma + ... ``` ## Outputs @@ -78,6 +82,12 @@ def g( x1 = lambda y: 1 x2 = lambda y,: 1 + +# Ignore trailing comma. +with ( + a, +): # magic trailing comma + ... ``` @@ -117,6 +127,10 @@ def g(a): x1 = lambda y: 1 x2 = lambda y,: 1 + +# Ignore trailing comma. +with a: # magic trailing comma + ... ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap index 6e16935581..1c54019b65 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap @@ -73,6 +73,24 @@ with ( a as b ): ... +with ( + a as b + # trailing comment +): ... + +with ( + a as ( + # leading comment + b + ) +): ... + +with ( + a as ( + b + # trailing comment + ) +): ... with (a # trailing same line comment # trailing own line comment @@ -84,7 +102,6 @@ with ( as b ): ... - with (a # trailing same line comment # trailing own line comment ) as b: ... @@ -164,6 +181,85 @@ with ( # outer comment CtxManager2(), ) as example: ... + +# Breaking of with items. +with (test # bar + as # foo + ( + # test + foo)): + pass + +with test as ( + # test + foo): + pass + +with (test # bar + as # foo + ( # baz + # test + foo)): + pass + +with (a as b, c as d): + pass + +with ( + a as b, + # foo + c as d +): + pass + +with ( + a as ( # foo + b + ) +): + pass + +with ( + f(a, ) as b + +): + pass + +with (x := 1) as d: + pass + +with (x[1, 2,] as d): + pass + +with (f(a, ) as b, c as d): + pass + +with f(a, ) as b, c as d: + pass + +with ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) as b: + pass + +with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b: + pass + +with ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) as b, c as d: + pass + +with ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b, c as d): + pass + +with ( + (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb) as b, c as d): + pass + +with (foo() as bar, baz() as bop): + pass ``` ## Output @@ -192,19 +288,19 @@ with ( with ( - a as # a # as - # own line - b, # b # comma + a as ( # a # as + # own line + b + ), # b # comma c, # c ): # colon ... # body # body trailing own -with ( - a as # a # as +with a as ( # a # as # own line - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb # b -): + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): # b pass @@ -239,34 +335,50 @@ with ( ): ... - with ( - a # trailing same line comment - # trailing own line comment -) as b: - ... - -with ( - a # trailing same line comment - # trailing own line comment -) as b: - ... - - -with ( - a # trailing same line comment - # trailing own line comment -) as b: - ... - -with ( - ( - a - # trailing own line comment - ) as b # trailing as same line comment # trailing b same line comment + a as b + # trailing comment ): ... +with a as ( + # leading comment + b +): + ... + +with a as ( + b + # trailing comment +): + ... + +with ( + a # trailing same line comment + # trailing own line comment +) as b: + ... + +with ( + a # trailing same line comment + # trailing own line comment +) as b: + ... + +with ( + a # trailing same line comment + # trailing own line comment +) as b: + ... + +with ( + a + # trailing own line comment +) as ( # trailing as same line comment + b +): # trailing b same line comment + ... + with ( [ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -334,6 +446,93 @@ with ( # outer comment CtxManager2(), ) as example: ... + +# Breaking of with items. +with test as ( # bar # foo + # test + foo +): + pass + +with test as ( + # test + foo +): + pass + +with test as ( # bar # foo # baz + # test + foo +): + pass + +with a as b, c as d: + pass + +with ( + a as b, + # foo + c as d, +): + pass + +with a as ( # foo + b +): + pass + +with f( + a, +) as b: + pass + +with (x := 1) as d: + pass + +with x[ + 1, + 2, +] as d: + pass + +with ( + f( + a, + ) as b, + c as d, +): + pass + +with f( + a, +) as b, c as d: + pass + +with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b: + pass + +with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b: + pass + +with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b, c as d: + pass + +with ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b, + c as d, +): + pass + +with ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b, + c as d, +): + pass + +with foo() as bar, baz() as bop: + pass ```