mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-16 00:20:22 +00:00
Format PatternMatchClass
(#6860)
This commit is contained in:
parent
91880b8273
commit
91a780c771
10 changed files with 458 additions and 536 deletions
|
@ -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)
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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
|
||||
|
|
|
@ -1,23 +1,97 @@
|
|||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,8 +99,56 @@ 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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue