Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Format PatternMatchClass #6860

Merged
merged 3 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ def foo():
y = 1



match foo:
case [1, 2, *rest]:
pass
Expand Down Expand Up @@ -399,3 +400,61 @@ def foo():
b,
}:
pass


match pattern_match_class:
case Point2D(
# own line
):
...

case (
Point2D
# own line
()
):
...

case Point2D( # end of line line
):
...

case Point2D( # end of line
0, 0
):
...

case Point2D(0, 0):
...

case Point2D(
( # end of line
# own line
0
), 0):
...

case Point3D(x=0, y=0, z=000000000000000000000000000000000000000000000000000000000000000000000000000000000):
...

case Bar(0, a=None, b="hello"):
...

case FooBar(# leading
# leading
# leading
# leading
0 # trailing
# trailing
# trailing
# trailing
):
...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add something like

match a:
    case A(
        b # b
        = # c
        2 # d
        # e
    ):
        pass

with the left hand side and the right hand side split of the assignment split by comment? I wanted to add an additional comment before the b but that's blocked by #6866


case A(
b # b
= # c
2 # d
# e
):
pass
93 changes: 84 additions & 9 deletions crates/ruff_python_formatter/src/comments/placement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,15 +180,7 @@ fn handle_enclosed_comment<'a>(
AnyNodeRef::Comprehension(comprehension) => {
handle_comprehension_comment(comment, comprehension, locator)
}
AnyNodeRef::PatternMatchSequence(pattern_match_sequence) => {
if SequenceType::from_pattern(pattern_match_sequence, locator.contents())
.is_parenthesized()
{
handle_bracketed_end_of_line_comment(comment, locator)
} else {
CommentPlacement::Default(comment)
}
}

AnyNodeRef::ExprAttribute(attribute) => {
handle_attribute_comment(comment, attribute, locator)
}
Expand Down Expand Up @@ -219,6 +211,18 @@ fn handle_enclosed_comment<'a>(
handle_module_level_own_line_comment_before_class_or_function_comment(comment, locator)
}
AnyNodeRef::WithItem(_) => handle_with_item_comment(comment, locator),
AnyNodeRef::PatternMatchSequence(pattern_match_sequence) => {
if SequenceType::from_pattern(pattern_match_sequence, locator.contents())
.is_parenthesized()
{
handle_bracketed_end_of_line_comment(comment, locator)
} else {
CommentPlacement::Default(comment)
}
}
AnyNodeRef::PatternMatchClass(class) => {
handle_pattern_match_class_comment(comment, class, locator)
}
AnyNodeRef::PatternMatchAs(_) => handle_pattern_match_as_comment(comment, locator),
AnyNodeRef::PatternMatchStar(_) => handle_pattern_match_star_comment(comment),
AnyNodeRef::PatternMatchMapping(pattern) => {
Expand Down Expand Up @@ -1229,6 +1233,77 @@ fn handle_with_item_comment<'a>(
}
}

/// Handles trailing comments after the `as` keyword of a pattern match item:
///
/// ```python
/// case (
/// Pattern
/// # dangling
/// ( # dangling
/// # dangling
/// )
/// ): ...
/// ```
fn handle_pattern_match_class_comment<'a>(
comment: DecoratedComment<'a>,
class: &'a ast::PatternMatchClass,
locator: &Locator,
) -> CommentPlacement<'a> {
// Find the open parentheses on the arguments.
let Some(left_paren) = SimpleTokenizer::starts_at(class.cls.end(), locator.contents())
.skip_trivia()
.find(|token| token.kind == SimpleTokenKind::LParen)
else {
return CommentPlacement::Default(comment);
};

// If the comment appears before the open parenthesis, it's dangling:
// ```python
// case (
// Pattern
// # dangling
// (...)
// ): ...
// ```
if comment.end() < left_paren.start() {
return CommentPlacement::dangling(comment.enclosing_node(), comment);
}

let Some(first_item) = class
.patterns
.first()
.map(Ranged::start)
.or_else(|| class.kwd_attrs.first().map(Ranged::start))
else {
// If there are no items, then the comment must be dangling:
// ```python
// case (
// Pattern(
// # dangling
// )
// ): ...
// ```
return CommentPlacement::dangling(comment.enclosing_node(), comment);
};

// If the comment appears before the first item or its parentheses, then it's dangling:
// ```python
// case (
// Pattern( # dangling
// 0,
// 0,
// )
// ): ...
// ```
if comment.line_position().is_end_of_line() {
if comment.end() < first_item {
return CommentPlacement::dangling(comment.enclosing_node(), comment);
}
}

CommentPlacement::Default(comment)
}

/// Handles trailing comments after the `as` keyword of a pattern match item:
///
/// ```python
Expand Down
146 changes: 134 additions & 12 deletions crates/ruff_python_formatter/src/pattern/pattern_match_class.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,154 @@
use ruff_formatter::{write, Buffer, FormatResult};
use crate::comments::{dangling_comments, SourceComment};
use ruff_formatter::write;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::PatternMatchClass;
use ruff_python_ast::{Pattern, PatternMatchClass, Ranged};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{TextRange, TextSize};

