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:
Charlie Marsh 2023-08-26 10:45:44 -04:00 committed by GitHub
parent ed1b4122d0
commit 15b73bdb8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 25299 additions and 25824 deletions

View file

@ -153,6 +153,36 @@ impl<'a> From<&'a ast::WithItem> for ComparableWithItem<'a> {
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ComparablePatternArguments<'a> {
patterns: Vec<ComparablePattern<'a>>,
keywords: Vec<ComparablePatternKeyword<'a>>,
}
impl<'a> From<&'a ast::PatternArguments> for ComparablePatternArguments<'a> {
fn from(parameters: &'a ast::PatternArguments) -> Self {
Self {
patterns: parameters.patterns.iter().map(Into::into).collect(),
keywords: parameters.keywords.iter().map(Into::into).collect(),
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ComparablePatternKeyword<'a> {
attr: &'a str,
pattern: ComparablePattern<'a>,
}
impl<'a> From<&'a ast::PatternKeyword> for ComparablePatternKeyword<'a> {
fn from(keyword: &'a ast::PatternKeyword) -> Self {
Self {
attr: keyword.attr.as_str(),
pattern: (&keyword.pattern).into(),
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct PatternMatchValue<'a> {
value: ComparableExpr<'a>,
@ -178,9 +208,7 @@ pub struct PatternMatchMapping<'a> {
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct PatternMatchClass<'a> {
cls: ComparableExpr<'a>,
patterns: Vec<ComparablePattern<'a>>,
kwd_attrs: Vec<&'a str>,
kwd_patterns: Vec<ComparablePattern<'a>>,
arguments: ComparablePatternArguments<'a>,
}
#[derive(Debug, PartialEq, Eq, Hash)]
@ -240,18 +268,12 @@ impl<'a> From<&'a ast::Pattern> for ComparablePattern<'a> {
patterns: patterns.iter().map(Into::into).collect(),
rest: rest.as_deref(),
}),
ast::Pattern::MatchClass(ast::PatternMatchClass {
cls,
patterns,
kwd_attrs,
kwd_patterns,
..
}) => Self::MatchClass(PatternMatchClass {
ast::Pattern::MatchClass(ast::PatternMatchClass { cls, arguments, .. }) => {
Self::MatchClass(PatternMatchClass {
cls: cls.into(),
patterns: patterns.iter().map(Into::into).collect(),
kwd_attrs: kwd_attrs.iter().map(ast::Identifier::as_str).collect(),
kwd_patterns: kwd_patterns.iter().map(Into::into).collect(),
}),
arguments: arguments.into(),
})
}
ast::Pattern::MatchStar(ast::PatternMatchStar { name, .. }) => {
Self::MatchStar(PatternMatchStar {
name: name.as_deref(),

View file

@ -279,19 +279,16 @@ where
.iter()
.any(|pattern| any_over_pattern(pattern, func))
}
Pattern::MatchClass(ast::PatternMatchClass {
cls,
patterns,
kwd_patterns,
..
}) => {
Pattern::MatchClass(ast::PatternMatchClass { cls, arguments, .. }) => {
any_over_expr(cls, func)
|| patterns
|| arguments
.patterns
.iter()
.any(|pattern| any_over_pattern(pattern, func))
|| kwd_patterns
|| arguments
.keywords
.iter()
.any(|pattern| any_over_pattern(pattern, func))
.any(|keyword| any_over_pattern(&keyword.pattern, func))
}
Pattern::MatchStar(_) => false,
Pattern::MatchAs(ast::PatternMatchAs { pattern, .. }) => pattern

View file

@ -1,8 +1,9 @@
use crate::visitor::preorder::PreorderVisitor;
use crate::{
self as ast, Alias, Arguments, Comprehension, Decorator, ExceptHandler, Expr, Keyword,
MatchCase, Mod, Parameter, ParameterWithDefault, Parameters, Pattern, Ranged, Stmt, TypeParam,
TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, TypeParams, WithItem,
MatchCase, Mod, Parameter, ParameterWithDefault, Parameters, Pattern, PatternArguments,
PatternKeyword, Ranged, Stmt, TypeParam, TypeParamParamSpec, TypeParamTypeVar,
TypeParamTypeVarTuple, TypeParams, WithItem,
};
use ruff_text_size::TextRange;
use std::ptr::NonNull;
@ -90,6 +91,8 @@ pub enum AnyNode {
PatternMatchStar(ast::PatternMatchStar),
PatternMatchAs(ast::PatternMatchAs),
PatternMatchOr(ast::PatternMatchOr),
PatternArguments(PatternArguments),
PatternKeyword(PatternKeyword),
Comprehension(Comprehension),
Arguments(Arguments),
Parameters(Parameters),
@ -175,6 +178,8 @@ impl AnyNode {
| AnyNode::PatternMatchStar(_)
| AnyNode::PatternMatchAs(_)
| AnyNode::PatternMatchOr(_)
| AnyNode::PatternArguments(_)
| AnyNode::PatternKeyword(_)
| AnyNode::Comprehension(_)
| AnyNode::Arguments(_)
| AnyNode::Parameters(_)
@ -260,6 +265,8 @@ impl AnyNode {
| AnyNode::PatternMatchStar(_)
| AnyNode::PatternMatchAs(_)
| AnyNode::PatternMatchOr(_)
| AnyNode::PatternArguments(_)
| AnyNode::PatternKeyword(_)
| AnyNode::Comprehension(_)
| AnyNode::Arguments(_)
| AnyNode::Parameters(_)
@ -345,6 +352,8 @@ impl AnyNode {
| AnyNode::PatternMatchStar(_)
| AnyNode::PatternMatchAs(_)
| AnyNode::PatternMatchOr(_)
| AnyNode::PatternArguments(_)
| AnyNode::PatternKeyword(_)
| AnyNode::Comprehension(_)
| AnyNode::Arguments(_)
| AnyNode::Parameters(_)
@ -430,6 +439,8 @@ impl AnyNode {
| AnyNode::ExprSlice(_)
| AnyNode::ExprIpyEscapeCommand(_)
| AnyNode::ExceptHandlerExceptHandler(_)
| AnyNode::PatternArguments(_)
| AnyNode::PatternKeyword(_)
| AnyNode::Comprehension(_)
| AnyNode::Arguments(_)
| AnyNode::Parameters(_)
@ -515,6 +526,8 @@ impl AnyNode {
| AnyNode::PatternMatchStar(_)
| AnyNode::PatternMatchAs(_)
| AnyNode::PatternMatchOr(_)
| AnyNode::PatternArguments(_)
| AnyNode::PatternKeyword(_)
| AnyNode::Comprehension(_)
| AnyNode::Arguments(_)
| AnyNode::Parameters(_)
@ -619,6 +632,8 @@ impl AnyNode {
Self::PatternMatchStar(node) => AnyNodeRef::PatternMatchStar(node),
Self::PatternMatchAs(node) => AnyNodeRef::PatternMatchAs(node),
Self::PatternMatchOr(node) => AnyNodeRef::PatternMatchOr(node),
Self::PatternArguments(node) => AnyNodeRef::PatternArguments(node),
Self::PatternKeyword(node) => AnyNodeRef::PatternKeyword(node),
Self::Comprehension(node) => AnyNodeRef::Comprehension(node),
Self::Arguments(node) => AnyNodeRef::Arguments(node),
Self::Parameters(node) => AnyNodeRef::Parameters(node),
@ -3247,19 +3262,11 @@ impl AstNode for ast::PatternMatchClass {
{
let ast::PatternMatchClass {
cls,
patterns,
kwd_attrs: _,
kwd_patterns,
arguments: parameters,
range: _,
} = self;
visitor.visit_expr(cls);
for pattern in patterns {
visitor.visit_pattern(pattern);
}
for pattern in kwd_patterns {
visitor.visit_pattern(pattern);
}
visitor.visit_pattern_arguments(parameters);
}
}
impl AstNode for ast::PatternMatchStar {
@ -3378,6 +3385,94 @@ impl AstNode for ast::PatternMatchOr {
}
}
}
impl AstNode for PatternArguments {
fn cast(kind: AnyNode) -> Option<Self>
where
Self: Sized,
{
if let AnyNode::PatternArguments(node) = kind {
Some(node)
} else {
None
}
}
fn cast_ref(kind: AnyNodeRef) -> Option<&Self> {
if let AnyNodeRef::PatternArguments(node) = kind {
Some(node)
} else {
None
}
}
fn as_any_node_ref(&self) -> AnyNodeRef {
AnyNodeRef::from(self)
}
fn into_any_node(self) -> AnyNode {
AnyNode::from(self)
}
fn visit_preorder<'a, V>(&'a self, visitor: &mut V)
where
V: PreorderVisitor<'a> + ?Sized,
{
let PatternArguments {
range: _,
patterns,
keywords,
} = self;
for pattern in patterns {
visitor.visit_pattern(pattern);
}
for keyword in keywords {
visitor.visit_pattern_keyword(keyword);
}
}
}
impl AstNode for PatternKeyword {
fn cast(kind: AnyNode) -> Option<Self>
where
Self: Sized,
{
if let AnyNode::PatternKeyword(node) = kind {
Some(node)
} else {
None
}
}
fn cast_ref(kind: AnyNodeRef) -> Option<&Self> {
if let AnyNodeRef::PatternKeyword(node) = kind {
Some(node)
} else {
None
}
}
fn as_any_node_ref(&self) -> AnyNodeRef {
AnyNodeRef::from(self)
}
fn into_any_node(self) -> AnyNode {
AnyNode::from(self)
}
fn visit_preorder<'a, V>(&'a self, visitor: &mut V)
where
V: PreorderVisitor<'a> + ?Sized,
{
let PatternKeyword {
range: _,
attr: _,
pattern,
} = self;
visitor.visit_pattern(pattern);
}
}
impl AstNode for Comprehension {
fn cast(kind: AnyNode) -> Option<Self>
@ -4475,6 +4570,18 @@ impl From<ast::PatternMatchOr> for AnyNode {
}
}
impl From<PatternArguments> for AnyNode {
fn from(node: PatternArguments) -> Self {
AnyNode::PatternArguments(node)
}
}
impl From<PatternKeyword> for AnyNode {
fn from(node: PatternKeyword) -> Self {
AnyNode::PatternKeyword(node)
}
}
impl From<Comprehension> for AnyNode {
fn from(node: Comprehension) -> Self {
AnyNode::Comprehension(node)
@ -4615,6 +4722,8 @@ impl Ranged for AnyNode {
AnyNode::PatternMatchStar(node) => node.range(),
AnyNode::PatternMatchAs(node) => node.range(),
AnyNode::PatternMatchOr(node) => node.range(),
AnyNode::PatternArguments(node) => node.range(),
AnyNode::PatternKeyword(node) => node.range(),
AnyNode::Comprehension(node) => node.range(),
AnyNode::Arguments(node) => node.range(),
AnyNode::Parameters(node) => node.range(),
@ -4700,6 +4809,8 @@ pub enum AnyNodeRef<'a> {
PatternMatchStar(&'a ast::PatternMatchStar),
PatternMatchAs(&'a ast::PatternMatchAs),
PatternMatchOr(&'a ast::PatternMatchOr),
PatternArguments(&'a ast::PatternArguments),
PatternKeyword(&'a ast::PatternKeyword),
Comprehension(&'a Comprehension),
Arguments(&'a Arguments),
Parameters(&'a Parameters),
@ -4784,6 +4895,8 @@ impl AnyNodeRef<'_> {
AnyNodeRef::PatternMatchStar(node) => NonNull::from(*node).cast(),
AnyNodeRef::PatternMatchAs(node) => NonNull::from(*node).cast(),
AnyNodeRef::PatternMatchOr(node) => NonNull::from(*node).cast(),
AnyNodeRef::PatternArguments(node) => NonNull::from(*node).cast(),
AnyNodeRef::PatternKeyword(node) => NonNull::from(*node).cast(),
AnyNodeRef::Comprehension(node) => NonNull::from(*node).cast(),
AnyNodeRef::Arguments(node) => NonNull::from(*node).cast(),
AnyNodeRef::Parameters(node) => NonNull::from(*node).cast(),
@ -4874,6 +4987,8 @@ impl AnyNodeRef<'_> {
AnyNodeRef::PatternMatchStar(_) => NodeKind::PatternMatchStar,
AnyNodeRef::PatternMatchAs(_) => NodeKind::PatternMatchAs,
AnyNodeRef::PatternMatchOr(_) => NodeKind::PatternMatchOr,
AnyNodeRef::PatternArguments(_) => NodeKind::PatternArguments,
AnyNodeRef::PatternKeyword(_) => NodeKind::PatternKeyword,
AnyNodeRef::Comprehension(_) => NodeKind::Comprehension,
AnyNodeRef::Arguments(_) => NodeKind::Arguments,
AnyNodeRef::Parameters(_) => NodeKind::Parameters,
@ -4959,6 +5074,8 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::PatternMatchStar(_)
| AnyNodeRef::PatternMatchAs(_)
| AnyNodeRef::PatternMatchOr(_)
| AnyNodeRef::PatternArguments(_)
| AnyNodeRef::PatternKeyword(_)
| AnyNodeRef::Comprehension(_)
| AnyNodeRef::Arguments(_)
| AnyNodeRef::Parameters(_)
@ -5044,6 +5161,8 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::PatternMatchStar(_)
| AnyNodeRef::PatternMatchAs(_)
| AnyNodeRef::PatternMatchOr(_)
| AnyNodeRef::PatternArguments(_)
| AnyNodeRef::PatternKeyword(_)
| AnyNodeRef::Comprehension(_)
| AnyNodeRef::Arguments(_)
| AnyNodeRef::Parameters(_)
@ -5128,6 +5247,8 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::PatternMatchStar(_)
| AnyNodeRef::PatternMatchAs(_)
| AnyNodeRef::PatternMatchOr(_)
| AnyNodeRef::PatternArguments(_)
| AnyNodeRef::PatternKeyword(_)
| AnyNodeRef::Comprehension(_)
| AnyNodeRef::Arguments(_)
| AnyNodeRef::Parameters(_)
@ -5212,6 +5333,8 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::ExprTuple(_)
| AnyNodeRef::ExprSlice(_)
| AnyNodeRef::ExprIpyEscapeCommand(_)
| AnyNodeRef::PatternArguments(_)
| AnyNodeRef::PatternKeyword(_)
| AnyNodeRef::ExceptHandlerExceptHandler(_)
| AnyNodeRef::Comprehension(_)
| AnyNodeRef::Arguments(_)
@ -5298,6 +5421,8 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::PatternMatchStar(_)
| AnyNodeRef::PatternMatchAs(_)
| AnyNodeRef::PatternMatchOr(_)
| AnyNodeRef::PatternArguments(_)
| AnyNodeRef::PatternKeyword(_)
| AnyNodeRef::Comprehension(_)
| AnyNodeRef::Arguments(_)
| AnyNodeRef::Parameters(_)
@ -5411,6 +5536,8 @@ impl AnyNodeRef<'_> {
AnyNodeRef::PatternMatchStar(node) => node.visit_preorder(visitor),
AnyNodeRef::PatternMatchAs(node) => node.visit_preorder(visitor),
AnyNodeRef::PatternMatchOr(node) => node.visit_preorder(visitor),
AnyNodeRef::PatternArguments(node) => node.visit_preorder(visitor),
AnyNodeRef::PatternKeyword(node) => node.visit_preorder(visitor),
AnyNodeRef::Comprehension(node) => node.visit_preorder(visitor),
AnyNodeRef::Arguments(node) => node.visit_preorder(visitor),
AnyNodeRef::Parameters(node) => node.visit_preorder(visitor),
@ -5820,6 +5947,18 @@ impl<'a> From<&'a ast::PatternMatchOr> for AnyNodeRef<'a> {
}
}
impl<'a> From<&'a ast::PatternArguments> for AnyNodeRef<'a> {
fn from(node: &'a ast::PatternArguments) -> Self {
AnyNodeRef::PatternArguments(node)
}
}
impl<'a> From<&'a ast::PatternKeyword> for AnyNodeRef<'a> {
fn from(node: &'a ast::PatternKeyword) -> Self {
AnyNodeRef::PatternKeyword(node)
}
}
impl<'a> From<&'a Decorator> for AnyNodeRef<'a> {
fn from(node: &'a Decorator) -> Self {
AnyNodeRef::Decorator(node)
@ -6073,6 +6212,8 @@ impl Ranged for AnyNodeRef<'_> {
AnyNodeRef::PatternMatchStar(node) => node.range(),
AnyNodeRef::PatternMatchAs(node) => node.range(),
AnyNodeRef::PatternMatchOr(node) => node.range(),
AnyNodeRef::PatternArguments(node) => node.range(),
AnyNodeRef::PatternKeyword(node) => node.range(),
AnyNodeRef::Comprehension(node) => node.range(),
AnyNodeRef::Arguments(node) => node.range(),
AnyNodeRef::Parameters(node) => node.range(),
@ -6160,6 +6301,8 @@ pub enum NodeKind {
PatternMatchStar,
PatternMatchAs,
PatternMatchOr,
PatternArguments,
PatternKeyword,
TypeIgnoreTypeIgnore,
Comprehension,
Arguments,

View file

@ -1900,9 +1900,7 @@ impl From<PatternMatchMapping> for Pattern {
pub struct PatternMatchClass {
pub range: TextRange,
pub cls: Box<Expr>,
pub patterns: Vec<Pattern>,
pub kwd_attrs: Vec<Identifier>,
pub kwd_patterns: Vec<Pattern>,
pub arguments: PatternArguments,
}
impl From<PatternMatchClass> for Pattern {
@ -1911,6 +1909,28 @@ impl From<PatternMatchClass> for Pattern {
}
}
/// An AST node to represent the arguments to a [`PatternMatchClass`], i.e., the
/// parenthesized contents in `case Point(1, x=0, y=0)`.
///
/// Like [`Arguments`], but for [`PatternMatchClass`].
#[derive(Clone, Debug, PartialEq)]
pub struct PatternArguments {
pub range: TextRange,
pub patterns: Vec<Pattern>,
pub keywords: Vec<PatternKeyword>,
}
/// An AST node to represent the keyword arguments to a [`PatternMatchClass`], i.e., the
/// `x=0` and `y=0` in `case Point(x=0, y=0)`.
///
/// Like [`Keyword`], but for [`PatternMatchClass`].
#[derive(Clone, Debug, PartialEq)]
pub struct PatternKeyword {
pub range: TextRange,
pub attr: Identifier,
pub pattern: Pattern,
}
/// See also [MatchStar](https://docs.python.org/3/library/ast.html#ast.MatchStar)
#[derive(Clone, Debug, PartialEq)]
pub struct PatternMatchStar {
@ -3004,6 +3024,16 @@ impl Ranged for crate::Pattern {
}
}
}
impl Ranged for crate::nodes::PatternArguments {
fn range(&self) -> TextRange {
self.range
}
}
impl Ranged for crate::nodes::PatternKeyword {
fn range(&self) -> TextRange {
self.range
}
}
impl Ranged for crate::nodes::TypeParams {
fn range(&self) -> TextRange {

View file

@ -5,7 +5,8 @@ pub mod preorder;
use crate::{
self as ast, Alias, Arguments, BoolOp, CmpOp, Comprehension, Decorator, ElifElseClause,
ExceptHandler, Expr, ExprContext, Keyword, MatchCase, Operator, Parameter, Parameters, Pattern,
Stmt, TypeParam, TypeParamTypeVar, TypeParams, UnaryOp, WithItem,
PatternArguments, PatternKeyword, Stmt, TypeParam, TypeParamTypeVar, TypeParams, UnaryOp,
WithItem,
};
/// A trait for AST visitors. Visits all nodes in the AST recursively in evaluation-order.
@ -82,6 +83,12 @@ pub trait Visitor<'a> {
fn visit_pattern(&mut self, pattern: &'a Pattern) {
walk_pattern(self, pattern);
}
fn visit_pattern_arguments(&mut self, pattern_arguments: &'a PatternArguments) {
walk_pattern_arguments(self, pattern_arguments);
}
fn visit_pattern_keyword(&mut self, pattern_keyword: &'a PatternKeyword) {
walk_pattern_keyword(self, pattern_keyword);
}
fn visit_body(&mut self, body: &'a [Stmt]) {
walk_body(self, body);
}
@ -674,20 +681,9 @@ pub fn walk_pattern<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, pattern: &'a P
visitor.visit_pattern(pattern);
}
}
Pattern::MatchClass(ast::PatternMatchClass {
cls,
patterns,
kwd_patterns,
..
}) => {
Pattern::MatchClass(ast::PatternMatchClass { cls, arguments, .. }) => {
visitor.visit_expr(cls);
for pattern in patterns {
visitor.visit_pattern(pattern);
}
for pattern in kwd_patterns {
visitor.visit_pattern(pattern);
}
visitor.visit_pattern_arguments(arguments);
}
Pattern::MatchStar(_) => {}
Pattern::MatchAs(ast::PatternMatchAs { pattern, .. }) => {
@ -703,6 +699,25 @@ pub fn walk_pattern<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, pattern: &'a P
}
}
pub fn walk_pattern_arguments<'a, V: Visitor<'a> + ?Sized>(
visitor: &mut V,
pattern_arguments: &'a PatternArguments,
) {
for pattern in &pattern_arguments.patterns {
visitor.visit_pattern(pattern);
}
for keyword in &pattern_arguments.keywords {
visitor.visit_pattern_keyword(keyword);
}
}
pub fn walk_pattern_keyword<'a, V: Visitor<'a> + ?Sized>(
visitor: &mut V,
pattern_keyword: &'a PatternKeyword,
) {
visitor.visit_pattern(&pattern_keyword.pattern);
}
#[allow(unused_variables)]
pub fn walk_expr_context<'a, V: Visitor<'a> + ?Sized>(visitor: &V, expr_context: &'a ExprContext) {}

View file

@ -2,7 +2,8 @@ use crate::node::{AnyNodeRef, AstNode};
use crate::{
Alias, Arguments, BoolOp, CmpOp, Comprehension, Constant, Decorator, ElifElseClause,
ExceptHandler, Expr, Keyword, MatchCase, Mod, Operator, Parameter, ParameterWithDefault,
Parameters, Pattern, Stmt, TypeParam, TypeParams, UnaryOp, WithItem,
Parameters, Pattern, PatternArguments, PatternKeyword, Stmt, TypeParam, TypeParams, UnaryOp,
WithItem,
};
/// Visitor that traverses all nodes recursively in pre-order.
@ -132,6 +133,17 @@ pub trait PreorderVisitor<'a> {
walk_pattern(self, pattern);
}
#[inline]
fn visit_pattern_arguments(&mut self, pattern_arguments: &'a PatternArguments) {
walk_pattern_arguments(self, pattern_arguments);
}
#[inline]
fn visit_pattern_keyword(&mut self, pattern_keyword: &'a PatternKeyword) {
walk_pattern_keyword(self, pattern_keyword);
}
#[inline]
fn visit_body(&mut self, body: &'a [Stmt]) {
walk_body(self, body);
@ -460,6 +472,33 @@ where
visitor.leave_node(node);
}
pub fn walk_pattern_arguments<'a, V>(visitor: &mut V, pattern_arguments: &'a PatternArguments)
where
V: PreorderVisitor<'a> + ?Sized,
{
let node = AnyNodeRef::from(pattern_arguments);
if visitor.enter_node(node).is_traverse() {
for pattern in &pattern_arguments.patterns {
visitor.visit_pattern(pattern);
}
for keyword in &pattern_arguments.keywords {
visitor.visit_pattern_keyword(keyword);
}
}
visitor.leave_node(node);
}
pub fn walk_pattern_keyword<'a, V>(visitor: &mut V, pattern_keyword: &'a PatternKeyword)
where
V: PreorderVisitor<'a> + ?Sized,
{
let node = AnyNodeRef::from(pattern_keyword);
if visitor.enter_node(node).is_traverse() {
visitor.visit_pattern(&pattern_keyword.pattern);
}
visitor.leave_node(node);
}
pub fn walk_bool_op<'a, V>(_visitor: &mut V, _bool_op: &'a BoolOp)
where
V: PreorderVisitor<'a> + ?Sized,

View file

@ -458,3 +458,12 @@ match pattern_match_class:
# e
):
pass
case A(
# a
b # b
= # c
2 # d
# e
):
pass

View file

@ -174,7 +174,7 @@ fn handle_enclosed_comment<'a>(
}
})
}
AnyNodeRef::Arguments(_) | AnyNodeRef::TypeParams(_) => {
AnyNodeRef::Arguments(_) | AnyNodeRef::TypeParams(_) | AnyNodeRef::PatternArguments(_) => {
handle_bracketed_end_of_line_comment(comment, locator)
}
AnyNodeRef::Comprehension(comprehension) => {
@ -220,9 +220,7 @@ fn handle_enclosed_comment<'a>(
CommentPlacement::Default(comment)
}
}
AnyNodeRef::PatternMatchClass(class) => {
handle_pattern_match_class_comment(comment, class, locator)
}
AnyNodeRef::PatternMatchClass(class) => handle_pattern_match_class_comment(comment, class),
AnyNodeRef::PatternMatchAs(_) => handle_pattern_match_as_comment(comment, locator),
AnyNodeRef::PatternMatchStar(_) => handle_pattern_match_star_comment(comment),
AnyNodeRef::PatternMatchMapping(pattern) => {
@ -1233,75 +1231,23 @@ fn handle_with_item_comment<'a>(
}
}
/// Handles trailing comments after the `as` keyword of a pattern match item:
///
/// Handles trailing comments between the class name and its arguments in:
/// ```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);
}
}
if class.cls.end() < comment.start() && comment.end() < class.arguments.start() {
CommentPlacement::dangling(comment.enclosing_node(), comment)
} else {
CommentPlacement::Default(comment)
}
}
/// Handles trailing comments after the `as` keyword of a pattern match item:

View file

@ -2256,6 +2256,78 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::PatternMatchOr {
}
}
impl FormatRule<ast::PatternArguments, PyFormatContext<'_>>
for crate::pattern::pattern_arguments::FormatPatternArguments
{
#[inline]
fn fmt(&self, node: &ast::PatternArguments, f: &mut PyFormatter) -> FormatResult<()> {
FormatNodeRule::<ast::PatternArguments>::fmt(self, node, f)
}
}
impl<'ast> AsFormat<PyFormatContext<'ast>> for ast::PatternArguments {
type Format<'a> = FormatRefWithRule<
'a,
ast::PatternArguments,
crate::pattern::pattern_arguments::FormatPatternArguments,
PyFormatContext<'ast>,
>;
fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(
self,
crate::pattern::pattern_arguments::FormatPatternArguments::default(),
)
}
}
impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::PatternArguments {
type Format = FormatOwnedWithRule<
ast::PatternArguments,
crate::pattern::pattern_arguments::FormatPatternArguments,
PyFormatContext<'ast>,
>;
fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(
self,
crate::pattern::pattern_arguments::FormatPatternArguments::default(),
)
}
}
impl FormatRule<ast::PatternKeyword, PyFormatContext<'_>>
for crate::pattern::pattern_keyword::FormatPatternKeyword
{
#[inline]
fn fmt(&self, node: &ast::PatternKeyword, f: &mut PyFormatter) -> FormatResult<()> {
FormatNodeRule::<ast::PatternKeyword>::fmt(self, node, f)
}
}
impl<'ast> AsFormat<PyFormatContext<'ast>> for ast::PatternKeyword {
type Format<'a> = FormatRefWithRule<
'a,
ast::PatternKeyword,
crate::pattern::pattern_keyword::FormatPatternKeyword,
PyFormatContext<'ast>,
>;
fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(
self,
crate::pattern::pattern_keyword::FormatPatternKeyword::default(),
)
}
}
impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::PatternKeyword {
type Format = FormatOwnedWithRule<
ast::PatternKeyword,
crate::pattern::pattern_keyword::FormatPatternKeyword,
PyFormatContext<'ast>,
>;
fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(
self,
crate::pattern::pattern_keyword::FormatPatternKeyword::default(),
)
}
}
impl FormatRule<ast::Comprehension, PyFormatContext<'_>>
for crate::other::comprehension::FormatComprehension
{

View file

@ -8,6 +8,8 @@ use crate::expression::parentheses::{
};
use crate::prelude::*;
pub(crate) mod pattern_arguments;
pub(crate) mod pattern_keyword;
pub(crate) mod pattern_match_as;
pub(crate) mod pattern_match_class;
pub(crate) mod pattern_match_mapping;

View 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
}

View file

@ -0,0 +1,17 @@
use crate::prelude::*;
use ruff_formatter::write;
use ruff_python_ast::PatternKeyword;
#[derive(Default)]
pub struct FormatPatternKeyword;
impl FormatNodeRule<PatternKeyword> for FormatPatternKeyword {
fn fmt_fields(&self, item: &PatternKeyword, f: &mut PyFormatter) -> FormatResult<()> {
let PatternKeyword {
range: _,
attr,
pattern,
} = item;
write!(f, [attr.format(), text("="), pattern.format()])
}
}

View file

@ -1,13 +1,9 @@
use ruff_formatter::write;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{Pattern, PatternMatchClass, Ranged};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{TextRange, TextSize};
use ruff_python_ast::PatternMatchClass;
use crate::comments::{dangling_comments, SourceComment};
use crate::expression::parentheses::{
empty_parenthesized, parenthesized, NeedsParentheses, OptionalParentheses, Parentheses,
};
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::prelude::*;
#[derive(Default)]
@ -16,75 +12,23 @@ pub struct FormatPatternMatchClass;
impl FormatNodeRule<PatternMatchClass> for FormatPatternMatchClass {
fn fmt_fields(&self, item: &PatternMatchClass, f: &mut PyFormatter) -> FormatResult<()> {
let PatternMatchClass {
range,
range: _,
cls,
patterns,
kwd_attrs,
kwd_patterns,
arguments,
} = 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)
cls.format(),
dangling_comments(dangling),
arguments.format()
]
)
}
_ => {
// 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,
@ -109,46 +53,10 @@ impl NeedsParentheses for PatternMatchClass {
// (...)
// ): ...
// ```
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;
};
}
}
if context.comments().has_dangling(self) {
OptionalParentheses::Multiline
} else {
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
}
}

View file

@ -65,15 +65,7 @@ match match(
```diff
--- Black
+++ Ruff
@@ -6,30 +6,35 @@
):
print(1)
case c(
- very_complex=True,
- perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1,
+ very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1
):
print(2)
@@ -13,20 +13,26 @@
case a:
pass
@ -103,10 +95,6 @@ match match(
re.match()
match match():
case case(
- arg, # comment
+ arg # comment
):
pass
```
## Ruff Output
@ -120,7 +108,8 @@ match something:
):
print(1)
case c(
very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1
very_complex=True,
perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1,
):
print(2)
case a:
@ -149,7 +138,7 @@ re.match(
re.match()
match match():
case case(
arg # comment
arg, # comment
):
pass
```

View file

@ -464,6 +464,15 @@ match pattern_match_class:
# e
):
pass
case A(
# a
b # b
= # c
2 # d
# e
):
pass
```
## Output
@ -903,10 +912,8 @@ match pattern_match_class:
):
...
case Point2D(
# end of line
0,
0,
case Point2D( # end of line
0, 0
):
...
@ -932,8 +939,7 @@ match pattern_match_class:
case Bar(0, a=None, b="hello"):
...
case FooBar(
# leading
case FooBar( # leading
# leading
# leading
# leading
@ -951,6 +957,15 @@ match pattern_match_class:
# e
):
pass
case A(
# a
b=# b
# c
2 # d
# e
):
pass
```

View file

@ -790,97 +790,62 @@ MappingPattern: ast::Pattern = {
},
}
MatchKeywordEntry: (ast::Identifier, ast::Pattern) = {
<k:Identifier> "=" <v:Pattern> => (k, v),
MatchKeywordEntry: ast::PatternKeyword = {
<location:@L> <attr:Identifier> "=" <pattern:Pattern> <end_location:@R> => ast::PatternKeyword {
attr,
pattern,
range: (location..end_location).into()
},
};
ClassPattern: ast::Pattern = {
<location:@L> <e:MatchName> "(" <patterns: OneOrMore<Pattern>> "," <kwds:OneOrMore<MatchKeywordEntry>> ","? ")" <end_location:@R> => {
let (kwd_attrs, kwd_patterns) = kwds
.into_iter()
.unzip();
<location:@L> <cls:MatchName> <arguments:PatternArguments> <end_location:@R> => {
ast::PatternMatchClass {
cls: Box::new(e),
patterns,
kwd_attrs,
kwd_patterns,
cls: Box::new(cls),
arguments,
range: (location..end_location).into()
}.into()
},
<location:@L> <e:MatchName> "(" <patterns: OneOrMore<Pattern>> ","? ")" <end_location:@R> => {
<location:@L> <cls:MatchNameOrAttr> <arguments:PatternArguments> <end_location:@R> => {
ast::PatternMatchClass {
cls: Box::new(e),
patterns,
kwd_attrs: vec![],
kwd_patterns: vec![],
range: (location..end_location).into()
}.into()
},
<location:@L> <e:MatchName> "(" <kwds:OneOrMore<MatchKeywordEntry>> ","? ")" <end_location:@R> => {
let (kwd_attrs, kwd_patterns) = kwds
.into_iter()
.unzip();
ast::PatternMatchClass {
cls: Box::new(e),
patterns: vec![],
kwd_attrs,
kwd_patterns,
range: (location..end_location).into()
}.into()
},
<location:@L> <e:MatchName> "(" ")" <end_location:@R> => {
ast::PatternMatchClass {
cls: Box::new(e),
patterns: vec![],
kwd_attrs: vec![],
kwd_patterns: vec![],
range: (location..end_location).into()
}.into()
},
<location:@L> <e:MatchNameOrAttr> "(" <patterns: OneOrMore<Pattern>> "," <kwds:OneOrMore<MatchKeywordEntry>> ","? ")" <end_location:@R> => {
let (kwd_attrs, kwd_patterns) = kwds
.into_iter()
.unzip();
ast::PatternMatchClass {
cls: Box::new(e),
patterns,
kwd_attrs,
kwd_patterns,
range: (location..end_location).into()
}.into()
},
<location:@L> <e:MatchNameOrAttr> "(" <patterns: OneOrMore<Pattern>> ","? ")" <end_location:@R> => {
ast::PatternMatchClass {
cls: Box::new(e),
patterns,
kwd_attrs: vec![],
kwd_patterns: vec![],
range: (location..end_location).into()
}.into()
},
<location:@L> <e:MatchNameOrAttr> "(" <kwds:OneOrMore<MatchKeywordEntry>> ","? ")" <end_location:@R> => {
let (kwd_attrs, kwd_patterns) = kwds
.into_iter()
.unzip();
ast::PatternMatchClass {
cls: Box::new(e),
patterns: vec![],
kwd_attrs,
kwd_patterns,
range: (location..end_location).into()
}.into()
},
<location:@L> <e:MatchNameOrAttr> "(" ")" <end_location:@R> => {
ast::PatternMatchClass {
cls: Box::new(e),
patterns: vec![],
kwd_attrs: vec![],
kwd_patterns: vec![],
cls: Box::new(cls),
arguments,
range: (location..end_location).into()
}.into()
},
}
PatternArguments: ast::PatternArguments = {
<location:@L> "(" <patterns: OneOrMore<Pattern>> "," <keywords:OneOrMore<MatchKeywordEntry>> ","? ")" <end_location:@R> => {
ast::PatternArguments {
patterns,
keywords,
range: (location..end_location).into()
}
},
<location:@L> "(" <patterns: OneOrMore<Pattern>> ","? ")" <end_location:@R> => {
ast::PatternArguments {
patterns,
keywords: vec![],
range: (location..end_location).into()
}
},
<location:@L> "(" <keywords:OneOrMore<MatchKeywordEntry>> ","? ")" <end_location:@R> => {
ast::PatternArguments {
patterns: vec![],
keywords,
range: (location..end_location).into()
}
},
<location:@L> "(" ")" <end_location:@R> => {
ast::PatternArguments {
patterns: vec![],
keywords: vec![],
range: (location..end_location).into()
}
},
};
IfStatement: ast::Stmt = {
<location:@L> "if" <test:NamedExpressionTest> ":" <body:Suite> <s2:(<@L> "elif" <NamedExpressionTest> ":" <Suite>)*> <s3:(<@L> "else" ":" <Suite>)?> => {
let elif_else_clauses: Vec<_> = s2.into_iter().map(|(start, test, body)| ast::ElifElseClause {

File diff suppressed because it is too large Load diff

View file

@ -168,9 +168,11 @@ expression: parse_ast
ctx: Load,
},
),
arguments: PatternArguments {
range: 130..132,
patterns: [],
kwd_attrs: [],
kwd_patterns: [],
keywords: [],
},
},
),
MatchSingleton(

View file

@ -90,6 +90,8 @@ expression: parse_ast
ctx: Load,
},
),
arguments: PatternArguments {
range: 149..152,
patterns: [
MatchAs(
PatternMatchAs {
@ -104,8 +106,8 @@ expression: parse_ast
},
),
],
kwd_attrs: [],
kwd_patterns: [],
keywords: [],
},
},
),
guard: None,