Format while Statement (#4810)

This commit is contained in:
Micha Reiser 2023-06-05 10:24:00 +02:00 committed by GitHub
parent d1d06960f0
commit c65f47d7c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 555 additions and 145 deletions

View file

@ -0,0 +1,30 @@
while 34: # trailing test comment
pass # trailing last statement comment
# trailing while body comment
# leading else comment
else: # trailing else comment
pass
# trailing else body comment
while aVeryLongConditionThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn: # trailing comment
pass
else:
...
while (
some_condition(unformatted, args) and anotherCondition or aThirdCondition
): # comment
print("Do something")
while (
some_condition(unformatted, args) # trailing some condition
and anotherCondition or aThirdCondition # trailing third condition
): # comment
print("Do something")

View file

@ -11,7 +11,7 @@ pub(crate) trait PyFormatterExtensions<'ast, 'buf> {
/// empty lines between any two nodes. Separates any two nodes by at least a hard line break. /// empty lines between any two nodes. Separates any two nodes by at least a hard line break.
/// ///
/// * [`NodeLevel::Module`]: Up to two empty lines /// * [`NodeLevel::Module`]: Up to two empty lines
/// * [`NodeLevel::Statement`]: Up to one empty line /// * [`NodeLevel::CompoundStatement`]: Up to one empty line
/// * [`NodeLevel::Parenthesized`]: No empty lines /// * [`NodeLevel::Parenthesized`]: No empty lines
fn join_nodes<'fmt>(&'fmt mut self, level: NodeLevel) -> JoinNodesBuilder<'fmt, 'ast, 'buf>; fn join_nodes<'fmt>(&'fmt mut self, level: NodeLevel) -> JoinNodesBuilder<'fmt, 'ast, 'buf>;
} }
@ -53,10 +53,12 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> {
2 => empty_line().fmt(f), 2 => empty_line().fmt(f),
_ => write!(f, [empty_line(), empty_line()]), _ => write!(f, [empty_line(), empty_line()]),
}, },
NodeLevel::Statement => match lines_before(f.context().contents(), node.start()) { NodeLevel::CompoundStatement => {
match lines_before(f.context().contents(), node.start()) {
0 | 1 => hard_line_break().fmt(f), 0 | 1 => hard_line_break().fmt(f),
_ => empty_line().fmt(f), _ => empty_line().fmt(f),
}, }
}
NodeLevel::Parenthesized => hard_line_break().fmt(f), NodeLevel::Parenthesized => hard_line_break().fmt(f),
}); });
@ -180,7 +182,7 @@ no_leading_newline = 30"#
// Should keep at most one empty level // Should keep at most one empty level
#[test] #[test]
fn ranged_builder_statement_level() { fn ranged_builder_statement_level() {
let printed = format_ranged(NodeLevel::Statement); let printed = format_ranged(NodeLevel::CompoundStatement);
assert_eq!( assert_eq!(
&printed, &printed,

View file

@ -6,27 +6,37 @@ use ruff_formatter::{format_args, write, FormatError, SourceCode};
use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::prelude::AstNode; use ruff_python_ast::prelude::AstNode;
use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_text_size::{TextLen, TextRange, TextSize};
use rustpython_parser::ast::Ranged;
/// Formats the leading comments of a node. /// Formats the leading comments of a node.
pub(crate) fn leading_comments<T>(node: &T) -> FormatLeadingComments pub(crate) fn leading_node_comments<T>(node: &T) -> FormatLeadingComments
where where
T: AstNode, T: AstNode,
{ {
FormatLeadingComments { FormatLeadingComments::Node(node.as_any_node_ref())
node: node.as_any_node_ref(),
} }
/// Formats the passed comments as leading comments
pub(crate) const fn leading_comments(comments: &[SourceComment]) -> FormatLeadingComments {
FormatLeadingComments::Comments(comments)
} }
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub(crate) struct FormatLeadingComments<'a> { pub(crate) enum FormatLeadingComments<'a> {
node: AnyNodeRef<'a>, Node(AnyNodeRef<'a>),
Comments(&'a [SourceComment]),
} }
impl Format<PyFormatContext<'_>> for FormatLeadingComments<'_> { impl Format<PyFormatContext<'_>> for FormatLeadingComments<'_> {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
let comments = f.context().comments().clone(); let comments = f.context().comments().clone();
for comment in comments.leading_comments(self.node) { let leading_comments = match self {
FormatLeadingComments::Node(node) => comments.leading_comments(*node),
FormatLeadingComments::Comments(comments) => comments,
};
for comment in leading_comments {
let slice = comment.slice(); let slice = comment.slice();
let lines_after_comment = lines_after(f.context().contents(), slice.end()); let lines_after_comment = lines_after(f.context().contents(), slice.end());
@ -42,32 +52,88 @@ impl Format<PyFormatContext<'_>> for FormatLeadingComments<'_> {
} }
} }
/// Formats the trailing comments of `node` /// Formats the leading `comments` of an alternate branch and ensures that it preserves the right
pub(crate) fn trailing_comments<T>(node: &T) -> FormatTrailingComments /// number of empty lines before. The `last_node` is the last node of the preceding body.
///
/// For example, `last_node` is the last statement in the if body when formatting the leading
/// comments of the `else` branch.
pub(crate) fn leading_alternate_branch_comments<'a, T>(
comments: &'a [SourceComment],
last_node: Option<T>,
) -> FormatLeadingAlternateBranchComments<'a>
where where
T: AstNode, T: Into<AnyNodeRef<'a>>,
{ {
FormatTrailingComments { FormatLeadingAlternateBranchComments {
node: node.as_any_node_ref(), comments,
last_node: last_node.map(std::convert::Into::into),
} }
} }
pub(crate) struct FormatTrailingComments<'a> { pub(crate) struct FormatLeadingAlternateBranchComments<'a> {
node: AnyNodeRef<'a>, comments: &'a [SourceComment],
last_node: Option<AnyNodeRef<'a>>,
}
impl Format<PyFormatContext<'_>> for FormatLeadingAlternateBranchComments<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
if let Some(first_leading) = self.comments.first() {
// Leading comments only preserves the lines after the comment but not before.
// Insert the necessary lines.
if lines_before(f.context().contents(), first_leading.slice().start()) > 1 {
write!(f, [empty_line()])?;
}
write!(f, [leading_comments(self.comments)])?;
} else if let Some(last_preceding) = self.last_node {
// The leading comments formatting ensures that it preserves the right amount of lines after
// We need to take care of this ourselves, if there's no leading `else` comment.
if lines_after(f.context().contents(), last_preceding.end()) > 1 {
write!(f, [empty_line()])?;
}
}
Ok(())
}
}
/// Formats the trailing comments of `node`
pub(crate) fn trailing_node_comments<T>(node: &T) -> FormatTrailingComments
where
T: AstNode,
{
FormatTrailingComments::Node(node.as_any_node_ref())
}
/// Formats the passed comments as trailing comments
pub(crate) fn trailing_comments(comments: &[SourceComment]) -> FormatTrailingComments {
FormatTrailingComments::Comments(comments)
}
pub(crate) enum FormatTrailingComments<'a> {
Node(AnyNodeRef<'a>),
Comments(&'a [SourceComment]),
} }
impl Format<PyFormatContext<'_>> for FormatTrailingComments<'_> { impl Format<PyFormatContext<'_>> for FormatTrailingComments<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> { fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
let comments = f.context().comments().clone(); let comments = f.context().comments().clone();
let mut has_empty_lines_before = false;
for trailing in comments.trailing_comments(self.node) { let trailing_comments = match self {
FormatTrailingComments::Node(node) => comments.trailing_comments(*node),
FormatTrailingComments::Comments(comments) => comments,
};
let mut has_trailing_own_line_comment = false;
for trailing in trailing_comments {
let slice = trailing.slice(); let slice = trailing.slice();
let lines_before_comment = lines_before(f.context().contents(), slice.start()); has_trailing_own_line_comment |= trailing.position().is_own_line();
has_empty_lines_before |= lines_before_comment > 0;
if has_trailing_own_line_comment {
let lines_before_comment = lines_before(f.context().contents(), slice.start());
if has_empty_lines_before {
// A trailing comment at the end of a body or list // A trailing comment at the end of a body or list
// ```python // ```python
// def test(): // def test():
@ -105,7 +171,7 @@ impl Format<PyFormatContext<'_>> for FormatTrailingComments<'_> {
} }
/// Formats the dangling comments of `node`. /// Formats the dangling comments of `node`.
pub(crate) fn dangling_comments<T>(node: &T) -> FormatDanglingComments pub(crate) fn dangling_node_comments<T>(node: &T) -> FormatDanglingComments
where where
T: AstNode, T: AstNode,
{ {
@ -229,7 +295,7 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLines {
_ => write!(f, [empty_line(), empty_line()]), _ => write!(f, [empty_line(), empty_line()]),
}, },
NodeLevel::Statement => match self.lines { NodeLevel::CompoundStatement => match self.lines {
0 | 1 => write!(f, [hard_line_break()]), 0 | 1 => write!(f, [hard_line_break()]),
_ => write!(f, [empty_line()]), _ => write!(f, [empty_line()]),
}, },

View file

@ -103,7 +103,10 @@ use crate::comments::debug::{DebugComment, DebugComments};
use crate::comments::map::MultiMap; use crate::comments::map::MultiMap;
use crate::comments::node_key::NodeRefEqualityKey; use crate::comments::node_key::NodeRefEqualityKey;
use crate::comments::visitor::CommentsVisitor; use crate::comments::visitor::CommentsVisitor;
pub(crate) use format::{dangling_comments, leading_comments, trailing_comments}; pub(crate) use format::{
dangling_node_comments, leading_alternate_branch_comments, leading_node_comments,
trailing_comments, trailing_node_comments,
};
use ruff_formatter::{SourceCode, SourceCodeSlice}; use ruff_formatter::{SourceCode, SourceCodeSlice};
use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::source_code::CommentRanges; use ruff_python_ast::source_code::CommentRanges;
@ -121,8 +124,6 @@ pub(crate) struct SourceComment {
position: CommentTextPosition, position: CommentTextPosition,
} }
#[allow(unused)]
// TODO(micha): Remove after using the new comments infrastructure in the formatter.
impl SourceComment { impl SourceComment {
/// Returns the location of the comment in the original source code. /// Returns the location of the comment in the original source code.
/// Allows retrieving the text of the comment. /// Allows retrieving the text of the comment.
@ -184,8 +185,6 @@ pub(crate) enum CommentTextPosition {
OwnLine, OwnLine,
} }
#[allow(unused)]
// TODO(micha): Remove after using the new comments infrastructure in the formatter.
impl CommentTextPosition { impl CommentTextPosition {
pub(crate) const fn is_own_line(self) -> bool { pub(crate) const fn is_own_line(self) -> bool {
matches!(self, CommentTextPosition::OwnLine) matches!(self, CommentTextPosition::OwnLine)

View file

@ -5,7 +5,7 @@ use crate::trivia::find_first_non_trivia_character_in_range;
use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::source_code::Locator; use ruff_python_ast::source_code::Locator;
use ruff_python_ast::whitespace; use ruff_python_ast::whitespace;
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextLen, TextRange, TextSize};
use rustpython_parser::ast::Ranged; use rustpython_parser::ast::Ranged;
use std::cmp::Ordering; use std::cmp::Ordering;
@ -20,6 +20,7 @@ pub(super) fn place_comment<'a>(
.or_else(|comment| handle_in_between_bodies_end_of_line_comment(comment, locator)) .or_else(|comment| handle_in_between_bodies_end_of_line_comment(comment, locator))
.or_else(|comment| handle_trailing_body_comment(comment, locator)) .or_else(|comment| handle_trailing_body_comment(comment, locator))
.or_else(handle_trailing_end_of_line_body_comment) .or_else(handle_trailing_end_of_line_body_comment)
.or_else(|comment| handle_trailing_end_of_line_condition_comment(comment, locator))
.or_else(|comment| handle_positional_only_arguments_separator_comment(comment, locator)) .or_else(|comment| handle_positional_only_arguments_separator_comment(comment, locator))
.or_else(|comment| { .or_else(|comment| {
handle_trailing_binary_expression_left_or_operator_comment(comment, locator) handle_trailing_binary_expression_left_or_operator_comment(comment, locator)
@ -471,6 +472,91 @@ fn handle_trailing_end_of_line_body_comment(comment: DecoratedComment<'_>) -> Co
} }
} }
/// Handles end of line comments after the `:` of a condition
///
/// ```python
/// while True: # comment
/// pass
/// ```
///
/// It attaches the comment as dangling comment to the enclosing `while` statement.
fn handle_trailing_end_of_line_condition_comment<'a>(
comment: DecoratedComment<'a>,
locator: &Locator,
) -> CommentPlacement<'a> {
use ruff_python_ast::prelude::*;
// Must be an end of line comment
if comment.text_position().is_own_line() {
return CommentPlacement::Default(comment);
}
// Must be between the condition expression and the first body element
let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) else {
return CommentPlacement::Default(comment);
};
let expression_before_colon = match comment.enclosing_node() {
AnyNodeRef::StmtIf(StmtIf { test: expr, .. })
| AnyNodeRef::StmtWhile(StmtWhile { test: expr, .. })
| AnyNodeRef::StmtFor(StmtFor { iter: expr, .. })
| AnyNodeRef::StmtAsyncFor(StmtAsyncFor { iter: expr, .. }) => {
Some(AnyNodeRef::from(expr.as_ref()))
}
AnyNodeRef::StmtWith(StmtWith { items, .. })
| AnyNodeRef::StmtAsyncWith(StmtAsyncWith { items, .. }) => {
items.last().map(AnyNodeRef::from)
}
_ => None,
};
let Some(last_before_colon) = expression_before_colon else {
return CommentPlacement::Default(comment);
};
// If the preceding is the node before the `colon`
// `while true:` The node before the `colon` is the `true` constant.
if preceding.ptr_eq(last_before_colon) {
let mut start = preceding.end();
while let Some((offset, c)) = find_first_non_trivia_character_in_range(
locator.contents(),
TextRange::new(start, following.start()),
) {
match c {
':' => {
if comment.slice().start() > offset {
// Comment comes after the colon
// ```python
// while a: # comment
// ...
// ```
return CommentPlacement::dangling(comment.enclosing_node(), comment);
}
// Comment comes before the colon
// ```python
// while (
// a # comment
// ):
// ...
// ```
break;
}
')' => {
// Skip over any closing parentheses
start = offset + ')'.text_len();
}
_ => {
unreachable!("Only ')' or ':' should follow the condition")
}
}
}
}
CommentPlacement::Default(comment)
}
/// Attaches comments for the positional-only arguments separator `/` as trailing comments to the /// Attaches comments for the positional-only arguments separator `/` as trailing comments to the
/// enclosing [`Arguments`] node. /// enclosing [`Arguments`] node.
/// ///

View file

@ -19,19 +19,19 @@ expression: comments.debug(test_case.source_code)
"trailing": [], "trailing": [],
}, },
Node { Node {
kind: ExprCompare, kind: StmtIf,
range: 51..57, range: 48..212,
source: `x == y`, source: `if x == y: # if statement e...ne comment⏎`,
}: { }: {
"leading": [], "leading": [],
"dangling": [], "dangling": [
"trailing": [
SourceComment { SourceComment {
text: "# if statement end of line comment", text: "# if statement end of line comment",
position: EndOfLine, position: EndOfLine,
formatted: false, formatted: false,
}, },
], ],
"trailing": [],
}, },
Node { Node {
kind: StmtIf, kind: StmtIf,

View file

@ -25,7 +25,6 @@ impl<'a> PyFormatContext<'a> {
} }
} }
#[allow(unused)]
pub(crate) fn contents(&self) -> &'a str { pub(crate) fn contents(&self) -> &'a str {
self.contents self.contents
} }
@ -35,7 +34,6 @@ impl<'a> PyFormatContext<'a> {
Locator::new(self.contents) Locator::new(self.contents)
} }
#[allow(unused)]
pub(crate) fn set_node_level(&mut self, level: NodeLevel) { pub(crate) fn set_node_level(&mut self, level: NodeLevel) {
self.node_level = level; self.node_level = level;
} }
@ -44,7 +42,6 @@ impl<'a> PyFormatContext<'a> {
self.node_level self.node_level
} }
#[allow(unused)]
pub(crate) fn comments(&self) -> &Comments<'a> { pub(crate) fn comments(&self) -> &Comments<'a> {
&self.comments &self.comments
} }
@ -80,11 +77,10 @@ pub(crate) enum NodeLevel {
#[default] #[default]
TopLevel, TopLevel,
/// Formatting nodes that are enclosed by a statement. /// Formatting the body statements of a [compound statement](https://docs.python.org/3/reference/compound_stmts.html#compound-statements)
#[allow(unused)] /// (`if`, `while`, `match`, etc.).
Statement, CompoundStatement,
/// Formatting nodes that are enclosed in a parenthesized expression. /// Formatting nodes that are enclosed in a parenthesized expression.
#[allow(unused)]
Parenthesized, Parenthesized,
} }