use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::expression::parentheses::{
empty_parenthesized, parenthesized, NeedsParentheses, OptionalParentheses, Parentheses,
};
use crate::prelude::*;
use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter};

#[derive(Default)]
pub struct FormatPatternMatchClass;

impl FormatNodeRule<PatternMatchClass> for FormatPatternMatchClass {
fn fmt_fields(&self, item: &PatternMatchClass, f: &mut PyFormatter) -> FormatResult<()> {
write!(
f,
[not_yet_implemented_custom_text(
"NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0)",
item
)]
)
let PatternMatchClass {
range,
cls,
patterns,
kwd_attrs,
kwd_patterns,
} = item;

let comments = f.context().comments().clone();
let dangling = comments.dangling(item);

// Identify the dangling comments before and after the open parenthesis.
let (before_parenthesis, after_parenthesis) = if let Some(left_paren) =
SimpleTokenizer::starts_at(cls.end(), f.context().source())
.find(|token| token.kind() == SimpleTokenKind::LParen)
{
dangling
.split_at(dangling.partition_point(|comment| comment.start() < left_paren.start()))
} else {
(dangling, [].as_slice())
};

write!(f, [cls.format(), dangling_comments(before_parenthesis)])?;

match (patterns.as_slice(), kwd_attrs.as_slice()) {
([], []) => {
// No patterns; render parentheses with any dangling comments.
write!(f, [empty_parenthesized("(", after_parenthesis, ")")])
}
([pattern], []) => {
// A single pattern. We need to take care not to re-parenthesize it, since our standard
// parenthesis detection will false-positive here.
let parentheses = if is_single_argument_parenthesized(
pattern,
item.end(),
f.context().source(),
) {
Parentheses::Always
} else {
Parentheses::Never
};
write!(
f,
[
parenthesized("(", &pattern.format().with_options(parentheses), ")")
.with_dangling_comments(after_parenthesis)
]
)
}
_ => {
// Multiple patterns: standard logic.
let items = format_with(|f| {
let mut join = f.join_comma_separated(range.end());
join.nodes(patterns.iter());
for (key, value) in kwd_attrs.iter().zip(kwd_patterns.iter()) {
join.entry(
key,
&format_with(|f| write!(f, [key.format(), text("="), value.format()])),
);
}
join.finish()
});
write!(
f,
[parenthesized("(", &group(&items), ")")
.with_dangling_comments(after_parenthesis)]
)
}
}
}

fn fmt_dangling_comments(
&self,
_dangling_comments: &[SourceComment],
_f: &mut PyFormatter,
) -> FormatResult<()> {
Ok(())
}
}

impl NeedsParentheses for PatternMatchClass {
fn needs_parentheses(
&self,
_parent: AnyNodeRef,
_context: &PyFormatContext,
context: &PyFormatContext,
) -> OptionalParentheses {
// If there are any comments outside of the class parentheses, break:
// ```python
// case (
// Pattern
// # dangling
// (...)
// ): ...
// ```
let dangling = context.comments().dangling(self);
if !dangling.is_empty() {
if let Some(left_paren) = SimpleTokenizer::starts_at(self.cls.end(), context.source())
.find(|token| token.kind() == SimpleTokenKind::LParen)
{
if dangling
.iter()
.any(|comment| comment.start() < left_paren.start())
{
return OptionalParentheses::Multiline;
};
}
}
OptionalParentheses::Never
}
}

/// Returns `true` if the pattern (which is the only argument to a [`PatternMatchClass`]) is
/// parenthesized. Used to avoid falsely assuming that `x` is parenthesized in cases like:
/// ```python
/// case Point2D(x): ...
/// ```
fn is_single_argument_parenthesized(pattern: &Pattern, call_end: TextSize, source: &str) -> bool {
let mut has_seen_r_paren = false;
for token in SimpleTokenizer::new(source, TextRange::new(pattern.end(), call_end)).skip_trivia()
{
match token.kind() {
SimpleTokenKind::RParen => {
if has_seen_r_paren {
return true;
}
has_seen_r_paren = true;
}
// Skip over any trailing comma
SimpleTokenKind::Comma => continue,
_ => {
// Passed the arguments
break;
}
}
}
false
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,6 @@ match x:
```diff
--- Black
+++ Ruff
@@ -6,7 +6,7 @@
y = 0
# case black_test_patma_142
match x:
- case bytes(z):
+ case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0):
y = 0
# case black_test_patma_073
match x:
@@ -16,11 +16,11 @@
y = 1
# case black_test_patma_006
Expand Down Expand Up @@ -226,7 +217,7 @@ match x:
y = 0
# case black_test_patma_142
match x:
case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0):
case bytes(z):
y = 0
# case black_test_patma_073
match x:
Expand Down
Loading
Loading