Handle pattern parentheses in FormatPattern (#6800)

## Summary

This PR fixes the duplicate-parenthesis problem that's visible in the
tests from https://github.com/astral-sh/ruff/pull/6799. The issue is
that we might have parentheses around the entire match-case pattern,
like in `(1)` here:

```python
match foo:
    case (1):
        y = 0
```

In this case, the inner expression (`1`) will _think_ it's
parenthesized, but we'll _also_ detect the parentheses at the case level
-- so they get rendered by the case, then again by the expression.
Instead, if we detect parentheses at the case level, we can force-off
the parentheses for the pattern using a design similar to the way we
handle parentheses on expressions.

Closes https://github.com/astral-sh/ruff/issues/6753.

## Test Plan

`cargo test`
This commit is contained in:
Charlie Marsh 2023-08-24 23:45:49 -04:00 committed by GitHub
parent 281ce56dc1
commit 6f23469e00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 406 additions and 124 deletions

View file

@ -1,6 +1,11 @@
use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule};
use ruff_python_ast::Pattern;
use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{Pattern, Ranged};
use ruff_python_trivia::{first_non_trivia_token, SimpleToken, SimpleTokenKind, SimpleTokenizer};
use crate::expression::parentheses::{
parenthesized, NeedsParentheses, OptionalParentheses, Parentheses,
};
use crate::prelude::*;
pub(crate) mod pattern_match_as;
@ -12,20 +17,64 @@ pub(crate) mod pattern_match_singleton;
pub(crate) mod pattern_match_star;
pub(crate) mod pattern_match_value;
#[derive(Default)]
pub struct FormatPattern;
#[derive(Copy, Clone, PartialEq, Eq, Default)]
pub struct FormatPattern {
parentheses: Parentheses,
}
impl FormatRuleWithOptions<Pattern, PyFormatContext<'_>> for FormatPattern {
type Options = Parentheses;
fn with_options(mut self, options: Self::Options) -> Self {
self.parentheses = options;
self
}
}
impl FormatRule<Pattern, PyFormatContext<'_>> for FormatPattern {
fn fmt(&self, item: &Pattern, f: &mut PyFormatter) -> FormatResult<()> {
match item {
Pattern::MatchValue(p) => p.format().fmt(f),
Pattern::MatchSingleton(p) => p.format().fmt(f),
Pattern::MatchSequence(p) => p.format().fmt(f),
Pattern::MatchMapping(p) => p.format().fmt(f),
Pattern::MatchClass(p) => p.format().fmt(f),
Pattern::MatchStar(p) => p.format().fmt(f),
Pattern::MatchAs(p) => p.format().fmt(f),
Pattern::MatchOr(p) => p.format().fmt(f),
fn fmt(&self, pattern: &Pattern, f: &mut PyFormatter) -> FormatResult<()> {
let format_pattern = format_with(|f| match pattern {
Pattern::MatchValue(pattern) => pattern.format().fmt(f),
Pattern::MatchSingleton(pattern) => pattern.format().fmt(f),
Pattern::MatchSequence(pattern) => pattern.format().fmt(f),
Pattern::MatchMapping(pattern) => pattern.format().fmt(f),
Pattern::MatchClass(pattern) => pattern.format().fmt(f),
Pattern::MatchStar(pattern) => pattern.format().fmt(f),
Pattern::MatchAs(pattern) => pattern.format().fmt(f),
Pattern::MatchOr(pattern) => pattern.format().fmt(f),
});
let parenthesize = match self.parentheses {
Parentheses::Preserve => is_pattern_parenthesized(pattern, f.context().source()),
Parentheses::Always => true,
Parentheses::Never => false,
};
if parenthesize {
let comments = f.context().comments().clone();
// Any comments on the open parenthesis.
//
// For example, `# comment` in:
// ```python
// ( # comment
// 1
// )
// ```
let open_parenthesis_comment = comments
.leading(pattern)
.first()
.filter(|comment| comment.line_position().is_end_of_line());
parenthesized("(", &format_pattern, ")")
.with_dangling_comments(
open_parenthesis_comment
.map(std::slice::from_ref)
.unwrap_or_default(),
)
.fmt(f)
} else {
format_pattern.fmt(f)
}
}
}
@ -34,7 +83,7 @@ impl<'ast> AsFormat<PyFormatContext<'ast>> for Pattern {
type Format<'a> = FormatRefWithRule<'a, Pattern, FormatPattern, PyFormatContext<'ast>>;
fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(self, FormatPattern)
FormatRefWithRule::new(self, FormatPattern::default())
}
}
@ -42,6 +91,49 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for Pattern {
type Format = FormatOwnedWithRule<Pattern, FormatPattern, PyFormatContext<'ast>>;
fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(self, FormatPattern)
FormatOwnedWithRule::new(self, FormatPattern::default())
}
}
fn is_pattern_parenthesized(pattern: &Pattern, contents: &str) -> bool {
// First test if there's a closing parentheses because it tends to be cheaper.
if matches!(
first_non_trivia_token(pattern.end(), contents),
Some(SimpleToken {
kind: SimpleTokenKind::RParen,
..
})
) {
let mut tokenizer =
SimpleTokenizer::up_to_without_back_comment(pattern.start(), contents).skip_trivia();
matches!(
tokenizer.next_back(),
Some(SimpleToken {
kind: SimpleTokenKind::LParen,
..
})
)
} else {
false
}
}
impl NeedsParentheses for Pattern {
fn needs_parentheses(
&self,
parent: AnyNodeRef,
context: &PyFormatContext,
) -> OptionalParentheses {
match self {
Pattern::MatchValue(pattern) => pattern.needs_parentheses(parent, context),
Pattern::MatchSingleton(pattern) => pattern.needs_parentheses(parent, context),
Pattern::MatchSequence(pattern) => pattern.needs_parentheses(parent, context),
Pattern::MatchMapping(pattern) => pattern.needs_parentheses(parent, context),
Pattern::MatchClass(pattern) => pattern.needs_parentheses(parent, context),
Pattern::MatchStar(pattern) => pattern.needs_parentheses(parent, context),
Pattern::MatchAs(pattern) => pattern.needs_parentheses(parent, context),
Pattern::MatchOr(pattern) => pattern.needs_parentheses(parent, context),
}
}
}

View file

@ -1,8 +1,9 @@
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::{Pattern, PatternMatchAs};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::PatternMatchAs;
use crate::comments::{dangling_comments, SourceComment};
use crate::expression::parentheses::parenthesized;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::prelude::*;
use crate::{FormatNodeRule, PyFormatter};
@ -21,18 +22,7 @@ impl FormatNodeRule<PatternMatchAs> for FormatPatternMatchAs {
if let Some(name) = name {
if let Some(pattern) = pattern {
// Parenthesize nested `PatternMatchAs` like `(a as b) as c`.
if matches!(
pattern.as_ref(),
Pattern::MatchAs(PatternMatchAs {
pattern: Some(_),
..
})
) {
parenthesized("(", &pattern.format(), ")").fmt(f)?;
} else {
pattern.format().fmt(f)?;
}
pattern.format().fmt(f)?;
if comments.has_trailing(pattern.as_ref()) {
write!(f, [hard_line_break()])?;
@ -68,3 +58,13 @@ impl FormatNodeRule<PatternMatchAs> for FormatPatternMatchAs {
Ok(())
}
}
impl NeedsParentheses for PatternMatchAs {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Multiline
}
}

View file

@ -1,6 +1,9 @@
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::PatternMatchClass;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::prelude::*;
use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter};
#[derive(Default)]
@ -17,3 +20,13 @@ impl FormatNodeRule<PatternMatchClass> for FormatPatternMatchClass {
)
}
}
impl NeedsParentheses for PatternMatchClass {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Never
}
}

View file

@ -1,6 +1,9 @@
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::PatternMatchMapping;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::prelude::*;
use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter};
#[derive(Default)]
@ -17,3 +20,13 @@ impl FormatNodeRule<PatternMatchMapping> for FormatPatternMatchMapping {
)
}
}
impl NeedsParentheses for PatternMatchMapping {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Never
}
}

View file

@ -1,6 +1,9 @@
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::PatternMatchOr;
use crate::context::PyFormatContext;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter};
#[derive(Default)]
@ -17,3 +20,13 @@ impl FormatNodeRule<PatternMatchOr> for FormatPatternMatchOr {
)
}
}
impl NeedsParentheses for PatternMatchOr {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Multiline
}
}

View file

@ -1,9 +1,13 @@
use ruff_formatter::prelude::format_with;
use ruff_formatter::{Format, FormatResult};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::PatternMatchSequence;
use crate::builders::PyFormatterExtensions;
use crate::expression::parentheses::{empty_parenthesized, optional_parentheses, parenthesized};
use crate::context::PyFormatContext;
use crate::expression::parentheses::{
empty_parenthesized, optional_parentheses, parenthesized, NeedsParentheses, OptionalParentheses,
};
use crate::{FormatNodeRule, PyFormatter};
#[derive(Default)]
@ -51,3 +55,13 @@ impl FormatNodeRule<PatternMatchSequence> for FormatPatternMatchSequence {
}
}
}
impl NeedsParentheses for PatternMatchSequence {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Never
}
}

View file

@ -1,6 +1,8 @@
use crate::prelude::*;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{Constant, PatternMatchSingleton};
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::{FormatNodeRule, PyFormatter};
#[derive(Default)]
@ -16,3 +18,13 @@ impl FormatNodeRule<PatternMatchSingleton> for FormatPatternMatchSingleton {
}
}
}
impl NeedsParentheses for PatternMatchSingleton {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Never
}
}

View file

@ -1,9 +1,10 @@
use ruff_formatter::{prelude::text, write, Buffer, FormatResult};
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::PatternMatchStar;
use crate::comments::{dangling_comments, SourceComment};
use crate::AsFormat;
use crate::{FormatNodeRule, PyFormatter};
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::prelude::*;
#[derive(Default)]
pub struct FormatPatternMatchStar;
@ -32,3 +33,13 @@ impl FormatNodeRule<PatternMatchStar> for FormatPatternMatchStar {
Ok(())
}
}
impl NeedsParentheses for PatternMatchStar {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Never
}
}

View file

@ -1,14 +1,26 @@
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::PatternMatchValue;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, Parentheses};
use crate::prelude::*;
use crate::{AsFormat, FormatNodeRule, PyFormatter};
#[derive(Default)]
pub struct FormatPatternMatchValue;
impl FormatNodeRule<PatternMatchValue> for FormatPatternMatchValue {
fn fmt_fields(&self, item: &PatternMatchValue, f: &mut PyFormatter) -> FormatResult<()> {
// TODO(charlie): Avoid double parentheses for parenthesized top-level `PatternMatchValue`.
let PatternMatchValue { value, range: _ } = item;
value.format().fmt(f)
value.format().with_options(Parentheses::Never).fmt(f)
}
}
impl NeedsParentheses for PatternMatchValue {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
) -> OptionalParentheses {
OptionalParentheses::Never
}
}