mirror of
https://github.com/astral-sh/ruff
synced 2026-01-22 22:10:48 -05:00
Introduce AST nodes for PatternMatchClass arguments (#6881)
## Summary This PR introduces two new AST nodes to improve the representation of `PatternMatchClass`. As a reminder, `PatternMatchClass` looks like this: ```python case Point2D(0, 0, x=1, y=2): ... ``` Historically, this was represented as a vector of patterns (for the `0, 0` portion) and parallel vectors of keyword names (for `x` and `y`) and values (for `1` and `2`). This introduces a bunch of challenges for the formatter, but importantly, it's also really different from how we represent similar nodes, like arguments (`func(0, 0, x=1, y=2)`) or parameters (`def func(x, y)`). So, firstly, we now use a single node (`PatternArguments`) for the entire parenthesized region, making it much more consistent with our other nodes. So, above, `PatternArguments` would be `(0, 0, x=1, y=2)`. Secondly, we now have a `PatternKeyword` node for `x=1` and `y=2`. This is much more similar to the how `Keyword` is represented within `Arguments` for call expressions. Closes https://github.com/astral-sh/ruff/issues/6866. Closes https://github.com/astral-sh/ruff/issues/6880.
This commit is contained in:
110
crates/ruff_python_formatter/src/pattern/pattern_arguments.rs
Normal file
110
crates/ruff_python_formatter/src/pattern/pattern_arguments.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::node::AstNode;
|
||||
use ruff_python_ast::{Pattern, PatternArguments, Ranged};
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::parentheses::{empty_parenthesized, parenthesized, Parentheses};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatPatternArguments;
|
||||
|
||||
impl FormatNodeRule<PatternArguments> for FormatPatternArguments {
|
||||
fn fmt_fields(&self, item: &PatternArguments, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
// If there are no arguments, all comments are dangling:
|
||||
// ```python
|
||||
// case Point2D( # dangling
|
||||
// # dangling
|
||||
// )
|
||||
// ```
|
||||
if item.patterns.is_empty() && item.keywords.is_empty() {
|
||||
let comments = f.context().comments().clone();
|
||||
let dangling = comments.dangling(item);
|
||||
return write!(f, [empty_parenthesized("(", dangling, ")")]);
|
||||
}
|
||||
|
||||
let all_arguments = format_with(|f: &mut PyFormatter| {
|
||||
let source = f.context().source();
|
||||
let mut joiner = f.join_comma_separated(item.end());
|
||||
match item.patterns.as_slice() {
|
||||
[pattern] if item.keywords.is_empty() => {
|
||||
let parentheses =
|
||||
if is_single_argument_parenthesized(pattern, item.end(), source) {
|
||||
Parentheses::Always
|
||||
} else {
|
||||
// Note: no need to handle opening-parenthesis comments, since
|
||||
// an opening-parenthesis comment implies that the argument is
|
||||
// parenthesized.
|
||||
Parentheses::Never
|
||||
};
|
||||
joiner.entry(pattern, &pattern.format().with_options(parentheses));
|
||||
}
|
||||
patterns => {
|
||||
joiner
|
||||
.entries(patterns.iter().map(|pattern| {
|
||||
(
|
||||
pattern,
|
||||
pattern.format().with_options(Parentheses::Preserve),
|
||||
)
|
||||
}))
|
||||
.nodes(item.keywords.iter());
|
||||
}
|
||||
}
|
||||
|
||||
joiner.finish()
|
||||
});
|
||||
|
||||
// If the arguments are non-empty, then a dangling comment indicates a comment on the
|
||||
// same line as the opening parenthesis, e.g.:
|
||||
// ```python
|
||||
// case Point2D( # dangling
|
||||
// ...
|
||||
// )
|
||||
// ```
|
||||
let comments = f.context().comments().clone();
|
||||
let dangling_comments = comments.dangling(item.as_any_node_ref());
|
||||
|
||||
write!(
|
||||
f,
|
||||
[parenthesized("(", &group(&all_arguments), ")")
|
||||
.with_dangling_comments(dangling_comments)]
|
||||
)
|
||||
}
|
||||
|
||||
fn fmt_dangling_comments(
|
||||
&self,
|
||||
_dangling_comments: &[SourceComment],
|
||||
_f: &mut PyFormatter,
|
||||
) -> FormatResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
Reference in New Issue
Block a user