Format UnaryExpr

<!--
Thank you for contributing to Ruff! To help us out with reviewing, please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

This PR adds basic formatting for unary expressions.

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

I added a new `unary.py` with custom test cases
This commit is contained in:
Micha Reiser 2023-06-21 10:09:47 +02:00 committed by GitHub
parent 3973836420
commit 1336ca601b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 623 additions and 115 deletions

View file

@ -323,6 +323,10 @@ fn can_break(expr: &Expr) -> bool {
}) => !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,17 +1,68 @@
use crate::comments::Comments;
use crate::comments::{trailing_comments, Comments};
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{not_yet_implemented, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprUnaryOp;
use crate::prelude::*;
use crate::trivia::{SimpleTokenizer, TokenKind};
use crate::FormatNodeRule;
use ruff_formatter::FormatContext;
use ruff_python_ast::prelude::UnaryOp;
use ruff_text_size::{TextLen, TextRange};
use rustpython_parser::ast::{ExprUnaryOp, Ranged};
#[derive(Default)]
pub struct FormatExprUnaryOp;
impl FormatNodeRule<ExprUnaryOp> for FormatExprUnaryOp {
fn fmt_fields(&self, item: &ExprUnaryOp, f: &mut PyFormatter) -> FormatResult<()> {
write!(f, [not_yet_implemented(item)])
let ExprUnaryOp {
range: _,
op,
operand,
} = item;
let operator = match op {
UnaryOp::Invert => "~",
UnaryOp::Not => "not",
UnaryOp::UAdd => "+",
UnaryOp::USub => "-",
};
text(operator).fmt(f)?;
let comments = f.context().comments().clone();
// Split off the comments that follow after the operator and format them as trailing comments.
// ```python
// (not # comment
// a)
// ```
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());
let (trailing_operator_comments, leading_operand_comments) =
leading_operand_comments.split_at(trailing_operator_comments_end);
if !trailing_operator_comments.is_empty() {
trailing_comments(trailing_operator_comments).fmt(f)?;
}
// Insert a line break if the operand has comments but itself is not parenthesized.
// ```python
// if (
// not
// # comment
// a)
// ```
if !leading_operand_comments.is_empty()
&& !is_operand_parenthesized(item, f.context().source_code().as_str())
{
hard_line_break().fmt(f)?;
} else if op.is_not() {
space().fmt(f)?;
}
operand.format().fmt(f)
}
}
@ -22,6 +73,37 @@ impl NeedsParentheses for ExprUnaryOp {
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 => {
// We preserve the parentheses of the operand. It should not be necessary to break this expression.
if is_operand_parenthesized(self, source) {
Parentheses::Never
} else {
Parentheses::Optional
}
}
parentheses => parentheses,
}
}
}
fn is_operand_parenthesized(unary: &ExprUnaryOp, source: &str) -> bool {
let operator_len = match unary.op {
UnaryOp::Invert => '~'.text_len(),
UnaryOp::Not => "not".text_len(),
UnaryOp::UAdd => '+'.text_len(),
UnaryOp::USub => '-'.text_len(),
};
let trivia_range = TextRange::new(unary.range.start() + operator_len, unary.operand.start());
if let Some(token) = SimpleTokenizer::new(source, trivia_range)
.skip_trivia()
.next()
{
debug_assert_eq!(token.kind(), TokenKind::LParen);
true
} else {
false
}
}

View file

@ -42,7 +42,7 @@ pub(super) fn default_expression_needs_parentheses(
}
/// Configures if the expression should be parenthesized.
#[derive(Copy, Clone, Debug, Default)]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum Parenthesize {
/// Parenthesize the expression if it has parenthesis in the source.
#[default]
@ -56,11 +56,11 @@ pub enum Parenthesize {
}
impl Parenthesize {
const fn is_if_breaks(self) -> bool {
pub(crate) const fn is_if_breaks(self) -> bool {
matches!(self, Parenthesize::IfBreaks)
}
const fn is_preserve(self) -> bool {
pub(crate) const fn is_preserve(self) -> bool {
matches!(self, Parenthesize::Preserve)
}
}