View file

@ -0,0 +1,53 @@
use crate::context::NodeLevel;
use crate::prelude::*;
use ruff_formatter::{format_args, write};
use rustpython_parser::ast::Expr;
/// Formats the passed expression. Adds parentheses if the expression doesn't fit on a line.
pub(crate) const fn maybe_parenthesize(expression: &Expr) -> MaybeParenthesize {
MaybeParenthesize { expression }
}
pub(crate) struct MaybeParenthesize<'a> {
expression: &'a Expr,
}
impl Format<PyFormatContext<'_>> for MaybeParenthesize<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
let saved_level = f.context().node_level();
f.context_mut().set_node_level(NodeLevel::Parenthesized);
let result = if needs_parentheses(self.expression) {
write!(
f,
[group(&format_args![
if_group_breaks(&text("(")),
soft_block_indent(&self.expression.format()),
if_group_breaks(&text(")"))
])]
)
} else {
// Don't add parentheses around expressions that have parentheses on their own (e.g. list, dict, tuple, call expression)
self.expression.format().fmt(f)
};
f.context_mut().set_node_level(saved_level);
result
}
}
const fn needs_parentheses(expr: &Expr) -> bool {
!matches!(
expr,
Expr::Tuple(_)
| Expr::List(_)
| Expr::Set(_)
| Expr::Dict(_)
| Expr::ListComp(_)
| Expr::SetComp(_)
| Expr::DictComp(_)
| Expr::GeneratorExp(_)
| Expr::Call(_)
)
}

View file

@ -29,6 +29,7 @@ pub(crate) mod expr_tuple;
pub(crate) mod expr_unary_op; pub(crate) mod expr_unary_op;
pub(crate) mod expr_yield; pub(crate) mod expr_yield;
pub(crate) mod expr_yield_from; pub(crate) mod expr_yield_from;
pub(crate) mod maybe_parenthesize;
#[derive(Default)] #[derive(Default)]
pub struct FormatExpr; pub struct FormatExpr;

View file

@ -14,7 +14,9 @@ use ruff_formatter::{
use ruff_python_ast::node::AstNode; use ruff_python_ast::node::AstNode;
use ruff_python_ast::source_code::{CommentRanges, CommentRangesBuilder, Locator}; use ruff_python_ast::source_code::{CommentRanges, CommentRangesBuilder, Locator};
use crate::comments::{dangling_comments, leading_comments, trailing_comments, Comments}; use crate::comments::{
dangling_node_comments, leading_node_comments, trailing_node_comments, Comments,
};
use crate::context::PyFormatContext; use crate::context::PyFormatContext;
pub(crate) mod builders; pub(crate) mod builders;
@ -64,7 +66,7 @@ where
/// You may want to override this method if you want to manually handle the formatting of comments /// You may want to override this method if you want to manually handle the formatting of comments
/// inside of the `fmt_fields` method or customize the formatting of the leading comments. /// inside of the `fmt_fields` method or customize the formatting of the leading comments.
fn fmt_leading_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> { fn fmt_leading_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> {
leading_comments(node).fmt(f) leading_node_comments(node).fmt(f)
} }
/// Formats the [dangling comments](comments#dangling-comments) of the node. /// Formats the [dangling comments](comments#dangling-comments) of the node.
@ -75,7 +77,7 @@ where
/// ///
/// A node can have dangling comments if all its children are tokens or if all node childrens are optional. /// A node can have dangling comments if all its children are tokens or if all node childrens are optional.
fn fmt_dangling_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> { fn fmt_dangling_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> {
dangling_comments(node).fmt(f) dangling_node_comments(node).fmt(f)
} }
/// Formats the [trailing comments](comments#trailing-comments) of the node. /// Formats the [trailing comments](comments#trailing-comments) of the node.
@ -83,7 +85,7 @@ where
/// You may want to override this method if you want to manually handle the formatting of comments /// You may want to override this method if you want to manually handle the formatting of comments
/// inside of the `fmt_fields` method or customize the formatting of the trailing comments. /// inside of the `fmt_fields` method or customize the formatting of the trailing comments.
fn fmt_trailing_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> { fn fmt_trailing_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> {
trailing_comments(node).fmt(f) trailing_node_comments(node).fmt(f)
} }
} }
@ -285,14 +287,58 @@ Formatted twice:
Ok(()) Ok(())
} }
#[fixture(pattern = "resources/test/fixtures/ruff/**/*.py")]
#[test]
fn ruff_test(input_path: &Path) -> Result<()> {
let content = fs::read_to_string(input_path)?;
let printed = format_module(&content)?;
let formatted_code = printed.as_code();
let reformatted =
format_module(formatted_code).expect("Expected formatted code to be valid syntax");
if reformatted.as_code() != formatted_code {
let diff = TextDiff::from_lines(formatted_code, reformatted.as_code())
.unified_diff()
.header("Formatted once", "Formatted twice")
.to_string();
panic!(
r#"Reformatting the formatted code a second time resulted in formatting changes.
{diff}
Formatted once:
{formatted_code}
Formatted twice:
{}"#,
reformatted.as_code()
);
}
let snapshot = format!(
r#"## Input
{}
## Output
{}"#,
CodeFrame::new("py", &content),
CodeFrame::new("py", formatted_code)
);
assert_snapshot!(snapshot);
Ok(())
}
/// Use this test to debug the formatting of some snipped /// Use this test to debug the formatting of some snipped
#[ignore] #[ignore]
#[test] #[test]
fn quick_test() { fn quick_test() {
let src = r#" let src = r#"
{ while True:
k: v for k, v in a_very_long_variable_name_that_exceeds_the_line_length_by_far_keep_going if something.changed:
} do.stuff() # trailing comment
other
"#; "#;
// Tokenize once // Tokenize once
let mut tokens = Vec::new(); let mut tokens = Vec::new();
@ -320,10 +366,10 @@ Formatted twice:
assert_eq!( assert_eq!(
printed.as_code(), printed.as_code(),
r#"{ r#"while True:
k: v if something.changed:
for k, v in a_very_long_variable_name_that_exceeds_the_line_length_by_far_keep_going do.stuff() # trailing comment
}"# "#
); );
} }

View file

@ -86,20 +86,20 @@ if __name__ == "__main__":
```diff ```diff
--- Black --- Black
+++ Ruff +++ Ruff
@@ -1,11 +1,6 @@ @@ -1,11 +1,9 @@
while True: while True:
if something.changed: if something.changed:
- do.stuff() # trailing comment - do.stuff() # trailing comment
- # Comment belongs to the `if` block. - # Comment belongs to the `if` block.
- # This one belongs to the `while` block.
-
- # Should this one, too? I guess so.
-
+ do.stuff() + do.stuff()
# This one belongs to the `while` block.
# Should this one, too? I guess so.
-
# This one is properly standalone now. # This one is properly standalone now.
for i in range(100): for i in range(100):
@@ -15,27 +10,18 @@ @@ -15,27 +13,18 @@
# then we do this # then we do this
print(i) print(i)
@ -127,7 +127,7 @@ if __name__ == "__main__":
# SECTION COMMENT # SECTION COMMENT
@@ -47,8 +33,6 @@ @@ -47,8 +36,6 @@
@deco3 @deco3
def decorated1(): def decorated1():
... ...
@ -136,7 +136,7 @@ if __name__ == "__main__":
# leading 1 # leading 1
@deco1 @deco1
# leading 2 # leading 2
@@ -56,18 +40,12 @@ @@ -56,18 +43,12 @@
# leading function comment # leading function comment
def decorated1(): def decorated1():
... ...
@ -163,6 +163,9 @@ if __name__ == "__main__":
while True: while True:
if something.changed: if something.changed:
do.stuff() do.stuff()
# This one belongs to the `while` block.
# Should this one, too? I guess so.
# This one is properly standalone now. # This one is properly standalone now.
for i in range(100): for i in range(100):

