Format BoolOp (#4986)

This commit is contained in:
Micha Reiser 2023-06-21 11:27:57 +02:00 committed by GitHub
parent db301c14bd
commit 653dbb6d17
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 804 additions and 446 deletions

View file

@ -0,0 +1,215 @@
//! This module provides helper utilities to format an expression that has a left side, an operator,
//! and a right side (binary like).
use crate::expression::parentheses::Parentheses;
use crate::prelude::*;
use ruff_formatter::{format_args, write};
use rustpython_parser::ast::Expr;
/// Trait to implement a binary like syntax that has a left operand, an operator, and a right operand.
pub(super) trait FormatBinaryLike<'ast> {
/// The type implementing the formatting of the operator.
type FormatOperator: Format<PyFormatContext<'ast>>;
/// Formats the binary like expression to `f`.
fn fmt_binary(
&self,
parentheses: Option<Parentheses>,
f: &mut PyFormatter<'ast, '_>,
) -> FormatResult<()> {
let left = self.left()?;
let operator = self.operator();
let right = self.right()?;
let layout = if parentheses == Some(Parentheses::Custom) {
self.binary_layout()
} else {
BinaryLayout::Default
};
match layout {
BinaryLayout::Default => self.fmt_default(f),
BinaryLayout::ExpandLeft => {
let left = left.format().memoized();
let right = right.format().memoized();
write!(
f,
[best_fitting![
// Everything on a single line
format_args![group(&left), space(), operator, space(), right],
// Break the left over multiple lines, keep the right flat
format_args![
group(&left).should_expand(true),
space(),
operator,
space(),
right
],
// The content doesn't fit, indent the content and break before the operator.
format_args![
text("("),
block_indent(&format_args![
left,
hard_line_break(),
operator,
space(),
right
]),
text(")")
]
]
.with_mode(BestFittingMode::AllLines)]
)
}
BinaryLayout::ExpandRight => {
let left_group = f.group_id("BinaryLeft");
write!(
f,
[
// Wrap the left in a group and gives it an id. The printer first breaks the
// right side if `right` contains any line break because the printer breaks
// sequences of groups from right to left.
// Indents the left side if the group breaks.
group(&format_args![
if_group_breaks(&text("(")),
indent_if_group_breaks(
&format_args![
soft_line_break(),
left.format(),
soft_line_break_or_space(),
operator,
space()
],
left_group
)
])
.with_group_id(Some(left_group)),
// Wrap the right in a group and indents its content but only if the left side breaks
group(&indent_if_group_breaks(&right.format(), left_group)),
// If the left side breaks, insert a hard line break to finish the indent and close the open paren.
if_group_breaks(&format_args![hard_line_break(), text(")")])
.with_group_id(Some(left_group))
]
)
}
BinaryLayout::ExpandRightThenLeft => {
// The formatter expands group-sequences from right to left, and expands both if
// there isn't enough space when expanding only one of them.
write!(
f,
[
group(&left.format()),
space(),
operator,
space(),
group(&right.format())
]
)
}
}
}
/// Determines which binary layout to use.
fn binary_layout(&self) -> BinaryLayout {
if let (Ok(left), Ok(right)) = (self.left(), self.right()) {
BinaryLayout::from_left_right(left, right)
} else {
BinaryLayout::Default
}
}
/// Formats the node according to the default layout.
fn fmt_default(&self, f: &mut PyFormatter<'ast, '_>) -> FormatResult<()>;
/// Returns the left operator
fn left(&self) -> FormatResult<&Expr>;
/// Returns the right operator.
fn right(&self) -> FormatResult<&Expr>;
/// Returns the object that formats the operator.
fn operator(&self) -> Self::FormatOperator;
}
fn can_break_expr(expr: &Expr) -> bool {
use ruff_python_ast::prelude::*;
match expr {
Expr::Tuple(ExprTuple {
elts: expressions, ..
})
| Expr::List(ExprList {
elts: expressions, ..
})
| Expr::Set(ExprSet {
elts: expressions, ..
})
| Expr::Dict(ExprDict {
values: expressions,
..
}) => !expressions.is_empty(),
Expr::Call(ExprCall { args, keywords, .. }) => !(args.is_empty() && keywords.is_empty()),
Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) | Expr::GeneratorExp(_) => true,
Expr::UnaryOp(ExprUnaryOp { operand, .. }) => match operand.as_ref() {
Expr::BinOp(_) => true,
_ => can_break_expr(operand.as_ref()),
},
_ => false,
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) enum BinaryLayout {
/// Put each operand on their own line if either side expands
Default,
/// Try to expand the left to make it fit. Add parentheses if the left or right don't fit.
///
///```python
/// [
/// a,
/// b
/// ] & c
///```
ExpandLeft,
/// Try to expand the right to make it fix. Add parentheses if the left or right don't fit.
///
/// ```python
/// a & [
/// b,
/// c
/// ]
/// ```
ExpandRight,
/// Both the left and right side can be expanded. Try in the following order:
/// * expand the right side
/// * expand the left side
/// * expand both sides
///
/// to make the expression fit
///
/// ```python
/// [
/// a,
/// b
/// ] & [
/// c,
/// d
/// ]
/// ```
ExpandRightThenLeft,
}
impl BinaryLayout {
pub(super) fn from_left_right(left: &Expr, right: &Expr) -> BinaryLayout {
match (can_break_expr(left), can_break_expr(right)) {
(false, false) => BinaryLayout::Default,
(true, false) => BinaryLayout::ExpandLeft,
(false, true) => BinaryLayout::ExpandRight,
(true, true) => BinaryLayout::ExpandRightThenLeft,
}
}
}

