mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-28 21:05:08 +00:00
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:
parent
281ce56dc1
commit
6f23469e00
15 changed files with 406 additions and 124 deletions
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue