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 00000000000000..3598e760507efe --- /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 fd722a6ccf5997..08a68dabb8fc61 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 36d927be2a5949..d2bb6a83dea721 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 88938b8266ee5e..b4db7662f566dc 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 0e9db27b15877e..7e23c757958bf1 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 885b0097ee1d2b..f8b1b7a63ddcfd 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 00000000000000..892b7f2d228716 --- /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 cd64f26de63e09..cc84ae492c3fbc 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 +```