View file

@ -1,14 +1,12 @@
use crate::comments::{trailing_comments, Comments};
use crate::expression::binary_like::{BinaryLayout, FormatBinaryLike};
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parenthesize,
};
use crate::expression::Parentheses;
use crate::prelude::*;
use crate::FormatNodeRule;
use ruff_formatter::{
format_args, write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions,
};
use ruff_python_ast::node::AstNode;
use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions};
use rustpython_parser::ast::{
Constant, Expr, ExprAttribute, ExprBinOp, ExprConstant, ExprUnaryOp, Operator, UnaryOp,
};
@ -29,132 +27,7 @@ impl FormatRuleWithOptions<ExprBinOp, PyFormatContext<'_>> for FormatExprBinOp {
impl FormatNodeRule<ExprBinOp> for FormatExprBinOp {
fn fmt_fields(&self, item: &ExprBinOp, f: &mut PyFormatter) -> FormatResult<()> {
let ExprBinOp {
left,
right,
op,
range: _,
} = item;
let layout = if self.parentheses == Some(Parentheses::Custom) {
BinaryLayout::from(item)
} else {
BinaryLayout::Default
};
match layout {
BinaryLayout::Default => {
let comments = f.context().comments().clone();
let operator_comments = comments.dangling_comments(item.as_any_node_ref());
let needs_space = !is_simple_power_expression(item);
let before_operator_space = if needs_space {
soft_line_break_or_space()
} else {
soft_line_break()
};
write!(
f,
[
left.format(),
before_operator_space,
op.format(),
trailing_comments(operator_comments),
]
)?;
// Format the operator on its own line if the right side has any leading comments.
if comments.has_leading_comments(right.as_ref()) {
write!(f, [hard_line_break()])?;
} else if needs_space {
write!(f, [space()])?;
}
write!(f, [group(&right.format())])
}
BinaryLayout::ExpandLeft => {
let left = left.format().memoized();
let right = right.format().memoized();
write!(
f,
[best_fitting![
// Everything on a single line
format_args![left, space(), op.format(), space(), right],
// Break the left over multiple lines, keep the right flat
format_args![
group(&left).should_expand(true),
space(),
op.format(),
space(),
right
],
// The content doesn't fit, indent the content and break before the operator.
format_args![
text("("),
block_indent(&format_args![
left,
hard_line_break(),
op.format(),
space(),
right
]),
text(")")
]
]
.with_mode(BestFittingMode::AllLines)]
)
}
BinaryLayout::ExpandRight => {
let left_group = f.group_id("BinaryLeft");
write!(
f,
[
// Wrap the left in a group and gives it an id. The printer first breaks the
// right side if `right` contains any line break because the printer breaks
// sequences of groups from right to left.
// Indents the left side if the group breaks.
group(&format_args![
if_group_breaks(&text("(")),
indent_if_group_breaks(
&format_args![
soft_line_break(),
left.format(),
soft_line_break_or_space(),
op.format(),
space()
],
left_group
)
])
.with_group_id(Some(left_group)),
// Wrap the right in a group and indents its content but only if the left side breaks
group(&indent_if_group_breaks(&right.format(), left_group)),
// If the left side breaks, insert a hard line break to finish the indent and close the open paren.
if_group_breaks(&format_args![hard_line_break(), text(")")])
.with_group_id(Some(left_group))
]
)
}
BinaryLayout::ExpandRightThenLeft => {
// The formatter expands group-sequences from right to left, and expands both if
// there isn't enough space when expanding only one of them.
write!(
f,
[
group(&left.format()),
space(),
op.format(),
space(),
group(&right.format())
]
)
}
}
item.fmt_binary(self.parentheses, f)
}
fn fmt_dangling_comments(&self, _node: &ExprBinOp, _f: &mut PyFormatter) -> FormatResult<()> {
@ -163,6 +36,60 @@ impl FormatNodeRule<ExprBinOp> for FormatExprBinOp {
}
}
impl<'ast> FormatBinaryLike<'ast> for ExprBinOp {
type FormatOperator = FormatOwnedWithRule<Operator, FormatOperator, PyFormatContext<'ast>>;
fn fmt_default(&self, f: &mut PyFormatter<'ast, '_>) -> FormatResult<()> {
let ExprBinOp {
range: _,
left,
op,
right,
} = self;
let comments = f.context().comments().clone();
let operator_comments = comments.dangling_comments(self);
let needs_space = !is_simple_power_expression(self);
let before_operator_space = if needs_space {
soft_line_break_or_space()
} else {
soft_line_break()
};
write!(
f,
[
left.format(),
before_operator_space,
op.format(),
trailing_comments(operator_comments),
]
)?;
// Format the operator on its own line if the right side has any leading comments.
if comments.has_leading_comments(right.as_ref()) {
write!(f, [hard_line_break()])?;
} else if needs_space {
write!(f, [space()])?;
}
write!(f, [group(&right.format())])
}
fn left(&self) -> FormatResult<&Expr> {
Ok(&self.left)
}
fn right(&self) -> FormatResult<&Expr> {
Ok(&self.right)
}
fn operator(&self) -> Self::FormatOperator {
self.op.into_format()
}
}
const fn is_simple_power_expression(expr: &ExprBinOp) -> bool {
expr.op.is_pow() && is_simple_power_operand(&expr.left) && is_simple_power_operand(&expr.right)
}
@ -235,7 +162,7 @@ impl NeedsParentheses for ExprBinOp {
) -> Parentheses {
match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) {
Parentheses::Optional => {
if BinaryLayout::from(self) == BinaryLayout::Default
if self.binary_layout() == BinaryLayout::Default
|| comments.has_leading_comments(self.right.as_ref())
|| comments.has_dangling_comments(self)
{
@ -248,85 +175,3 @@ impl NeedsParentheses for ExprBinOp {
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum BinaryLayout {
/// Put each operand on their own line if either side expands
Default,
/// Try to expand the left to make it fit. Add parentheses if the left or right don't fit.
///
///```python
/// [
/// a,
/// b
/// ] & c
///```
ExpandLeft,
/// Try to expand the right to make it fix. Add parentheses if the left or right don't fit.
///
/// ```python
/// a & [
/// b,
/// c
/// ]
/// ```
ExpandRight,
/// Both the left and right side can be expanded. Try in the following order:
/// * expand the right side
/// * expand the left side
/// * expand both sides
///
/// to make the expression fit
///
/// ```python
/// [
/// a,
/// b
/// ] & [
/// c,
/// d
/// ]
/// ```
ExpandRightThenLeft,
}
impl BinaryLayout {
fn from(expr: &ExprBinOp) -> Self {
match (can_break(&expr.left), can_break(&expr.right)) {
(false, false) => Self::Default,
(true, false) => Self::ExpandLeft,
(false, true) => Self::ExpandRight,
(true, true) => Self::ExpandRightThenLeft,
}
}
}
fn can_break(expr: &Expr) -> bool {
use ruff_python_ast::prelude::*;
match expr {
Expr::Tuple(ExprTuple {
elts: expressions, ..
})
| Expr::List(ExprList {
elts: expressions, ..
})
| Expr::Set(ExprSet {
elts: expressions, ..
})
| Expr::Dict(ExprDict {
values: expressions,
..
}) => !expressions.is_empty(),
Expr::Call(ExprCall { args, keywords, .. }) => !(args.is_empty() && keywords.is_empty()),
Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) | Expr::GeneratorExp(_) => true,
Expr::UnaryOp(ExprUnaryOp { operand, .. }) => match operand.as_ref() {
Expr::BinOp(_) => true,
_ => can_break(operand.as_ref()),
},
_ => false,
}
}

View file

@ -1,22 +1,87 @@
use crate::comments::Comments;
use crate::comments::{leading_comments, Comments};
use crate::expression::binary_like::{BinaryLayout, FormatBinaryLike};
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprBoolOp;
use crate::prelude::*;
use ruff_formatter::{
write, FormatError, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions,
};
use rustpython_parser::ast::{BoolOp, Expr, ExprBoolOp};
#[derive(Default)]
pub struct FormatExprBoolOp;
pub struct FormatExprBoolOp {
parentheses: Option<Parentheses>,
}
impl FormatRuleWithOptions<ExprBoolOp, PyFormatContext<'_>> for FormatExprBoolOp {
type Options = Option<Parentheses>;
fn with_options(mut self, options: Self::Options) -> Self {
self.parentheses = options;
self
}
}
impl FormatNodeRule<ExprBoolOp> for FormatExprBoolOp {
fn fmt_fields(&self, _item: &ExprBoolOp, f: &mut PyFormatter) -> FormatResult<()> {
write!(
f,
[not_yet_implemented_custom_text(
"NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2"
)]
)
fn fmt_fields(&self, item: &ExprBoolOp, f: &mut PyFormatter) -> FormatResult<()> {
item.fmt_binary(self.parentheses, f)
}
}
impl<'ast> FormatBinaryLike<'ast> for ExprBoolOp {
type FormatOperator = FormatOwnedWithRule<BoolOp, FormatBoolOp, PyFormatContext<'ast>>;
fn binary_layout(&self) -> BinaryLayout {
match self.values.as_slice() {
[left, right] => BinaryLayout::from_left_right(left, right),
[..] => BinaryLayout::Default,
}
}
fn fmt_default(&self, f: &mut PyFormatter<'ast, '_>) -> FormatResult<()> {
let ExprBoolOp {
range: _,
op,
values,
} = self;
let mut values = values.iter();
let comments = f.context().comments().clone();
let Some(first) = values.next() else {
return Ok(())
};
write!(f, [group(&first.format())])?;
for value in values {
let leading_value_comments = comments.leading_comments(value);
// Format the expressions leading comments **before** the operator
if leading_value_comments.is_empty() {
write!(f, [soft_line_break_or_space()])?;
} else {
write!(
f,
[hard_line_break(), leading_comments(leading_value_comments)]
)?;
}
write!(f, [op.format(), space(), group(&value.format())])?;
}
Ok(())
}
fn left(&self) -> FormatResult<&Expr> {
self.values.first().ok_or(FormatError::SyntaxError)
}
fn right(&self) -> FormatResult<&Expr> {
self.values.last().ok_or(FormatError::SyntaxError)
}
fn operator(&self) -> Self::FormatOperator {
self.op.into_format()
}
}
@ -27,6 +92,53 @@ impl NeedsParentheses for ExprBoolOp {
source: &str,
comments: &Comments,
) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source, comments)
match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) {
Parentheses::Optional => match self.binary_layout() {
BinaryLayout::Default => Parentheses::Optional,
BinaryLayout::ExpandRight
| BinaryLayout::ExpandLeft
| BinaryLayout::ExpandRightThenLeft
if self
.values
.last()
.map_or(false, |right| comments.has_leading_comments(right)) =>
{
Parentheses::Optional
}
_ => Parentheses::Custom,
},
parentheses => parentheses,
}
}
}
#[derive(Copy, Clone)]
pub struct FormatBoolOp;
impl<'ast> AsFormat<PyFormatContext<'ast>> for BoolOp {
type Format<'a> = FormatRefWithRule<'a, BoolOp, FormatBoolOp, PyFormatContext<'ast>>;
fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(self, FormatBoolOp)
}
}
impl<'ast> IntoFormat<PyFormatContext<'ast>> for BoolOp {
type Format = FormatOwnedWithRule<BoolOp, FormatBoolOp, PyFormatContext<'ast>>;
fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(self, FormatBoolOp)
}
}
impl FormatRule<BoolOp, PyFormatContext<'_>> for FormatBoolOp {
fn fmt(&self, item: &BoolOp, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
let operator = match item {
BoolOp::And => "and",
BoolOp::Or => "or",
};
text(operator).fmt(f)
}
}