View file

@ -511,7 +511,7 @@ last_call()
Ø = set() Ø = set()
authors.łukasz.say_thanks() authors.łukasz.say_thanks()
mapping = { mapping = {
@@ -233,138 +170,83 @@ @@ -233,138 +170,84 @@
C: 0.1 * (10.0 / 12), C: 0.1 * (10.0 / 12),
D: 0.1 * (10.0 / 12), D: 0.1 * (10.0 / 12),
} }
@ -550,15 +550,6 @@ last_call()
- ... - ...
-for j in 1 + (2 + 3): -for j in 1 + (2 + 3):
- ... - ...
-while this and that:
- ...
-for (
- addr_family,
- addr_type,
- addr_proto,
- addr_canonname,
- addr_sockaddr,
-) in socket.getaddrinfo("google.com", "http"):
+print(* lambda x: x) +print(* lambda x: x)
+assert(not Test),("Short message") +assert(not Test),("Short message")
+assert this is ComplexTest and not requirements.fit_in_a_single_line(force=False), "Short message" +assert this is ComplexTest and not requirements.fit_in_a_single_line(force=False), "Short message"
@ -568,7 +559,15 @@ last_call()
+for z in (i for i in (1, 2, 3)): ... +for z in (i for i in (1, 2, 3)): ...
+for i in (call()): ... +for i in (call()): ...
+for j in (1 + (2 + 3)): ... +for j in (1 + (2 + 3)): ...
+while(this and that): ... while this and that:
...
-for (
- addr_family,
- addr_type,
- addr_proto,
- addr_canonname,
- addr_sockaddr,
-) in socket.getaddrinfo("google.com", "http"):
+for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'): +for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'):
pass pass
-a = ( -a = (
@ -879,7 +878,8 @@ for y in (): ...
for z in (i for i in (1, 2, 3)): ... for z in (i for i in (1, 2, 3)): ...
for i in (call()): ... for i in (call()): ...
for j in (1 + (2 + 3)): ... for j in (1 + (2 + 3)): ...
while(this and that): ... while this and that:
...
for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'): for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'):
pass pass
a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz

View file

@ -100,20 +100,24 @@ async def test_async_with():
# Make sure a leading comment is not removed. # Make sure a leading comment is not removed.
if unformatted_call( args ): # fmt: skip if unformatted_call( args ): # fmt: skip
print("First branch") print("First branch")
@@ -30,33 +23,21 @@ @@ -29,34 +22,22 @@
elif another_unformatted_call( args ): # fmt: skip
print("Second branch") print("Second branch")
else : # fmt: skip else : # fmt: skip
print("Last branch") - print("Last branch")
- -
- -
while some_condition( unformatted, args ): # fmt: skip -while some_condition( unformatted, args ): # fmt: skip
+ print("Last branch") # fmt: skip
+while some_condition( unformatted, args ): # fmt: skip
print("Do something") print("Do something")
- -
- -
for i in some_iter( unformatted, args ): # fmt: skip for i in some_iter( unformatted, args ): # fmt: skip
print("Do something") - print("Do something")
- -
- -
+ print("Do something") # fmt: skip
async def test_async_for(): async def test_async_for():
async for i in some_async_iter( unformatted, args ): # fmt: skip async for i in some_async_iter( unformatted, args ): # fmt: skip
print("Do something") print("Do something")
@ -128,9 +132,10 @@ async def test_async_with():
- -
- -
with give_me_context( unformatted, args ): # fmt: skip with give_me_context( unformatted, args ): # fmt: skip
print("Do something") - print("Do something")
- -
- -
+ print("Do something") # fmt: skip
async def test_async_with(): async def test_async_with():
async with give_me_async_context( unformatted, args ): # fmt: skip async with give_me_async_context( unformatted, args ): # fmt: skip
print("Do something") print("Do something")
@ -163,11 +168,11 @@ if unformatted_call( args ): # fmt: skip
elif another_unformatted_call( args ): # fmt: skip elif another_unformatted_call( args ): # fmt: skip
print("Second branch") print("Second branch")
else : # fmt: skip else : # fmt: skip
print("Last branch") print("Last branch") # fmt: skip
while some_condition( unformatted, args ): # fmt: skip while some_condition( unformatted, args ): # fmt: skip
print("Do something") print("Do something")
for i in some_iter( unformatted, args ): # fmt: skip for i in some_iter( unformatted, args ): # fmt: skip
print("Do something") print("Do something") # fmt: skip
async def test_async_for(): async def test_async_for():
async for i in some_async_iter( unformatted, args ): # fmt: skip async for i in some_async_iter( unformatted, args ): # fmt: skip
print("Do something") print("Do something")
@ -178,7 +183,7 @@ except UnformattedError as ex: # fmt: skip
finally : # fmt: skip finally : # fmt: skip
finally_call() finally_call()
with give_me_context( unformatted, args ): # fmt: skip with give_me_context( unformatted, args ): # fmt: skip
print("Do something") print("Do something") # fmt: skip
async def test_async_with(): async def test_async_with():
async with give_me_async_context( unformatted, args ): # fmt: skip async with give_me_async_context( unformatted, args ): # fmt: skip
print("Do something") print("Do something")

View file

@ -121,30 +121,30 @@ with open("/path/to/file.txt", mode="r") as read_file:
```diff ```diff
--- Black --- Black
+++ Ruff +++ Ruff
@@ -1,78 +1,74 @@ @@ -1,78 +1,68 @@
import random import random
- +def foo1():
-
def foo1():
- print("The newline above me should be deleted!")
-
+ print("The newline above me should be deleted!") + print("The newline above me should be deleted!")
def foo2(): +def foo2():
- print("All the newlines above me should be deleted!")
-def foo1():
- print("The newline above me should be deleted!")
+ -def foo2():
+ print("All the newlines above me should be deleted!") print("All the newlines above me should be deleted!")
-
-
def foo3(): def foo3():
+ +
print("No newline above me!") print("No newline above me!")
print("There is a newline above me, and that's OK!") print("There is a newline above me, and that's OK!")
+def foo4():
- -
-def foo4(): -
def foo4():
+
# There is a comment here # There is a comment here
print("The newline above me should not be deleted!") print("The newline above me should not be deleted!")
@ -154,23 +154,23 @@ with open("/path/to/file.txt", mode="r") as read_file:
def bar(self): def bar(self):
+ +
print("The newline above me should be deleted!") print("The newline above me should be deleted!")
- +for i in range(5):
-
for i in range(5):
- print(f"{i}) The line above me should be removed!")
-
+ print(f"{i}) The line above me should be removed!") + print(f"{i}) The line above me should be removed!")
+for i in range(5):
-for i in range(5):
- print(f"{i}) The line above me should be removed!")
+ print(f"{i}) The lines above me should be removed!")
for i in range(5): for i in range(5):
- print(f"{i}) The lines above me should be removed!") - print(f"{i}) The lines above me should be removed!")
+ for j in range(7):
+ -for i in range(5):
+ print(f"{i}) The lines above me should be removed!") - for j in range(7):
for i in range(5):
+
for j in range(7):
+
print(f"{i}) The lines above me should be removed!") print(f"{i}) The lines above me should be removed!")
+if random.randint(0, 3) == 0: +if random.randint(0, 3) == 0:
@ -189,38 +189,33 @@ with open("/path/to/file.txt", mode="r") as read_file:
if random.uniform(0, 1) > 0.5: if random.uniform(0, 1) > 0.5:
print("Two lines above me are about to be removed!") print("Two lines above me are about to be removed!")
- -
+while True: -
while True:
+ print("The newline above me should be deleted!") print("The newline above me should be deleted!")
-
-
while True: while True:
- print("The newline above me should be deleted!")
-while True:
+
print("The newlines above me should be deleted!") print("The newlines above me should be deleted!")
+while True:
- -
-while True: -
while True:
while False: while False:
- print("The newlines above me should be deleted!") print("The newlines above me should be deleted!")
- +with open("/path/to/file.txt", mode="w") as file:
+ print("The newlines above me should be deleted!")
with open("/path/to/file.txt", mode="w") as file:
- file.write("The new line above me is about to be removed!")
- -
+ file.write("The new line above me is about to be removed!") + file.write("The new line above me is about to be removed!")
with open("/path/to/file.txt", mode="w") as file: with open("/path/to/file.txt", mode="w") as file:
- file.write("The new lines above me is about to be removed!") - file.write("The new line above me is about to be removed!")
-with open("/path/to/file.txt", mode="w") as file:
+ +
+ file.write("The new lines above me is about to be removed!") file.write("The new lines above me is about to be removed!")
with open("/path/to/file.txt", mode="r") as read_file: +with open("/path/to/file.txt", mode="r") as read_file:
+
-
-with open("/path/to/file.txt", mode="r") as read_file:
with open("/path/to/output_file.txt", mode="w") as write_file: with open("/path/to/output_file.txt", mode="w") as write_file:
+ +
write_file.writelines(read_file.readlines()) write_file.writelines(read_file.readlines())
@ -278,17 +273,11 @@ if random.randint(0, 3) == 0:
if random.uniform(0, 1) > 0.5: if random.uniform(0, 1) > 0.5:
print("Two lines above me are about to be removed!") print("Two lines above me are about to be removed!")
while True: while True:
print("The newline above me should be deleted!") print("The newline above me should be deleted!")
while True: while True:
print("The newlines above me should be deleted!") print("The newlines above me should be deleted!")
while True: while True:
while False: while False:
print("The newlines above me should be deleted!") print("The newlines above me should be deleted!")
with open("/path/to/file.txt", mode="w") as file: with open("/path/to/file.txt", mode="w") as file:

View file

@ -0,0 +1,70 @@
---
source: crates/ruff_python_formatter/src/lib.rs
expression: snapshot
---
## Input
```py
while 34: # trailing test comment
pass # trailing last statement comment
# trailing while body comment
# leading else comment
else: # trailing else comment
pass
# trailing else body comment
while aVeryLongConditionThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn: # trailing comment
pass
else:
...
while (
some_condition(unformatted, args) and anotherCondition or aThirdCondition
): # comment
print("Do something")
while (
some_condition(unformatted, args) # trailing some condition
and anotherCondition or aThirdCondition # trailing third condition
): # comment
print("Do something")
```
## Output
```py
while 34: # trailing test comment
pass # trailing last statement comment
# trailing while body comment
# leading else comment
else: # trailing else comment
pass
# trailing else body comment
while (
aVeryLongConditionThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn
): # trailing comment
pass
else:
...
while some_condition(unformatted, args) and anotherCondition or aThirdCondition: # comment
print("Do something")
while (
some_condition(unformatted, args) # trailing some condition
and anotherCondition or aThirdCondition # trailing third condition
): # comment
print("Do something")
```

View file

@ -1,12 +1,68 @@
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::comments::{leading_alternate_branch_comments, trailing_comments};
use ruff_formatter::{write, Buffer, FormatResult}; use crate::expression::maybe_parenthesize::maybe_parenthesize;
use rustpython_parser::ast::StmtWhile; use crate::prelude::*;
use crate::FormatNodeRule;
use ruff_formatter::write;
use ruff_python_ast::node::AstNode;
use rustpython_parser::ast::{Ranged, Stmt, StmtWhile};
#[derive(Default)] #[derive(Default)]
pub struct FormatStmtWhile; pub struct FormatStmtWhile;
impl FormatNodeRule<StmtWhile> for FormatStmtWhile { impl FormatNodeRule<StmtWhile> for FormatStmtWhile {
fn fmt_fields(&self, item: &StmtWhile, f: &mut PyFormatter) -> FormatResult<()> { fn fmt_fields(&self, item: &StmtWhile, f: &mut PyFormatter) -> FormatResult<()> {
write!(f, [verbatim_text(item.range)]) let StmtWhile {
range: _,
test,
body,
orelse,
} = item;
let comments = f.context().comments().clone();
let dangling_comments = comments.dangling_comments(item.as_any_node_ref());
let body_start = body.first().map_or(test.end(), Stmt::start);
let or_else_comments_start =
dangling_comments.partition_point(|comment| comment.slice().end() < body_start);
let (trailing_condition_comments, or_else_comments) =
dangling_comments.split_at(or_else_comments_start);
write!(
f,
[
text("while"),
space(),
maybe_parenthesize(test),
text(":"),
trailing_comments(trailing_condition_comments),
block_indent(&body.format())
]
)?;
if !orelse.is_empty() {
// Split between leading comments before the `else` keyword and end of line comments at the end of
// the `else:` line.
let trailing_start =
or_else_comments.partition_point(|comment| comment.position().is_own_line());
let (leading, trailing) = or_else_comments.split_at(trailing_start);
write!(
f,
[
leading_alternate_branch_comments(leading, body.last()),
text("else:"),
trailing_comments(trailing),
block_indent(&orelse.format())
]
)?;
}
Ok(())
}
fn fmt_dangling_comments(&self, _node: &StmtWhile, _f: &mut PyFormatter) -> FormatResult<()> {
// Handled in `fmt_fields`
Ok(())
} }
} }

View file

@ -28,10 +28,15 @@ impl Default for FormatSuite {
impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite { impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
fn fmt(&self, statements: &Suite, f: &mut PyFormatter) -> FormatResult<()> { fn fmt(&self, statements: &Suite, f: &mut PyFormatter) -> FormatResult<()> {
let mut joiner = f.join_nodes(match self.level { let node_level = match self.level {
SuiteLevel::TopLevel => NodeLevel::TopLevel, SuiteLevel::TopLevel => NodeLevel::TopLevel,
SuiteLevel::Nested => NodeLevel::Statement, SuiteLevel::Nested => NodeLevel::CompoundStatement,
}); };
let saved_level = f.context().node_level();
f.context_mut().set_node_level(node_level);
let mut joiner = f.join_nodes(node_level);
let mut iter = statements.iter(); let mut iter = statements.iter();
let Some(first) = iter.next() else { let Some(first) = iter.next() else {
@ -67,7 +72,11 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
is_last_function_or_class_definition = is_current_function_or_class_definition; is_last_function_or_class_definition = is_current_function_or_class_definition;
} }
joiner.finish() let result = joiner.finish();
f.context_mut().set_node_level(saved_level);
result
} }
} }

View file

@ -41,7 +41,6 @@ pub(crate) fn find_first_non_trivia_character_in_range(
} }
/// Returns the number of newlines between `offset` and the first non whitespace character in the source code. /// Returns the number of newlines between `offset` and the first non whitespace character in the source code.
#[allow(unused)] // TODO(micha) Remove after using for statements.
pub(crate) fn lines_before(code: &str, offset: TextSize) -> u32 { pub(crate) fn lines_before(code: &str, offset: TextSize) -> u32 {
let head = &code[TextRange::up_to(offset)]; let head = &code[TextRange::up_to(offset)];
let mut newlines = 0u32; let mut newlines = 0u32;