From be0b8859ee5387fc213d89a7c9b56eeb9a0a9a1b Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 25 Sep 2024 13:32:07 +0200 Subject: [PATCH 1/2] Use BestFitParenthesize layout for literals and match as --- .../pattern/pattern_maybe_parenthesize.py | 101 +++++++ .../src/other/match_case.rs | 55 ++-- .../ruff_python_formatter/src/pattern/mod.rs | 73 ++++- .../src/pattern/pattern_match_as.rs | 13 +- .../src/pattern/pattern_match_value.rs | 11 +- crates/ruff_python_formatter/src/preview.rs | 6 + ...attern__pattern_maybe_parenthesize.py.snap | 267 ++++++++++++++++++ .../snapshots/format@statement__match.py.snap | 28 +- 8 files changed, 520 insertions(+), 34 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@pattern__pattern_maybe_parenthesize.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py new file mode 100644 index 0000000000000..3598e760507ef --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py @@ -0,0 +1,101 @@ +# Patterns that use BestFit should be parenthesized if they exceed the configured line width +# but fit within parentheses. +match x: + case ( + "averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsPar" + ): + pass + + +match x: + case ( + b"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsPa" + ): + pass + +match x: + case ( + f"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsPa" + ): + pass + + +match x: + case ( + 5444444444444444444444444444444444444444444444444444444444444444444444444444444j + ): + pass + + +match x: + case ( + 5444444444444444444444444444444444444444444444444444444444444444444444444444444 + ): + pass + + +match x: + case ( + 5.44444444444444444444444444444444444444444444444444444444444444444444444444444 + ): + pass + + +match x: + case ( + averyLongIdentThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenth + ): + pass + + +# But they aren't parenthesized when they exceed the line length even parenthesized +match x: + case "averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenthesized": + pass + + +match x: + case b"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenthesized": + pass + +match x: + case f"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenthesized": + pass + + +match x: + case 54444444444444444444444444444444444444444444444444444444444444444444444444444444444j: + pass + + +match x: + case 5444444444444444444444444444444444444444444444444444444444444444444444444444444444: + pass + + +match x: + case 5.444444444444444444444444444444444444444444444444444444444444444444444444444444444: + pass + + +match x: + case averyLongIdentifierThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenthesized: + pass + + +# It uses the Multiline layout when there's an alias. +match x: + case ( + averyLongIdentifierThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenthe as b + ): + pass + + + +match x: + case ( + "an implicit concatenated" "string literal" "in a match case" "that goes over multiple lines" + ): + pass + + diff --git a/crates/ruff_python_formatter/src/other/match_case.rs b/crates/ruff_python_formatter/src/other/match_case.rs index fd722a6ccf599..08a68dabb8fc6 100644 --- a/crates/ruff_python_formatter/src/other/match_case.rs +++ b/crates/ruff_python_formatter/src/other/match_case.rs @@ -4,7 +4,9 @@ use ruff_python_ast::MatchCase; use crate::builders::parenthesize_if_expands; use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, Parentheses}; +use crate::pattern::maybe_parenthesize_pattern; use crate::prelude::*; +use crate::preview::is_match_case_parentheses_enabled; use crate::statement::clause::{clause_body, clause_header, ClauseHeader}; use crate::statement::suite::SuiteKind; @@ -34,6 +36,32 @@ impl FormatNodeRule for FormatMatchCase { let comments = f.context().comments().clone(); let dangling_item_comments = comments.dangling(item); + let format_pattern = format_with(|f| { + if is_match_case_parentheses_enabled(f.context()) { + maybe_parenthesize_pattern(pattern, item).fmt(f) + } else { + let has_comments = + comments.has_leading(pattern) || comments.has_trailing_own_line(pattern); + + if has_comments { + pattern.format().with_options(Parentheses::Always).fmt(f) + } else { + match pattern.needs_parentheses(item.as_any_node_ref(), f.context()) { + OptionalParentheses::Multiline => parenthesize_if_expands( + &pattern.format().with_options(Parentheses::Never), + ) + .fmt(f), + OptionalParentheses::Always => { + pattern.format().with_options(Parentheses::Always).fmt(f) + } + OptionalParentheses::Never | OptionalParentheses::BestFit => { + pattern.format().with_options(Parentheses::Never).fmt(f) + } + } + } + } + }); + write!( f, [ @@ -41,32 +69,7 @@ impl FormatNodeRule for FormatMatchCase { ClauseHeader::MatchCase(item), dangling_item_comments, &format_with(|f| { - write!(f, [token("case"), space()])?; - - let has_comments = comments.has_leading(pattern) - || comments.has_trailing_own_line(pattern); - - if has_comments { - pattern.format().with_options(Parentheses::Always).fmt(f)?; - } else { - match pattern.needs_parentheses(item.as_any_node_ref(), f.context()) { - OptionalParentheses::Multiline => { - parenthesize_if_expands( - &pattern.format().with_options(Parentheses::Never), - ) - .fmt(f)?; - } - OptionalParentheses::Always => { - pattern.format().with_options(Parentheses::Always).fmt(f)?; - } - OptionalParentheses::Never => { - pattern.format().with_options(Parentheses::Never).fmt(f)?; - } - OptionalParentheses::BestFit => { - pattern.format().with_options(Parentheses::Never).fmt(f)?; - } - } - } + write!(f, [token("case"), space(), format_pattern])?; if let Some(guard) = guard { write!(f, [space(), token("if"), space(), guard.format()])?; diff --git a/crates/ruff_python_formatter/src/pattern/mod.rs b/crates/ruff_python_formatter/src/pattern/mod.rs index 36d927be2a594..d2bb6a83dea72 100644 --- a/crates/ruff_python_formatter/src/pattern/mod.rs +++ b/crates/ruff_python_formatter/src/pattern/mod.rs @@ -1,14 +1,16 @@ use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions}; use ruff_python_ast::AnyNodeRef; -use ruff_python_ast::Pattern; +use ruff_python_ast::{MatchCase, Pattern}; use ruff_python_trivia::CommentRanges; use ruff_python_trivia::{ first_non_trivia_token, BackwardsTokenizer, SimpleToken, SimpleTokenKind, }; use ruff_text_size::Ranged; +use crate::builders::parenthesize_if_expands; +use crate::context::{NodeLevel, WithNodeLevel}; use crate::expression::parentheses::{ - parenthesized, NeedsParentheses, OptionalParentheses, Parentheses, + optional_parentheses, parenthesized, NeedsParentheses, OptionalParentheses, Parentheses, }; use crate::prelude::*; @@ -150,3 +152,70 @@ impl NeedsParentheses for Pattern { } } } + +pub(crate) fn maybe_parenthesize_pattern<'a>( + pattern: &'a Pattern, + case: &'a MatchCase, +) -> MaybeParenthesizePattern<'a> { + MaybeParenthesizePattern { pattern, case } +} + +#[derive(Debug)] +pub(crate) struct MaybeParenthesizePattern<'a> { + pattern: &'a Pattern, + case: &'a MatchCase, +} + +impl Format> for MaybeParenthesizePattern<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let MaybeParenthesizePattern { pattern, case } = self; + + let comments = f.context().comments(); + let pattern_comments = comments.leading_dangling_trailing(*pattern); + + // If the pattern has comments, we always want to preserve the parentheses. This also + // ensures that we correctly handle parenthesized comments, and don't need to worry about + // them in the implementation below. + if pattern_comments.has_leading() || pattern_comments.has_trailing_own_line() { + return pattern.format().with_options(Parentheses::Always).fmt(f); + } + + let needs_parentheses = pattern.needs_parentheses(AnyNodeRef::from(*case), f.context()); + + match needs_parentheses { + OptionalParentheses::Always => { + pattern.format().with_options(Parentheses::Always).fmt(f) + } + OptionalParentheses::Never => pattern.format().with_options(Parentheses::Never).fmt(f), + OptionalParentheses::Multiline => { + if can_pattern_omit_optional_parentheses(pattern, f.context()) { + optional_parentheses(&pattern.format().with_options(Parentheses::Never)).fmt(f) + } else { + parenthesize_if_expands(&pattern.format().with_options(Parentheses::Never)) + .fmt(f) + } + } + OptionalParentheses::BestFit => { + if pattern_comments.has_trailing() { + pattern.format().with_options(Parentheses::Always).fmt(f) + } else { + // The group id is necessary because the nested expressions may reference it. + let group_id = f.group_id("optional_parentheses"); + let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f); + + best_fit_parenthesize(&pattern.format().with_options(Parentheses::Never)) + .with_group_id(Some(group_id)) + .fmt(f) + } + } + } + } +} + +pub(crate) fn can_pattern_omit_optional_parentheses( + _pattern: &Pattern, + _context: &PyFormatContext, +) -> bool { + // TODO Implement + false +} diff --git a/crates/ruff_python_formatter/src/pattern/pattern_match_as.rs b/crates/ruff_python_formatter/src/pattern/pattern_match_as.rs index 88938b8266ee5..b4db7662f566d 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_match_as.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_match_as.rs @@ -5,6 +5,7 @@ use ruff_python_ast::PatternMatchAs; use crate::comments::dangling_comments; use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::prelude::*; +use crate::preview::is_match_case_parentheses_enabled; #[derive(Default)] pub struct FormatPatternMatchAs; @@ -54,8 +55,16 @@ impl NeedsParentheses for PatternMatchAs { fn needs_parentheses( &self, _parent: AnyNodeRef, - _context: &PyFormatContext, + context: &PyFormatContext, ) -> OptionalParentheses { - OptionalParentheses::Multiline + if is_match_case_parentheses_enabled(context) { + if self.name.is_some() { + OptionalParentheses::Multiline + } else { + OptionalParentheses::BestFit + } + } else { + OptionalParentheses::Multiline + } } } diff --git a/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs b/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs index 0e9db27b15877..7e23c757958bf 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs @@ -3,6 +3,7 @@ use ruff_python_ast::PatternMatchValue; use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, Parentheses}; use crate::prelude::*; +use crate::preview::is_match_case_parentheses_enabled; #[derive(Default)] pub struct FormatPatternMatchValue; @@ -17,9 +18,13 @@ impl FormatNodeRule for FormatPatternMatchValue { impl NeedsParentheses for PatternMatchValue { fn needs_parentheses( &self, - _parent: AnyNodeRef, - _context: &PyFormatContext, + parent: AnyNodeRef, + context: &PyFormatContext, ) -> OptionalParentheses { - OptionalParentheses::Never + if is_match_case_parentheses_enabled(context) { + self.value.needs_parentheses(parent, context) + } else { + OptionalParentheses::Never + } } } diff --git a/crates/ruff_python_formatter/src/preview.rs b/crates/ruff_python_formatter/src/preview.rs index 885b0097ee1d2..f8b1b7a63ddcf 100644 --- a/crates/ruff_python_formatter/src/preview.rs +++ b/crates/ruff_python_formatter/src/preview.rs @@ -36,3 +36,9 @@ pub(crate) fn is_empty_parameters_no_unnecessary_parentheses_around_return_value ) -> bool { context.is_preview() } + +/// See [#6933](https://github.com/astral-sh/ruff/issues/6933). +/// This style also covers the black preview styles `remove_redundant_guard_parens` and `parens_for_long_if_clauses_in_case_block `. +pub(crate) fn is_match_case_parentheses_enabled(context: &PyFormatContext) -> bool { + context.is_preview() +} diff --git a/crates/ruff_python_formatter/tests/snapshots/format@pattern__pattern_maybe_parenthesize.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@pattern__pattern_maybe_parenthesize.py.snap new file mode 100644 index 0000000000000..892b7f2d22871 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@pattern__pattern_maybe_parenthesize.py.snap @@ -0,0 +1,267 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py +--- +## Input +```python +# Patterns that use BestFit should be parenthesized if they exceed the configured line width +# but fit within parentheses. +match x: + case ( + "averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsPar" + ): + pass + + +match x: + case ( + b"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsPa" + ): + pass + +match x: + case ( + f"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsPa" + ): + pass + + +match x: + case ( + 5444444444444444444444444444444444444444444444444444444444444444444444444444444j + ): + pass + + +match x: + case ( + 5444444444444444444444444444444444444444444444444444444444444444444444444444444 + ): + pass + + +match x: + case ( + 5.44444444444444444444444444444444444444444444444444444444444444444444444444444 + ): + pass + + +match x: + case ( + averyLongIdentThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenth + ): + pass + + +# But they aren't parenthesized when they exceed the line length even parenthesized +match x: + case "averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenthesized": + pass + + +match x: + case b"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenthesized": + pass + +match x: + case f"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenthesized": + pass + + +match x: + case 54444444444444444444444444444444444444444444444444444444444444444444444444444444444j: + pass + + +match x: + case 5444444444444444444444444444444444444444444444444444444444444444444444444444444444: + pass + + +match x: + case 5.444444444444444444444444444444444444444444444444444444444444444444444444444444444: + pass + + +match x: + case averyLongIdentifierThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenthesized: + pass + + +# It uses the Multiline layout when there's an alias. +match x: + case ( + averyLongIdentifierThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenthe as b + ): + pass + + + +match x: + case ( + "an implicit concatenated" "string literal" "in a match case" "that goes over multiple lines" + ): + pass + + +``` + +## Output +```python +# Patterns that use BestFit should be parenthesized if they exceed the configured line width +# but fit within parentheses. +match x: + case "averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsPar": + pass + + +match x: + case b"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsPa": + pass + +match x: + case f"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsPa": + pass + + +match x: + case 5444444444444444444444444444444444444444444444444444444444444444444444444444444j: + pass + + +match x: + case 5444444444444444444444444444444444444444444444444444444444444444444444444444444: + pass + + +match x: + case 5.44444444444444444444444444444444444444444444444444444444444444444444444444444: + pass + + +match x: + case ( + averyLongIdentThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenth + ): + pass + + +# But they aren't parenthesized when they exceed the line length even parenthesized +match x: + case "averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenthesized": + pass + + +match x: + case b"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenthesized": + pass + +match x: + case f"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenthesized": + pass + + +match x: + case 54444444444444444444444444444444444444444444444444444444444444444444444444444444444j: + pass + + +match x: + case 5444444444444444444444444444444444444444444444444444444444444444444444444444444444: + pass + + +match x: + case 5.444444444444444444444444444444444444444444444444444444444444444444444444444444444: + pass + + +match x: + case ( + averyLongIdentifierThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenthesized + ): + pass + + +# It uses the Multiline layout when there's an alias. +match x: + case ( + averyLongIdentifierThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsParenthe as b + ): + pass + + +match x: + case "an implicit concatenated" "string literal" "in a match case" "that goes over multiple lines": + pass +``` + + +## Preview changes +```diff +--- Stable ++++ Preview +@@ -1,31 +1,43 @@ + # Patterns that use BestFit should be parenthesized if they exceed the configured line width + # but fit within parentheses. + match x: +- case "averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsPar": ++ case ( ++ "averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsPar" ++ ): + pass + + + match x: +- case b"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsPa": ++ case ( ++ b"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsPa" ++ ): + pass + + match x: +- case f"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsPa": ++ case ( ++ f"averyLongStringThatGetsParenthesizedOnceItExceedsTheConfiguredLineWidthFitsPa" ++ ): + pass + + + match x: +- case 5444444444444444444444444444444444444444444444444444444444444444444444444444444j: ++ case ( ++ 5444444444444444444444444444444444444444444444444444444444444444444444444444444j ++ ): + pass + + + match x: +- case 5444444444444444444444444444444444444444444444444444444444444444444444444444444: ++ case ( ++ 5444444444444444444444444444444444444444444444444444444444444444444444444444444 ++ ): + pass + + + match x: +- case 5.44444444444444444444444444444444444444444444444444444444444444444444444444444: ++ case ( ++ 5.44444444444444444444444444444444444444444444444444444444444444444444444444444 ++ ): + pass + + +@@ -82,5 +94,10 @@ + + + match x: +- case "an implicit concatenated" "string literal" "in a match case" "that goes over multiple lines": ++ case ( ++ "an implicit concatenated" ++ "string literal" ++ "in a match case" ++ "that goes over multiple lines" ++ ): + pass +``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap index cd64f26de63e0..cc84ae492c3fb 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap @@ -1235,4 +1235,30 @@ match x: ``` - +## Preview changes +```diff +--- Stable ++++ Preview +@@ -82,7 +82,9 @@ + + + match long_lines: +- case "this is a long line for if condition" if aaaaaaaaahhhhhhhh == 1 and bbbbbbaaaaaaaaaaa == 2: # comment ++ case ( ++ "this is a long line for if condition" ++ ) if aaaaaaaaahhhhhhhh == 1 and bbbbbbaaaaaaaaaaa == 2: # comment + pass + + case "this is a long line for if condition with parentheses" if ( +@@ -249,7 +251,9 @@ + 1 + ): + y = 1 +- case 1: # comment ++ case ( ++ 1 # comment ++ ): + y = 1 + case ( + 1 +``` From 2584f8e3b64e5dd0e719bdd7a89309e3b3718eeb Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 25 Sep 2024 17:14:59 +0200 Subject: [PATCH 2/2] Implement `can_omit_parentheses` for patterns --- .../pattern/pattern_maybe_parenthesize.py | 191 +++++ .../ruff_python_formatter/src/pattern/mod.rs | 195 +++++- .../src/pattern/pattern_match_class.rs | 2 +- .../src/pattern/pattern_match_or.rs | 10 +- .../src/pattern/pattern_match_singleton.rs | 9 +- .../src/pattern/pattern_match_star.rs | 2 + ...attern__pattern_maybe_parenthesize.py.snap | 660 +++++++++++++++++- .../snapshots/format@statement__match.py.snap | 37 +- 8 files changed, 1094 insertions(+), 12 deletions(-) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py index 3598e760507ef..b898fad81bbba 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py @@ -99,3 +99,194 @@ pass +## Patterns ending with a sequence, mapping, class, or parenthesized pattern should break the parenthesized-like pattern first +match x: + case A | [ + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ]: + pass + +match x: + case A | ( + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ): + pass + + +match x: + case A | { + "a": aaaaaa, + "b": bbbbbbbbbbbbbbbb, + "c": cccccccccccccccccc, + "d": ddddddddddddddddddddddddddd, + }: + pass + + +match x: + case A | Class( + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ): + pass + + + +match x: + case A | ( + aaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbb.cccccccccccccccccccccccccccc.ddddddddddddddddddddddd + ): + pass + + +## Patterns starting with a sequence, mapping, class, or parenthesized pattern should break the parenthesized-like pattern first +match x: + case [ + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ] | A: + pass + +match x: + case ( + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ) | A: + pass + + +match x: + case { + "a": aaaaaa, + "b": bbbbbbbbbbbbbbbb, + "c": cccccccccccccccccc, + "d": ddddddddddddddddddddddddddd, + } | A: + pass + + +match x: + case Class( + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ): + pass + + +## Not for non-parenthesized sequence patterns +match x: + case ( + (1) | aaaaaaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccccccccccccccc, + ): + pass + +## Parenthesize patterns that start with a token +match x: + case ( + A( + aaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbb.cccccccccccccccccccccccccccc.ddddddddddddddddddddddd + ) + | B + ): + pass + + +## Always use parentheses for implicitly concatenated strings +match x: + case ( + "implicit" + "concatenated" + "string" + | [aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd] + ): + pass + + +match x: + case ( + b"implicit" + b"concatenated" + b"string" + | [aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd] + ): + pass + + +match x: + case ( + f"implicit" + "concatenated" + "string" + | [aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd] + ): + pass + + +## Complex number expressions and unary expressions + +match x: + case 4 - 3j | [ + aaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccccccccccccccccccccccc, + ]: + pass + + +match x: + case 4 + 3j | [ + aaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccccccccccccccccccccccc, + ]: + pass + + +match x: + case -1 | [ + aaaaaaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccccccccccccccc, + ]: + pass + + + +### Parenthesized patterns +match x: + case (1) | [ + aaaaaaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccccccccccccccc, + ]: + pass + + +match x: + case ( # comment + 1 + ) | [ + aaaaaaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccccccccccccccc, + ]: + pass + + + diff --git a/crates/ruff_python_formatter/src/pattern/mod.rs b/crates/ruff_python_formatter/src/pattern/mod.rs index d2bb6a83dea72..d564a6f97025a 100644 --- a/crates/ruff_python_formatter/src/pattern/mod.rs +++ b/crates/ruff_python_formatter/src/pattern/mod.rs @@ -1,11 +1,12 @@ use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions}; -use ruff_python_ast::AnyNodeRef; +use ruff_python_ast::{AnyNodeRef, Expr}; use ruff_python_ast::{MatchCase, Pattern}; use ruff_python_trivia::CommentRanges; use ruff_python_trivia::{ first_non_trivia_token, BackwardsTokenizer, SimpleToken, SimpleTokenKind, }; use ruff_text_size::Ranged; +use std::cmp::Ordering; use crate::builders::parenthesize_if_expands; use crate::context::{NodeLevel, WithNodeLevel}; @@ -212,10 +213,194 @@ impl Format> for MaybeParenthesizePattern<'_> { } } +/// This function is very similar to [`can_omit_optional_parentheses`] with the only difference that it is for patterns +/// and not expressions. +/// +/// The base idea of the omit optional parentheses layout is to prefer using parentheses of sub-patterns +/// when splitting the pattern over introducing new patterns. For example, prefer splitting the sequence pattern in +/// `a | [b, c]` over splitting before the `|` operator. +/// +/// The layout is only applied when the parenthesized pattern is the first or last item in the pattern. +/// For example, the layout isn't used for `a | [b, c] | d` because that would look weird. pub(crate) fn can_pattern_omit_optional_parentheses( - _pattern: &Pattern, - _context: &PyFormatContext, + pattern: &Pattern, + context: &PyFormatContext, ) -> bool { - // TODO Implement - false + let mut visitor = CanOmitOptionalParenthesesVisitor::default(); + visitor.visit_pattern(pattern, context); + + if !visitor.any_parenthesized_expressions { + // Only use the more complex IR if there's a parenthesized pattern that can be split before + // splitting other patterns. E.g. split the sequence pattern before the string literal `"a" "b" | [a, b, c, d]`. + false + } else if visitor.max_precedence_count > 1 { + false + } else { + // It's complicated + fn has_parentheses_and_is_non_empty(pattern: &Pattern, context: &PyFormatContext) -> bool { + let has_own_non_empty = match pattern { + Pattern::MatchValue(_) + | Pattern::MatchSingleton(_) + | Pattern::MatchStar(_) + | Pattern::MatchAs(_) + | Pattern::MatchOr(_) => false, + Pattern::MatchSequence(sequence) => { + !sequence.patterns.is_empty() || context.comments().has_dangling(pattern) + } + Pattern::MatchMapping(mapping) => { + !mapping.patterns.is_empty() || context.comments().has_dangling(pattern) + } + Pattern::MatchClass(class) => !class.arguments.patterns.is_empty(), + }; + + if has_own_non_empty { + true + } else { + // If the pattern has no own parentheses or it is empty (e.g. ([])), check for surrounding parentheses (that should be preserved). + is_pattern_parenthesized(pattern, context.comments().ranges(), context.source()) + } + } + + visitor + .last + .is_some_and(|last| has_parentheses_and_is_non_empty(last, context)) + || visitor + .first + .pattern() + .is_some_and(|first| has_parentheses_and_is_non_empty(first, context)) + } +} + +#[derive(Debug, Default)] +struct CanOmitOptionalParenthesesVisitor<'input> { + max_precedence: OperatorPrecedence, + max_precedence_count: usize, + any_parenthesized_expressions: bool, + last: Option<&'input Pattern>, + first: First<'input>, +} + +impl<'a> CanOmitOptionalParenthesesVisitor<'a> { + fn visit_pattern(&mut self, pattern: &'a Pattern, context: &PyFormatContext) { + match pattern { + Pattern::MatchSequence(_) | Pattern::MatchMapping(_) => { + self.any_parenthesized_expressions = true; + } + + Pattern::MatchValue(value) => match &*value.value { + Expr::StringLiteral(string) => { + self.update_max_precedence(OperatorPrecedence::String, string.value.len()); + } + Expr::BytesLiteral(bytes) => { + self.update_max_precedence(OperatorPrecedence::String, bytes.value.len()); + } + // F-strings are allowed according to python's grammar but fail with a syntax error at runtime. + // That's why we need to support them for formatting. + Expr::FString(string) => { + self.update_max_precedence( + OperatorPrecedence::String, + string.value.as_slice().len(), + ); + } + + Expr::NumberLiteral(_) | Expr::Attribute(_) | Expr::UnaryOp(_) => { + // require no state update other than visit_pattern does. + } + + // `case 4+3j:` or `case 4-3j: + // Can not contain arbitrary expressions. Limited to complex numbers. + Expr::BinOp(_) => { + self.update_max_precedence(OperatorPrecedence::Additive, 1); + } + + _ => { + debug_assert!( + false, + "Unsupported expression in pattern mach value: {:?}", + value.value + ); + } + }, + Pattern::MatchClass(_) => { + self.any_parenthesized_expressions = true; + + // The pattern doesn't start with a parentheses pattern, but with the class's identifier. + self.first.set_if_none(First::Token); + } + Pattern::MatchStar(_) | Pattern::MatchSingleton(_) | Pattern::MatchAs(_) => {} + Pattern::MatchOr(or_pattern) => { + self.update_max_precedence( + OperatorPrecedence::Or, + or_pattern.patterns.len().saturating_sub(1), + ); + + for pattern in &or_pattern.patterns { + self.visit_sub_pattern(pattern, context); + } + } + } + } + + fn visit_sub_pattern(&mut self, pattern: &'a Pattern, context: &PyFormatContext) { + self.last = Some(pattern); + + // Rule only applies for non-parenthesized patterns. + if is_pattern_parenthesized(pattern, context.comments().ranges(), context.source()) { + self.any_parenthesized_expressions = true; + } else { + self.visit_pattern(pattern, context); + } + + self.first.set_if_none(First::Pattern(pattern)); + } + + fn update_max_precedence(&mut self, precedence: OperatorPrecedence, count: usize) { + match self.max_precedence.cmp(&precedence) { + Ordering::Less => { + self.max_precedence_count = count; + self.max_precedence = precedence; + } + Ordering::Equal => { + self.max_precedence_count += count; + } + Ordering::Greater => {} + } + } +} + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default)] +enum OperatorPrecedence { + #[default] + None, + Additive, + Or, + // Implicit string concatenation + String, +} + +#[derive(Copy, Clone, Debug, Default)] +enum First<'a> { + #[default] + None, + + /// Pattern starts with a non-parentheses token. E.g. `*x` + Token, + + Pattern(&'a Pattern), +} + +impl<'a> First<'a> { + #[inline] + fn set_if_none(&mut self, first: First<'a>) { + if matches!(self, First::None) { + *self = first; + } + } + + fn pattern(self) -> Option<&'a Pattern> { + match self { + First::None | First::Token => None, + First::Pattern(pattern) => Some(pattern), + } + } } diff --git a/crates/ruff_python_formatter/src/pattern/pattern_match_class.rs b/crates/ruff_python_formatter/src/pattern/pattern_match_class.rs index 3235f0f37d7ba..d516940302435 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_match_class.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_match_class.rs @@ -46,7 +46,7 @@ impl NeedsParentheses for PatternMatchClass { // ): ... // ``` if context.comments().has_dangling(self) { - OptionalParentheses::Multiline + OptionalParentheses::Always } else { OptionalParentheses::Never } diff --git a/crates/ruff_python_formatter/src/pattern/pattern_match_or.rs b/crates/ruff_python_formatter/src/pattern/pattern_match_or.rs index 91e141dcc790a..8364491bce5a0 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_match_or.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_match_or.rs @@ -4,9 +4,11 @@ use ruff_python_ast::PatternMatchOr; use crate::comments::leading_comments; use crate::expression::parentheses::{ - in_parentheses_only_soft_line_break_or_space, NeedsParentheses, OptionalParentheses, + in_parentheses_only_group, in_parentheses_only_soft_line_break_or_space, NeedsParentheses, + OptionalParentheses, }; use crate::prelude::*; +use crate::preview::is_match_case_parentheses_enabled; #[derive(Default)] pub struct FormatPatternMatchOr; @@ -41,7 +43,11 @@ impl FormatNodeRule for FormatPatternMatchOr { Ok(()) }); - inner.fmt(f) + if is_match_case_parentheses_enabled(f.context()) { + in_parentheses_only_group(&inner).fmt(f) + } else { + inner.fmt(f) + } } } diff --git a/crates/ruff_python_formatter/src/pattern/pattern_match_singleton.rs b/crates/ruff_python_formatter/src/pattern/pattern_match_singleton.rs index 649b0ec79f040..8e15c3357d286 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_match_singleton.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_match_singleton.rs @@ -3,6 +3,7 @@ use ruff_python_ast::{PatternMatchSingleton, Singleton}; use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::prelude::*; +use crate::preview::is_match_case_parentheses_enabled; #[derive(Default)] pub struct FormatPatternMatchSingleton; @@ -21,8 +22,12 @@ impl NeedsParentheses for PatternMatchSingleton { fn needs_parentheses( &self, _parent: AnyNodeRef, - _context: &PyFormatContext, + context: &PyFormatContext, ) -> OptionalParentheses { - OptionalParentheses::Never + if is_match_case_parentheses_enabled(context) { + OptionalParentheses::BestFit + } else { + OptionalParentheses::Never + } } } diff --git a/crates/ruff_python_formatter/src/pattern/pattern_match_star.rs b/crates/ruff_python_formatter/src/pattern/pattern_match_star.rs index 6930511e9d5b9..515f6b60ca12a 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_match_star.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_match_star.rs @@ -31,6 +31,8 @@ impl NeedsParentheses for PatternMatchStar { _parent: AnyNodeRef, _context: &PyFormatContext, ) -> OptionalParentheses { + // Doesn't matter what we return here because starred patterns can never be used + // outside a sequence pattern. OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@pattern__pattern_maybe_parenthesize.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@pattern__pattern_maybe_parenthesize.py.snap index 892b7f2d22871..29004a1548d04 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@pattern__pattern_maybe_parenthesize.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@pattern__pattern_maybe_parenthesize.py.snap @@ -105,6 +105,197 @@ match x: pass +## Patterns ending with a sequence, mapping, class, or parenthesized pattern should break the parenthesized-like pattern first +match x: + case A | [ + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ]: + pass + +match x: + case A | ( + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ): + pass + + +match x: + case A | { + "a": aaaaaa, + "b": bbbbbbbbbbbbbbbb, + "c": cccccccccccccccccc, + "d": ddddddddddddddddddddddddddd, + }: + pass + + +match x: + case A | Class( + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ): + pass + + + +match x: + case A | ( + aaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbb.cccccccccccccccccccccccccccc.ddddddddddddddddddddddd + ): + pass + + +## Patterns starting with a sequence, mapping, class, or parenthesized pattern should break the parenthesized-like pattern first +match x: + case [ + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ] | A: + pass + +match x: + case ( + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ) | A: + pass + + +match x: + case { + "a": aaaaaa, + "b": bbbbbbbbbbbbbbbb, + "c": cccccccccccccccccc, + "d": ddddddddddddddddddddddddddd, + } | A: + pass + + +match x: + case Class( + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ): + pass + + +## Not for non-parenthesized sequence patterns +match x: + case ( + (1) | aaaaaaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccccccccccccccc, + ): + pass + +## Parenthesize patterns that start with a token +match x: + case ( + A( + aaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbb.cccccccccccccccccccccccccccc.ddddddddddddddddddddddd + ) + | B + ): + pass + + +## Always use parentheses for implicitly concatenated strings +match x: + case ( + "implicit" + "concatenated" + "string" + | [aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd] + ): + pass + + +match x: + case ( + b"implicit" + b"concatenated" + b"string" + | [aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd] + ): + pass + + +match x: + case ( + f"implicit" + "concatenated" + "string" + | [aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd] + ): + pass + + +## Complex number expressions and unary expressions + +match x: + case 4 - 3j | [ + aaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccccccccccccccccccccccc, + ]: + pass + + +match x: + case 4 + 3j | [ + aaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccccccccccccccccccccccc, + ]: + pass + + +match x: + case -1 | [ + aaaaaaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccccccccccccccc, + ]: + pass + + + +### Parenthesized patterns +match x: + case (1) | [ + aaaaaaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccccccccccccccc, + ]: + pass + + +match x: + case ( # comment + 1 + ) | [ + aaaaaaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccccccccccccccc, + ]: + pass + + + ``` ## Output @@ -195,6 +386,228 @@ match x: match x: case "an implicit concatenated" "string literal" "in a match case" "that goes over multiple lines": pass + + +## Patterns ending with a sequence, mapping, class, or parenthesized pattern should break the parenthesized-like pattern first +match x: + case ( + A + | [ + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ] + ): + pass + +match x: + case ( + A + | ( + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ) + ): + pass + + +match x: + case ( + A + | { + "a": aaaaaa, + "b": bbbbbbbbbbbbbbbb, + "c": cccccccccccccccccc, + "d": ddddddddddddddddddddddddddd, + } + ): + pass + + +match x: + case ( + A + | Class( + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ) + ): + pass + + +match x: + case ( + A + | ( + aaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbb.cccccccccccccccccccccccccccc.ddddddddddddddddddddddd + ) + ): + pass + + +## Patterns starting with a sequence, mapping, class, or parenthesized pattern should break the parenthesized-like pattern first +match x: + case ( + [ + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ] + | A + ): + pass + +match x: + case ( + ( + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ) + | A + ): + pass + + +match x: + case ( + { + "a": aaaaaa, + "b": bbbbbbbbbbbbbbbb, + "c": cccccccccccccccccc, + "d": ddddddddddddddddddddddddddd, + } + | A + ): + pass + + +match x: + case Class( + aaaaaa, + bbbbbbbbbbbbbbbb, + cccccccccccccccccc, + ddddddddddddddddddddddddddd, + ): + pass + + +## Not for non-parenthesized sequence patterns +match x: + case ( + (1) + | aaaaaaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccccccccccccccc, + ): + pass + +## Parenthesize patterns that start with a token +match x: + case ( + A( + aaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbb.cccccccccccccccccccccccccccc.ddddddddddddddddddddddd + ) + | B + ): + pass + + +## Always use parentheses for implicitly concatenated strings +match x: + case ( + "implicit" "concatenated" "string" + | [aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd] + ): + pass + + +match x: + case ( + b"implicit" b"concatenated" b"string" + | [aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd] + ): + pass + + +match x: + case ( + f"implicit" "concatenated" "string" + | [aaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccccc, ddddddddddddddddddddddddddd] + ): + pass + + +## Complex number expressions and unary expressions + +match x: + case ( + 4 - 3j + | [ + aaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccccccccccccccccccccccc, + ] + ): + pass + + +match x: + case ( + 4 + 3j + | [ + aaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccccccccccccccccccccccc, + ] + ): + pass + + +match x: + case ( + -1 + | [ + aaaaaaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccccccccccccccc, + ] + ): + pass + + +### Parenthesized patterns +match x: + case ( + (1) + | [ + aaaaaaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccccccccccccccc, + ] + ): + pass + + +match x: + case ( + ( # comment + 1 + ) + | [ + aaaaaaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccccccccccccccc, + ] + ): + pass ``` @@ -252,7 +665,7 @@ match x: pass -@@ -82,5 +94,10 @@ +@@ -82,108 +94,89 @@ match x: @@ -264,4 +677,249 @@ match x: + "that goes over multiple lines" + ): pass + + + ## Patterns ending with a sequence, mapping, class, or parenthesized pattern should break the parenthesized-like pattern first + match x: +- case ( +- A +- | [ +- aaaaaa, +- bbbbbbbbbbbbbbbb, +- cccccccccccccccccc, +- ddddddddddddddddddddddddddd, +- ] +- ): ++ case A | [ ++ aaaaaa, ++ bbbbbbbbbbbbbbbb, ++ cccccccccccccccccc, ++ ddddddddddddddddddddddddddd, ++ ]: + pass + + match x: +- case ( +- A +- | ( +- aaaaaa, +- bbbbbbbbbbbbbbbb, +- cccccccccccccccccc, +- ddddddddddddddddddddddddddd, +- ) ++ case A | ( ++ aaaaaa, ++ bbbbbbbbbbbbbbbb, ++ cccccccccccccccccc, ++ ddddddddddddddddddddddddddd, + ): + pass + + + match x: +- case ( +- A +- | { +- "a": aaaaaa, +- "b": bbbbbbbbbbbbbbbb, +- "c": cccccccccccccccccc, +- "d": ddddddddddddddddddddddddddd, +- } +- ): ++ case A | { ++ "a": aaaaaa, ++ "b": bbbbbbbbbbbbbbbb, ++ "c": cccccccccccccccccc, ++ "d": ddddddddddddddddddddddddddd, ++ }: + pass + + + match x: +- case ( +- A +- | Class( +- aaaaaa, +- bbbbbbbbbbbbbbbb, +- cccccccccccccccccc, +- ddddddddddddddddddddddddddd, +- ) ++ case A | Class( ++ aaaaaa, ++ bbbbbbbbbbbbbbbb, ++ cccccccccccccccccc, ++ ddddddddddddddddddddddddddd, + ): + pass + + + match x: +- case ( +- A +- | ( +- aaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbb.cccccccccccccccccccccccccccc.ddddddddddddddddddddddd +- ) ++ case A | ( ++ aaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbb.cccccccccccccccccccccccccccc.ddddddddddddddddddddddd + ): + pass + + + ## Patterns starting with a sequence, mapping, class, or parenthesized pattern should break the parenthesized-like pattern first + match x: +- case ( +- [ +- aaaaaa, +- bbbbbbbbbbbbbbbb, +- cccccccccccccccccc, +- ddddddddddddddddddddddddddd, +- ] +- | A +- ): ++ case [ ++ aaaaaa, ++ bbbbbbbbbbbbbbbb, ++ cccccccccccccccccc, ++ ddddddddddddddddddddddddddd, ++ ] | A: + pass + + match x: + case ( +- ( +- aaaaaa, +- bbbbbbbbbbbbbbbb, +- cccccccccccccccccc, +- ddddddddddddddddddddddddddd, +- ) +- | A +- ): ++ aaaaaa, ++ bbbbbbbbbbbbbbbb, ++ cccccccccccccccccc, ++ ddddddddddddddddddddddddddd, ++ ) | A: + pass + + + match x: +- case ( +- { +- "a": aaaaaa, +- "b": bbbbbbbbbbbbbbbb, +- "c": cccccccccccccccccc, +- "d": ddddddddddddddddddddddddddd, +- } +- | A +- ): ++ case { ++ "a": aaaaaa, ++ "b": bbbbbbbbbbbbbbbb, ++ "c": cccccccccccccccccc, ++ "d": ddddddddddddddddddddddddddd, ++ } | A: + pass + + +@@ -200,8 +193,7 @@ + ## Not for non-parenthesized sequence patterns + match x: + case ( +- (1) +- | aaaaaaaaaaaaaaaaaaaaaaaaaaaa, ++ (1) | aaaaaaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccccccccccccccc, + ): +@@ -246,63 +238,48 @@ + ## Complex number expressions and unary expressions + + match x: +- case ( +- 4 - 3j +- | [ +- aaaaaaaaaaaaaaaaaaaaaaaa, +- bbbbbbbbbbbbbbbbbbbbbbbbbbbb, +- cccccccccccccccccccccccccccccccccccccccc, +- ] +- ): ++ case 4 - 3j | [ ++ aaaaaaaaaaaaaaaaaaaaaaaa, ++ bbbbbbbbbbbbbbbbbbbbbbbbbbbb, ++ cccccccccccccccccccccccccccccccccccccccc, ++ ]: + pass + + + match x: +- case ( +- 4 + 3j +- | [ +- aaaaaaaaaaaaaaaaaaaaaaaa, +- bbbbbbbbbbbbbbbbbbbbbbbbbbbb, +- cccccccccccccccccccccccccccccccccccccccc, +- ] +- ): ++ case 4 + 3j | [ ++ aaaaaaaaaaaaaaaaaaaaaaaa, ++ bbbbbbbbbbbbbbbbbbbbbbbbbbbb, ++ cccccccccccccccccccccccccccccccccccccccc, ++ ]: + pass + + + match x: +- case ( +- -1 +- | [ +- aaaaaaaaaaaaaaaaaaaaaaaaaaaa, +- bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, +- ccccccccccccccccccccccccccccccccc, +- ] +- ): ++ case -1 | [ ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaa, ++ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, ++ ccccccccccccccccccccccccccccccccc, ++ ]: + pass + + + ### Parenthesized patterns + match x: +- case ( +- (1) +- | [ +- aaaaaaaaaaaaaaaaaaaaaaaaaaaa, +- bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, +- ccccccccccccccccccccccccccccccccc, +- ] +- ): ++ case (1) | [ ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaa, ++ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, ++ ccccccccccccccccccccccccccccccccc, ++ ]: + pass + + + match x: +- case ( +- ( # comment +- 1 +- ) +- | [ +- aaaaaaaaaaaaaaaaaaaaaaaaaaaa, +- bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, +- ccccccccccccccccccccccccccccccccc, +- ] +- ): ++ case ( # comment ++ 1 ++ ) | [ ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaa, ++ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, ++ ccccccccccccccccccccccccccccccccc, ++ ]: + pass ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap index cc84ae492c3fb..038af7c075669 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap @@ -1250,7 +1250,18 @@ match x: pass case "this is a long line for if condition with parentheses" if ( -@@ -249,7 +251,9 @@ +@@ -198,7 +200,9 @@ + # trailing own 2 + ): + pass +- case True: # trailing ++ case ( ++ True # trailing ++ ): + pass + case False: + pass +@@ -249,7 +253,9 @@ 1 ): y = 1 @@ -1261,4 +1272,28 @@ match x: y = 1 case ( 1 +@@ -507,11 +513,8 @@ + pass + + case ( +- ( +- a # trailing +- ) +- | (b) +- ): ++ a # trailing ++ ) | (b): + pass + + case a | b | c: +@@ -525,8 +528,7 @@ + pass + + case ( # end of line +- a +- | b ++ a | b + # own line + ): + pass ```