View file

@ -39,7 +39,7 @@ impl FormatNodeRule<ExprUnaryOp> for FormatExprUnaryOp {
// ```
let leading_operand_comments = comments.leading_comments(operand.as_ref());
let trailing_operator_comments_end =
leading_operand_comments.partition_point(|p| p.position().is_end_of_line());
leading_operand_comments.partition_point(|p| p.line_position().is_end_of_line());
let (trailing_operator_comments, leading_operand_comments) =
leading_operand_comments.split_at(trailing_operator_comments_end);

View file

@ -7,6 +7,7 @@ use ruff_formatter::{
};
use rustpython_parser::ast::Expr;
mod binary_like;
pub(crate) mod expr_attribute;
pub(crate) mod expr_await;
pub(crate) mod expr_bin_op;
@ -59,7 +60,7 @@ impl FormatRule<Expr, PyFormatContext<'_>> for FormatExpr {
);
let format_expr = format_with(|f| match item {
Expr::BoolOp(expr) => expr.format().fmt(f),
Expr::BoolOp(expr) => expr.format().with_options(Some(parentheses)).fmt(f),
Expr::NamedExpr(expr) => expr.format().fmt(f),
Expr::BinOp(expr) => expr.format().with_options(Some(parentheses)).fmt(f),
Expr::UnaryOp(expr) => expr.format().fmt(f),