diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/match.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/match.py new file mode 100644 index 0000000000..0ff31d4383 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/match.py @@ -0,0 +1,73 @@ +def http_error(status): + match status : # fmt: skip + case 400 : # fmt: skip + return "Bad request" + case 404: + return "Not found" + case 418: + return "I'm a teapot" + case _: + return "Something's wrong with the internet" + +# point is an (x, y) tuple +match point: + case (0, 0): # fmt: skip + print("Origin") + case (0, y): + print(f"Y={y}") + case (x, 0): + print(f"X={x}") + case (x, y): + print(f"X={x}, Y={y}") + case _: + raise ValueError("Not a point") + +class Point: + x: int + y: int + +def location(point): + match point: + case Point(x=0, y =0 ) : # fmt: skip + print("Origin is the point's location.") + case Point(x=0, y=y): + print(f"Y={y} and the point is on the y-axis.") + case Point(x=x, y=0): + print(f"X={x} and the point is on the x-axis.") + case Point(): + print("The point is located somewhere else on the plane.") + case _: + print("Not a point") + + +match points: + case []: + print("No points in the list.") + case [ + Point(0, 0) + ]: # fmt: skip + print("The origin is the only point in the list.") + case [Point(x, y)]: + print(f"A single point {x}, {y} is in the list.") + case [Point(0, y1), Point(0, y2)]: + print(f"Two points on the Y axis at {y1}, {y2} are in the list.") + case _: + print("Something else is found in the list.") + + +match test_variable: + case ( + 'warning', + code, + 40 + ): # fmt: skip + print("A warning has been received.") + case ('error', code, _): + print(f"An error {code} occurred.") + + +match point: + case Point(x, y) if x == y: # fmt: skip + print(f"The point is located on the diagonal Y=X at {x}.") + case Point(x, y): + print(f"Point is not on the diagonal.") diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/or_else.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/or_else.py new file mode 100644 index 0000000000..c2dccee81e --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/or_else.py @@ -0,0 +1,31 @@ +for item in container: + if search_something(item): + # Found it! + process(item) + break + # leading comment +else : #fmt: skip + # Didn't find anything.. + not_found_in_container() + + +while i < 10: + print(i) + +# leading comment +else : #fmt: skip + # Didn't find anything.. + print("I was already larger than 9") + + +try : # fmt: skip + some_call() +except Exception : # fmt: skip + pass +except : # fmt: skip + handle_exception() + +else : # fmt: skip + pass +finally : # fmt: skip + finally_call() diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/parentheses.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/parentheses.py new file mode 100644 index 0000000000..7533f56a1e --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/parentheses.py @@ -0,0 +1,20 @@ +if ( + # a leading condition comment + len([1, 23, 3, 4, 5]) > 2 # trailing condition comment + # trailing own line comment +): # fmt: skip + pass + + +if ( # trailing open parentheses comment + # a leading condition comment + len([1, 23, 3, 4, 5]) > 2 +) and ((((y)))): # fmt: skip + pass + + +if ( # trailing open parentheses comment + # a leading condition comment + len([1, 23, 3, 4, 5]) > 2 +) and y: # fmt: skip + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/type_params.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/type_params.py new file mode 100644 index 0000000000..1cbb4f1549 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/type_params.py @@ -0,0 +1,28 @@ +class TestTypeParam[ T ]: # fmt: skip + pass + +class TestTypeParam [ # trailing open paren comment + # leading comment + T # trailing type param comment + # trailing type param own line comment +]: # fmt: skip + pass + +class TestTrailingComment4[ + T +] ( # trailing arguments open parenthesis comment + # leading argument comment + A # trailing argument comment + # trailing argument own line comment +): # fmt: skip + pass + +def test [ + # comment + A, + + # another + + B, +] (): # fmt: skip + ... diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index 04d4c5da7b..3f27a142e2 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -400,7 +400,6 @@ impl<'a> Comments<'a> { } /// Returns an iterator over the [leading](self#leading-comments) and [trailing comments](self#trailing-comments) of `node`. - #[allow(unused)] pub(crate) fn leading_trailing_comments( &self, node: T, diff --git a/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs index 2f3524ba23..15e1ef6205 100644 --- a/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs +++ b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs @@ -1,12 +1,14 @@ -use crate::comments::{trailing_comments, SourceComment}; -use crate::expression::maybe_parenthesize_expression; -use crate::expression::parentheses::Parenthesize; -use crate::prelude::*; -use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::FormatRuleWithOptions; use ruff_formatter::{write, Buffer, FormatResult}; use ruff_python_ast::ExceptHandlerExceptHandler; +use crate::comments::SourceComment; +use crate::expression::maybe_parenthesize_expression; +use crate::expression::parentheses::Parenthesize; +use crate::prelude::*; +use crate::statement::clause::{clause_header, ClauseHeader}; +use crate::{FormatNodeRule, PyFormatter}; + #[derive(Copy, Clone, Default)] pub enum ExceptHandlerKind { #[default] @@ -49,32 +51,42 @@ impl FormatNodeRule for FormatExceptHandlerExceptHan write!( f, [ - text("except"), - match self.except_handler_kind { - ExceptHandlerKind::Regular => None, - ExceptHandlerKind::Starred => Some(text("*")), - } - ] - )?; + clause_header( + ClauseHeader::ExceptHandler(item), + dangling_comments, + &format_with(|f| { + write!( + f, + [ + text("except"), + match self.except_handler_kind { + ExceptHandlerKind::Regular => None, + ExceptHandlerKind::Starred => Some(text("*")), + } + ] + )?; - if let Some(type_) = type_ { - write!( - f, - [ - space(), - maybe_parenthesize_expression(type_, item, Parenthesize::IfBreaks) - ] - )?; - if let Some(name) = name { - write!(f, [space(), text("as"), space(), name.format()])?; - } - } - write!( - f, - [ - text(":"), - trailing_comments(dangling_comments), - block_indent(&body.format()), + if let Some(type_) = type_ { + write!( + f, + [ + space(), + maybe_parenthesize_expression( + type_, + item, + Parenthesize::IfBreaks + ) + ] + )?; + if let Some(name) = name { + write!(f, [space(), text("as"), space(), name.format()])?; + } + } + + Ok(()) + }), + ), + block_indent(&body.format()) ] ) } diff --git a/crates/ruff_python_formatter/src/other/match_case.rs b/crates/ruff_python_formatter/src/other/match_case.rs index 1f60c163f0..8d9d51550d 100644 --- a/crates/ruff_python_formatter/src/other/match_case.rs +++ b/crates/ruff_python_formatter/src/other/match_case.rs @@ -3,9 +3,10 @@ use ruff_python_ast::{MatchCase, Pattern, Ranged}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::TextRange; -use crate::comments::{leading_comments, trailing_comments, SourceComment}; +use crate::comments::{leading_comments, SourceComment}; use crate::expression::parentheses::parenthesized; use crate::prelude::*; +use crate::statement::clause::{clause_header, ClauseHeader}; use crate::{FormatError, FormatNodeRule, PyFormatter}; #[derive(Default)] @@ -23,30 +24,39 @@ impl FormatNodeRule for FormatMatchCase { let comments = f.context().comments().clone(); let dangling_item_comments = comments.dangling_comments(item); - write!(f, [text("case"), space()])?; - let leading_pattern_comments = comments.leading_comments(pattern); - if !leading_pattern_comments.is_empty() { - parenthesized( - "(", - &format_args![leading_comments(leading_pattern_comments), pattern.format()], - ")", - ) - .fmt(f)?; - } else if is_match_case_pattern_parenthesized(item, pattern, f.context())? { - parenthesized("(", &pattern.format(), ")").fmt(f)?; - } else { - pattern.format().fmt(f)?; - } - - if let Some(guard) = guard { - write!(f, [space(), text("if"), space(), guard.format()])?; - } - write!( f, [ - text(":"), - trailing_comments(dangling_item_comments), + clause_header( + ClauseHeader::MatchCase(item), + dangling_item_comments, + &format_with(|f| { + write!(f, [text("case"), space()])?; + + let leading_pattern_comments = comments.leading_comments(pattern); + if !leading_pattern_comments.is_empty() { + parenthesized( + "(", + &format_args![ + leading_comments(leading_pattern_comments), + pattern.format() + ], + ")", + ) + .fmt(f)?; + } else if is_match_case_pattern_parenthesized(item, pattern, f.context())? { + parenthesized("(", &pattern.format(), ")").fmt(f)?; + } else { + pattern.format().fmt(f)?; + } + + if let Some(guard) = guard { + write!(f, [space(), text("if"), space(), guard.format()])?; + } + + Ok(()) + }), + ), block_indent(&body.format()) ] ) diff --git a/crates/ruff_python_formatter/src/statement/clause.rs b/crates/ruff_python_formatter/src/statement/clause.rs new file mode 100644 index 0000000000..e52a342e22 --- /dev/null +++ b/crates/ruff_python_formatter/src/statement/clause.rs @@ -0,0 +1,407 @@ +use crate::comments::{ + leading_alternate_branch_comments, trailing_comments, SourceComment, SuppressionKind, +}; +use crate::prelude::*; +use crate::verbatim::write_suppressed_clause_header; +use ruff_formatter::{Argument, Arguments, FormatError}; +use ruff_python_ast::node::AnyNodeRef; +use ruff_python_ast::{ + ElifElseClause, ExceptHandlerExceptHandler, MatchCase, Ranged, StmtClassDef, StmtFor, + StmtFunctionDef, StmtIf, StmtMatch, StmtTry, StmtWhile, StmtWith, +}; +use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer}; +use ruff_text_size::{TextRange, TextSize}; + +/// The header of a compound statement clause. +/// +/// > A compound statement consists of one or more ‘clauses.’ A clause consists of a header and a ‘suite.’ +/// > The clause headers of a particular compound statement are all at the same indentation level. +/// > Each clause header begins with a uniquely identifying keyword and ends with a colon. +/// [source](https://docs.python.org/3/reference/compound_stmts.html#compound-statements) +#[derive(Copy, Clone)] +pub(crate) enum ClauseHeader<'a> { + Class(&'a StmtClassDef), + Function(&'a StmtFunctionDef), + If(&'a StmtIf), + ElifElse(&'a ElifElseClause), + Try(&'a StmtTry), + ExceptHandler(&'a ExceptHandlerExceptHandler), + TryFinally(&'a StmtTry), + Match(&'a StmtMatch), + MatchCase(&'a MatchCase), + For(&'a StmtFor), + While(&'a StmtWhile), + With(&'a StmtWith), + OrElse(ElseClause<'a>), +} + +impl<'a> ClauseHeader<'a> { + /// The range from the clause keyword up to and including the final colon. + pub(crate) fn range(self, source: &str) -> FormatResult { + let keyword_range = self.first_keyword_range(source)?; + + let mut last_child_end = None; + + self.visit(&mut |child| last_child_end = Some(child.end())); + + let end = match self { + ClauseHeader::Class(class) => Some(last_child_end.unwrap_or(class.name.end())), + ClauseHeader::Function(function) => Some(last_child_end.unwrap_or(function.name.end())), + ClauseHeader::ElifElse(_) + | ClauseHeader::Try(_) + | ClauseHeader::If(_) + | ClauseHeader::TryFinally(_) + | ClauseHeader::Match(_) + | ClauseHeader::MatchCase(_) + | ClauseHeader::For(_) + | ClauseHeader::While(_) + | ClauseHeader::With(_) + | ClauseHeader::OrElse(_) => last_child_end, + + ClauseHeader::ExceptHandler(handler) => handler + .name + .as_ref() + .map(ruff_python_ast::Ranged::end) + .or(last_child_end), + }; + + let colon = colon_range(end.unwrap_or(keyword_range.end()), source)?; + + Ok(TextRange::new(keyword_range.start(), colon.end())) + } + + /// Visits the nodes in the case header. + pub(crate) fn visit(self, visitor: &mut F) + where + F: FnMut(AnyNodeRef), + { + fn visit<'a, N, F>(node: N, visitor: &mut F) + where + N: Into>, + F: FnMut(AnyNodeRef<'a>), + { + visitor(node.into()); + } + + match self { + ClauseHeader::Class(StmtClassDef { + type_params, + arguments, + range: _, + decorator_list: _, + name: _, + body: _, + }) => { + if let Some(type_params) = type_params.as_deref() { + visit(type_params, visitor); + } + + if let Some(arguments) = arguments { + visit(arguments.as_ref(), visitor); + } + } + ClauseHeader::Function(StmtFunctionDef { + type_params, + parameters, + range: _, + is_async: _, + decorator_list: _, + name: _, + returns: _, + body: _, + }) => { + if let Some(type_params) = type_params.as_ref() { + visit(type_params, visitor); + } + visit(parameters.as_ref(), visitor); + } + ClauseHeader::If(StmtIf { + test, + range: _, + body: _, + elif_else_clauses: _, + }) => { + visit(test.as_ref(), visitor); + } + ClauseHeader::ElifElse(ElifElseClause { + test, + range: _, + body: _, + }) => { + if let Some(test) = test.as_ref() { + visit(test, visitor); + } + } + + ClauseHeader::ExceptHandler(ExceptHandlerExceptHandler { + type_: type_expr, + range: _, + name: _, + body: _, + }) => { + if let Some(type_expr) = type_expr.as_deref() { + visit(type_expr, visitor); + } + } + ClauseHeader::Match(StmtMatch { + subject, + range: _, + cases: _, + }) => { + visit(subject.as_ref(), visitor); + } + ClauseHeader::MatchCase(MatchCase { + guard, + pattern, + range: _, + body: _, + }) => { + visit(pattern, visitor); + + if let Some(guard) = guard.as_deref() { + visit(guard, visitor); + } + } + ClauseHeader::For(StmtFor { + target, + iter, + range: _, + is_async: _, + body: _, + orelse: _, + }) => { + visit(target.as_ref(), visitor); + visit(iter.as_ref(), visitor); + } + ClauseHeader::While(StmtWhile { + test, + range: _, + body: _, + orelse: _, + }) => { + visit(test.as_ref(), visitor); + } + ClauseHeader::With(StmtWith { + items, + range: _, + is_async: _, + body: _, + }) => { + for item in items { + visit(item, visitor); + } + } + ClauseHeader::Try(_) | ClauseHeader::TryFinally(_) | ClauseHeader::OrElse(_) => {} + } + } + + /// Returns the range of the first keyword that marks the start of the clause header. + fn first_keyword_range(self, source: &str) -> FormatResult { + match self { + ClauseHeader::Class(header) => { + find_keyword(header.start(), SimpleTokenKind::Class, source) + } + ClauseHeader::Function(header) => { + let keyword = if header.is_async { + SimpleTokenKind::Async + } else { + SimpleTokenKind::Def + }; + find_keyword(header.start(), keyword, source) + } + ClauseHeader::If(header) => find_keyword(header.start(), SimpleTokenKind::If, source), + ClauseHeader::ElifElse(ElifElseClause { + test: None, range, .. + }) => find_keyword(range.start(), SimpleTokenKind::Else, source), + ClauseHeader::ElifElse(ElifElseClause { + test: Some(_), + range, + .. + }) => find_keyword(range.start(), SimpleTokenKind::Elif, source), + ClauseHeader::Try(header) => find_keyword(header.start(), SimpleTokenKind::Try, source), + ClauseHeader::ExceptHandler(header) => { + find_keyword(header.start(), SimpleTokenKind::Except, source) + } + ClauseHeader::TryFinally(header) => { + let last_statement = header + .orelse + .last() + .map(AnyNodeRef::from) + .or_else(|| header.handlers.last().map(AnyNodeRef::from)) + .or_else(|| header.body.last().map(AnyNodeRef::from)) + .unwrap(); + + find_keyword(last_statement.end(), SimpleTokenKind::Finally, source) + } + ClauseHeader::Match(header) => { + find_keyword(header.start(), SimpleTokenKind::Match, source) + } + ClauseHeader::MatchCase(header) => { + find_keyword(header.start(), SimpleTokenKind::Case, source) + } + ClauseHeader::For(header) => { + let keyword = if header.is_async { + SimpleTokenKind::Async + } else { + SimpleTokenKind::For + }; + find_keyword(header.start(), keyword, source) + } + ClauseHeader::While(header) => { + find_keyword(header.start(), SimpleTokenKind::While, source) + } + ClauseHeader::With(header) => { + let keyword = if header.is_async { + SimpleTokenKind::Async + } else { + SimpleTokenKind::With + }; + + find_keyword(header.start(), keyword, source) + } + ClauseHeader::OrElse(header) => match header { + ElseClause::Try(try_stmt) => { + let last_statement = try_stmt + .handlers + .last() + .map(AnyNodeRef::from) + .or_else(|| try_stmt.body.last().map(AnyNodeRef::from)) + .unwrap(); + + find_keyword(last_statement.end(), SimpleTokenKind::Else, source) + } + ElseClause::For(StmtFor { body, .. }) + | ElseClause::While(StmtWhile { body, .. }) => { + find_keyword(body.last().unwrap().end(), SimpleTokenKind::Else, source) + } + }, + } + } +} + +#[derive(Copy, Clone)] +pub(crate) enum ElseClause<'a> { + Try(&'a StmtTry), + For(&'a StmtFor), + While(&'a StmtWhile), +} + +pub(crate) struct FormatClauseHeader<'a, 'ast> { + header: ClauseHeader<'a>, + /// How to format the clause header + formatter: Argument<'a, PyFormatContext<'ast>>, + + /// Leading comments coming before the branch, together with the previous node, if any. Only relevant + /// for alternate branches. + leading_comments: Option<(&'a [SourceComment], Option>)>, + + /// The trailing comments coming after the colon. + trailing_colon_comment: &'a [SourceComment], +} + +/// Formats a clause header, handling the case where the clause header is suppressed and should not be formatted. +/// +/// Calls the `formatter` to format the content of the `header`, except if the `trailing_colon_comment` is a `fmt: skip` suppression comment. +/// Takes care of formatting the `trailing_colon_comment` and adds the `:` at the end of the header. +pub(crate) fn clause_header<'a, 'ast, Content>( + header: ClauseHeader<'a>, + trailing_colon_comment: &'a [SourceComment], + formatter: &'a Content, +) -> FormatClauseHeader<'a, 'ast> +where + Content: Format>, +{ + FormatClauseHeader { + header, + formatter: Argument::new(formatter), + leading_comments: None, + trailing_colon_comment, + } +} + +impl<'a> FormatClauseHeader<'a, '_> { + /// Sets the leading comments that precede an alternate branch. + #[must_use] + pub(crate) fn with_leading_comments( + mut self, + comments: &'a [SourceComment], + last_node: Option, + ) -> Self + where + N: Into>, + { + self.leading_comments = Some((comments, last_node.map(Into::into))); + self + } +} + +impl<'ast> Format> for FormatClauseHeader<'_, 'ast> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + if let Some((leading_comments, last_node)) = self.leading_comments { + leading_alternate_branch_comments(leading_comments, last_node).fmt(f)?; + } + + if SuppressionKind::has_skip_comment(self.trailing_colon_comment, f.context().source()) { + write_suppressed_clause_header(self.header, f)?; + } else { + f.write_fmt(Arguments::from(&self.formatter))?; + text(":").fmt(f)?; + } + + trailing_comments(self.trailing_colon_comment).fmt(f) + } +} + +/// Finds the range of `keyword` starting the search at `start_position`. Expects only comments and `(` between +/// the `start_position` and the `keyword` token. +fn find_keyword( + start_position: TextSize, + keyword: SimpleTokenKind, + source: &str, +) -> FormatResult { + let mut tokenizer = SimpleTokenizer::starts_at(start_position, source).skip_trivia(); + + match tokenizer.next() { + Some(token) if token.kind() == keyword => Ok(token.range()), + Some(other) => { + debug_assert!( + false, + "Expected the keyword token {keyword:?} but found the token {other:?} instead." + ); + Err(FormatError::syntax_error( + "Expected the keyword token but found another token instead.", + )) + } + None => { + debug_assert!( + false, + "Expected the keyword token {keyword:?} but reached the end of the source instead." + ); + Err(FormatError::syntax_error( + "Expected the case header keyword token but reached the end of the source instead.", + )) + } + } +} + +/// Returns the range of the `:` ending the clause header or `Err` if the colon can't be found. +fn colon_range(after_keyword_or_condition: TextSize, source: &str) -> FormatResult { + let mut tokenizer = SimpleTokenizer::starts_at(after_keyword_or_condition, source) + .skip_trivia() + .skip_while(|token| token.kind() == SimpleTokenKind::RParen); + + match tokenizer.next() { + Some(SimpleToken { + kind: SimpleTokenKind::Colon, + range, + }) => Ok(range), + Some(token) => { + debug_assert!(false, "Expected the colon marking the end of the case header but found {token:?} instead."); + Err(FormatError::syntax_error("Expected colon marking the end of the case header but found another token instead.")) + } + None => { + debug_assert!(false, "Expected the colon marking the end of the case header but found the end of the range."); + Err(FormatError::syntax_error("Expected the colon marking the end of the case header but found the end of the range.")) + } + } +} diff --git a/crates/ruff_python_formatter/src/statement/mod.rs b/crates/ruff_python_formatter/src/statement/mod.rs index 2eed3ce744..7bc0a4c27f 100644 --- a/crates/ruff_python_formatter/src/statement/mod.rs +++ b/crates/ruff_python_formatter/src/statement/mod.rs @@ -3,6 +3,7 @@ use ruff_python_ast::Stmt; use crate::prelude::*; +pub(super) mod clause; pub(crate) mod stmt_ann_assign; pub(crate) mod stmt_assert; pub(crate) mod stmt_assign; diff --git a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs index 3b4a1f2ac0..2957950506 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -5,6 +5,8 @@ use ruff_python_trivia::lines_after_ignoring_trivia; use crate::comments::{leading_comments, trailing_comments, SourceComment}; use crate::prelude::*; use crate::statement::suite::SuiteKind; + +use crate::statement::clause::{clause_header, ClauseHeader}; use crate::FormatNodeRule; #[derive(Default)] @@ -30,78 +32,81 @@ impl FormatNodeRule for FormatStmtClassDef { let (leading_definition_comments, trailing_definition_comments) = dangling_comments.split_at(trailing_definition_comments_start); - FormatDecorators { - decorators: decorator_list, - leading_definition_comments, - } - .fmt(f)?; - - write!(f, [text("class"), space(), name.format()])?; - - if let Some(type_params) = type_params.as_deref() { - write!(f, [type_params.format()])?; - } - - if let Some(arguments) = arguments.as_deref() { - // Drop empty the arguments node entirely (i.e., remove the parentheses) if it is empty, - // e.g., given: - // ```python - // class A(): - // ... - // ``` - // - // Format as: - // ```python - // class A: - // ... - // ``` - // - // However, preserve any dangling end-of-line comments, e.g., given: - // ```python - // class A( # comment - // ): - // ... - // - // Format as: - // ```python - // class A: # comment - // ... - // ``` - // - // However, the arguments contain any dangling own-line comments, we retain the - // parentheses, e.g., given: - // ```python - // class A( # comment - // # comment - // ): - // ... - // ``` - // - // Format as: - // ```python - // class A( # comment - // # comment - // ): - // ... - // ``` - if arguments.is_empty() - && comments - .dangling_comments(arguments) - .iter() - .all(|comment| comment.line_position().is_end_of_line()) - { - let dangling = comments.dangling_comments(arguments); - write!(f, [trailing_comments(dangling)])?; - } else { - write!(f, [arguments.format()])?; - } - } - write!( f, [ - text(":"), - trailing_comments(trailing_definition_comments), + FormatDecorators { + decorators: decorator_list, + leading_definition_comments, + }, + clause_header( + ClauseHeader::Class(item), + trailing_definition_comments, + &format_with(|f| { + write!(f, [text("class"), space(), name.format()])?; + + if let Some(type_params) = type_params.as_deref() { + write!(f, [type_params.format()])?; + } + + if let Some(arguments) = arguments.as_deref() { + // Drop empty the arguments node entirely (i.e., remove the parentheses) if it is empty, + // e.g., given: + // ```python + // class A(): + // ... + // ``` + // + // Format as: + // ```python + // class A: + // ... + // ``` + // + // However, preserve any dangling end-of-line comments, e.g., given: + // ```python + // class A( # comment + // ): + // ... + // + // Format as: + // ```python + // class A: # comment + // ... + // ``` + // + // However, the arguments contain any dangling own-line comments, we retain the + // parentheses, e.g., given: + // ```python + // class A( # comment + // # comment + // ): + // ... + // ``` + // + // Format as: + // ```python + // class A( # comment + // # comment + // ): + // ... + // ``` + if arguments.is_empty() + && comments + .dangling_comments(arguments) + .iter() + .all(|comment| comment.line_position().is_end_of_line()) + { + let dangling = comments.dangling_comments(arguments); + write!(f, [trailing_comments(dangling)])?; + } else { + write!(f, [arguments.format()])?; + } + } + + Ok(()) + }), + ), block_indent(&body.format().with_options(SuiteKind::Class)) ] ) diff --git a/crates/ruff_python_formatter/src/statement/stmt_for.rs b/crates/ruff_python_formatter/src/statement/stmt_for.rs index 22bc0017cf..779f315025 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_for.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_for.rs @@ -1,11 +1,12 @@ use ruff_formatter::{format_args, write}; use ruff_python_ast::{Expr, Ranged, Stmt, StmtFor}; -use crate::comments::{leading_alternate_branch_comments, trailing_comments, SourceComment}; +use crate::comments::SourceComment; use crate::expression::expr_tuple::TupleParentheses; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; +use crate::statement::clause::{clause_header, ClauseHeader, ElseClause}; use crate::FormatNodeRule; #[derive(Debug)] @@ -49,16 +50,20 @@ impl FormatNodeRule for FormatStmtFor { write!( f, [ - is_async.then_some(format_args![text("async"), space()]), - text("for"), - space(), - ExprTupleWithoutParentheses(target), - space(), - text("in"), - space(), - maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaks), - text(":"), - trailing_comments(trailing_condition_comments), + clause_header( + ClauseHeader::For(item), + trailing_condition_comments, + &format_args![ + is_async.then_some(format_args![text("async"), space()]), + text("for"), + space(), + ExprTupleWithoutParentheses(target), + space(), + text("in"), + space(), + maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaks), + ], + ), block_indent(&body.format()) ] )?; @@ -75,9 +80,12 @@ impl FormatNodeRule for FormatStmtFor { write!( f, [ - leading_alternate_branch_comments(leading, body.last()), - text("else:"), - trailing_comments(trailing), + clause_header( + ClauseHeader::OrElse(ElseClause::For(item)), + trailing, + &text("else"), + ) + .with_leading_comments(leading, body.last()), block_indent(&orelse.format()) ] )?; diff --git a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs index 5897c4cab0..771ef48721 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -2,10 +2,11 @@ use ruff_formatter::write; use ruff_python_ast::{Parameters, Ranged, StmtFunctionDef}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; -use crate::comments::{trailing_comments, SourceComment}; +use crate::comments::SourceComment; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::{Parentheses, Parenthesize}; use crate::prelude::*; +use crate::statement::clause::{clause_header, ClauseHeader}; use crate::statement::stmt_class_def::FormatDecorators; use crate::statement::suite::SuiteKind; use crate::FormatNodeRule; @@ -35,105 +36,114 @@ impl FormatNodeRule for FormatStmtFunctionDef { let (leading_definition_comments, trailing_definition_comments) = dangling_comments.split_at(trailing_definition_comments_start); - FormatDecorators { - decorators: decorator_list, - leading_definition_comments, - } - .fmt(f)?; - - if *is_async { - write!(f, [text("async"), space()])?; - } - - write!(f, [text("def"), space(), name.format()])?; - - if let Some(type_params) = type_params.as_ref() { - write!(f, [type_params.format()])?; - } - - let format_inner = format_with(|f: &mut PyFormatter| { - write!(f, [parameters.format()])?; - - if let Some(return_annotation) = returns.as_ref() { - write!(f, [space(), text("->"), space()])?; - - if return_annotation.is_tuple_expr() { - let parentheses = if comments.has_leading_comments(return_annotation.as_ref()) { - Parentheses::Always - } else { - Parentheses::Never - }; - write!(f, [return_annotation.format().with_options(parentheses)])?; - } else if comments.has_trailing_comments(return_annotation.as_ref()) { - // Intentionally parenthesize any return annotations with trailing comments. - // This avoids an instability in cases like: - // ```python - // def double( - // a: int - // ) -> ( - // int # Hello - // ): - // pass - // ``` - // If we allow this to break, it will be formatted as follows: - // ```python - // def double( - // a: int - // ) -> int: # Hello - // pass - // ``` - // On subsequent formats, the `# Hello` will be interpreted as a dangling - // comment on a function, yielding: - // ```python - // def double(a: int) -> int: # Hello - // pass - // ``` - // Ideally, we'd reach that final formatting in a single pass, but doing so - // requires that the parent be aware of how the child is formatted, which - // is challenging. As a compromise, we break those expressions to avoid an - // instability. - write!( - f, - [return_annotation.format().with_options(Parentheses::Always)] - )?; - } else { - write!( - f, - [maybe_parenthesize_expression( - return_annotation, - item, - if empty_parameters(parameters, f.context().source()) { - // If the parameters are empty, add parentheses if the return annotation - // breaks at all. - Parenthesize::IfBreaksOrIfRequired - } else { - // Otherwise, use our normal rules for parentheses, which allows us to break - // like: - // ```python - // def f( - // x, - // ) -> Tuple[ - // int, - // int, - // ]: - // ... - // ``` - Parenthesize::IfBreaks - }, - )] - )?; - } - } - Ok(()) - }); - - write!(f, [group(&format_inner)])?; - write!( f, [ - text(":"), - trailing_comments(trailing_definition_comments), + FormatDecorators { + decorators: decorator_list, + leading_definition_comments, + }, + clause_header( + ClauseHeader::Function(item), + trailing_definition_comments, + &format_with(|f| { + if *is_async { + write!(f, [text("async"), space()])?; + } + + write!(f, [text("def"), space(), name.format()])?; + + if let Some(type_params) = type_params.as_ref() { + write!(f, [type_params.format()])?; + } + + let format_inner = format_with(|f: &mut PyFormatter| { + write!(f, [parameters.format()])?; + + if let Some(return_annotation) = returns.as_ref() { + write!(f, [space(), text("->"), space()])?; + + if return_annotation.is_tuple_expr() { + let parentheses = if comments + .has_leading_comments(return_annotation.as_ref()) + { + Parentheses::Always + } else { + Parentheses::Never + }; + write!( + f, + [return_annotation.format().with_options(parentheses)] + )?; + } else if comments.has_trailing_comments(return_annotation.as_ref()) + { + // Intentionally parenthesize any return annotations with trailing comments. + // This avoids an instability in cases like: + // ```python + // def double( + // a: int + // ) -> ( + // int # Hello + // ): + // pass + // ``` + // If we allow this to break, it will be formatted as follows: + // ```python + // def double( + // a: int + // ) -> int: # Hello + // pass + // ``` + // On subsequent formats, the `# Hello` will be interpreted as a dangling + // comment on a function, yielding: + // ```python + // def double(a: int) -> int: # Hello + // pass + // ``` + // Ideally, we'd reach that final formatting in a single pass, but doing so + // requires that the parent be aware of how the child is formatted, which + // is challenging. As a compromise, we break those expressions to avoid an + // instability. + write!( + f, + [return_annotation + .format() + .with_options(Parentheses::Always)] + )?; + } else { + write!( + f, + [maybe_parenthesize_expression( + return_annotation, + item, + if empty_parameters(parameters, f.context().source()) { + // If the parameters are empty, add parentheses if the return annotation + // breaks at all. + Parenthesize::IfBreaksOrIfRequired + } else { + // Otherwise, use our normal rules for parentheses, which allows us to break + // like: + // ```python + // def f( + // x, + // ) -> Tuple[ + // int, + // int, + // ]: + // ... + // ``` + Parenthesize::IfBreaks + }, + )] + )?; + } + } + Ok(()) + }); + + group(&format_inner).fmt(f) + }), + ), block_indent(&body.format().with_options(SuiteKind::Function)) ] ) diff --git a/crates/ruff_python_formatter/src/statement/stmt_if.rs b/crates/ruff_python_formatter/src/statement/stmt_if.rs index 35596b3c32..18470728dc 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_if.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_if.rs @@ -1,11 +1,13 @@ -use crate::comments::{leading_alternate_branch_comments, trailing_comments, SourceComment}; +use ruff_formatter::{format_args, write}; +use ruff_python_ast::node::AnyNodeRef; +use ruff_python_ast::{ElifElseClause, StmtIf}; + +use crate::comments::SourceComment; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; +use crate::statement::clause::{clause_header, ClauseHeader}; use crate::FormatNodeRule; -use ruff_formatter::write; -use ruff_python_ast::node::AnyNodeRef; -use ruff_python_ast::{ElifElseClause, StmtIf}; #[derive(Default)] pub struct FormatStmtIf; @@ -25,11 +27,15 @@ impl FormatNodeRule for FormatStmtIf { write!( f, [ - text("if"), - space(), - maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), - text(":"), - trailing_comments(trailing_colon_comment), + clause_header( + ClauseHeader::If(item), + trailing_colon_comment, + &format_args![ + text("if"), + space(), + maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), + ], + ), block_indent(&body.format()) ] )?; @@ -70,26 +76,28 @@ pub(crate) fn format_elif_else_clause( let trailing_colon_comment = comments.dangling_comments(item); let leading_comments = comments.leading_comments(item); - leading_alternate_branch_comments(leading_comments, last_node).fmt(f)?; - - if let Some(test) = test { - write!( - f, - [ - text("elif"), - space(), - maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), - ] - )?; - } else { - text("else").fmt(f)?; - } - write!( f, [ - text(":"), - trailing_comments(trailing_colon_comment), + clause_header( + ClauseHeader::ElifElse(item), + trailing_colon_comment, + &format_with(|f| { + if let Some(test) = test { + write!( + f, + [ + text("elif"), + space(), + maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), + ] + ) + } else { + text("else").fmt(f) + } + }), + ) + .with_leading_comments(leading_comments, last_node), block_indent(&body.format()) ] ) diff --git a/crates/ruff_python_formatter/src/statement/stmt_match.rs b/crates/ruff_python_formatter/src/statement/stmt_match.rs index 09347947d8..9c9b790eae 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_match.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_match.rs @@ -1,11 +1,12 @@ use ruff_formatter::{format_args, write}; use ruff_python_ast::StmtMatch; -use crate::comments::{leading_alternate_branch_comments, trailing_comments, SourceComment}; +use crate::comments::{leading_alternate_branch_comments, SourceComment}; use crate::context::{NodeLevel, WithNodeLevel}; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; +use crate::statement::clause::{clause_header, ClauseHeader}; use crate::FormatNodeRule; #[derive(Default)] @@ -25,16 +26,16 @@ impl FormatNodeRule for FormatStmtMatch { // There can be at most one dangling comment after the colon in a match statement. debug_assert!(dangling_item_comments.len() <= 1); - write!( - f, - [ + clause_header( + ClauseHeader::Match(item), + dangling_item_comments, + &format_args![ text("match"), space(), maybe_parenthesize_expression(subject, item, Parenthesize::IfBreaks), - text(":"), - trailing_comments(dangling_item_comments) - ] - )?; + ], + ) + .fmt(f)?; let mut cases_iter = cases.iter(); let Some(first) = cases_iter.next() else { diff --git a/crates/ruff_python_formatter/src/statement/stmt_try.rs b/crates/ruff_python_formatter/src/statement/stmt_try.rs index 7213a3a027..17263898a4 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_try.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_try.rs @@ -1,11 +1,12 @@ use ruff_formatter::{write, FormatRuleWithOptions}; -use ruff_python_ast::{ExceptHandler, Ranged, StmtTry, Suite}; +use ruff_python_ast::{ExceptHandler, Ranged, StmtTry}; use crate::comments; +use crate::comments::leading_alternate_branch_comments; use crate::comments::SourceComment; -use crate::comments::{leading_alternate_branch_comments, trailing_comments}; use crate::other::except_handler_except_handler::ExceptHandlerKind; use crate::prelude::*; +use crate::statement::clause::{clause_header, ClauseHeader, ElseClause}; use crate::statement::{FormatRefWithRule, Stmt}; use crate::{FormatNodeRule, PyFormatter}; @@ -55,8 +56,8 @@ impl FormatNodeRule for FormatStmtTry { let StmtTry { body, handlers, - orelse, - finalbody, + orelse: _, + finalbody: _, is_star, range: _, } = item; @@ -64,7 +65,7 @@ impl FormatNodeRule for FormatStmtTry { let comments_info = f.context().comments().clone(); let mut dangling_comments = comments_info.dangling_comments(item); - (_, dangling_comments) = format_case("try", body, None, dangling_comments, f)?; + (_, dangling_comments) = format_case(item, CaseKind::Try, None, dangling_comments, f)?; let mut previous_node = body.last(); for handler in handlers { @@ -86,9 +87,9 @@ impl FormatNodeRule for FormatStmtTry { } (previous_node, dangling_comments) = - format_case("else", orelse, previous_node, dangling_comments, f)?; + format_case(item, CaseKind::Else, previous_node, dangling_comments, f)?; - format_case("finally", finalbody, previous_node, dangling_comments, f)?; + format_case(item, CaseKind::Finally, previous_node, dangling_comments, f)?; write!(f, [comments::dangling_comments(dangling_comments)]) } @@ -104,25 +105,39 @@ impl FormatNodeRule for FormatStmtTry { } fn format_case<'a>( - name: &'static str, - body: &Suite, + try_statement: &StmtTry, + kind: CaseKind, previous_node: Option<&Stmt>, dangling_comments: &'a [SourceComment], f: &mut PyFormatter, ) -> FormatResult<(Option<&'a Stmt>, &'a [SourceComment])> { + let body = match kind { + CaseKind::Try => &try_statement.body, + CaseKind::Else => &try_statement.orelse, + CaseKind::Finally => &try_statement.finalbody, + }; + Ok(if let Some(last) = body.last() { let case_comments_start = dangling_comments.partition_point(|comment| comment.slice().end() <= last.end()); let (case_comments, rest) = dangling_comments.split_at(case_comments_start); let partition_point = case_comments.partition_point(|comment| comment.line_position().is_own_line()); + + let (leading_case_comments, trailing_case_comments) = + case_comments.split_at(partition_point); + + let header = match kind { + CaseKind::Try => ClauseHeader::Try(try_statement), + CaseKind::Else => ClauseHeader::OrElse(ElseClause::Try(try_statement)), + CaseKind::Finally => ClauseHeader::TryFinally(try_statement), + }; + write!( f, [ - leading_alternate_branch_comments(&case_comments[..partition_point], previous_node), - text(name), - text(":"), - trailing_comments(&case_comments[partition_point..]), + clause_header(header, trailing_case_comments, &text(kind.keyword())) + .with_leading_comments(leading_case_comments, previous_node), block_indent(&body.format()) ] )?; @@ -131,3 +146,20 @@ fn format_case<'a>( (None, dangling_comments) }) } + +#[derive(Copy, Clone)] +enum CaseKind { + Try, + Else, + Finally, +} + +impl CaseKind { + fn keyword(self) -> &'static str { + match self { + CaseKind::Try => "try", + CaseKind::Else => "else", + CaseKind::Finally => "finally", + } + } +} diff --git a/crates/ruff_python_formatter/src/statement/stmt_while.rs b/crates/ruff_python_formatter/src/statement/stmt_while.rs index 0508278667..e01dd5ae6e 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_while.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_while.rs @@ -1,11 +1,12 @@ -use ruff_formatter::write; +use ruff_formatter::{format_args, write}; use ruff_python_ast::node::AstNode; use ruff_python_ast::{Ranged, Stmt, StmtWhile}; -use crate::comments::{leading_alternate_branch_comments, trailing_comments, SourceComment}; +use crate::comments::SourceComment; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; +use crate::statement::clause::{clause_header, ClauseHeader, ElseClause}; use crate::FormatNodeRule; #[derive(Default)] @@ -33,11 +34,15 @@ impl FormatNodeRule for FormatStmtWhile { write!( f, [ - text("while"), - space(), - maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), - text(":"), - trailing_comments(trailing_condition_comments), + clause_header( + ClauseHeader::While(item), + trailing_condition_comments, + &format_args![ + text("while"), + space(), + maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), + ] + ), block_indent(&body.format()) ] )?; @@ -52,9 +57,12 @@ impl FormatNodeRule for FormatStmtWhile { write!( f, [ - leading_alternate_branch_comments(leading, body.last()), - text("else:"), - trailing_comments(trailing), + clause_header( + ClauseHeader::OrElse(ElseClause::While(item)), + trailing, + &text("else") + ) + .with_leading_comments(leading, body.last()), block_indent(&orelse.format()) ] )?; diff --git a/crates/ruff_python_formatter/src/statement/stmt_with.rs b/crates/ruff_python_formatter/src/statement/stmt_with.rs index fb08c27f76..b96e4f7535 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_with.rs @@ -4,11 +4,12 @@ use ruff_python_ast::{Ranged, StmtWith}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::TextRange; -use crate::comments::{trailing_comments, SourceComment}; +use crate::comments::SourceComment; use crate::expression::parentheses::{ in_parentheses_only_soft_line_break_or_space, optional_parentheses, parenthesized, }; use crate::prelude::*; +use crate::statement::clause::{clause_header, ClauseHeader}; use crate::FormatNodeRule; #[derive(Default)] @@ -16,16 +17,6 @@ pub struct FormatStmtWith; impl FormatNodeRule for FormatStmtWith { fn fmt_fields(&self, item: &StmtWith, f: &mut PyFormatter) -> FormatResult<()> { - write!( - f, - [ - item.is_async - .then_some(format_args![text("async"), space()]), - text("with"), - space() - ] - )?; - // The `with` statement can have one dangling comment on the open parenthesis, like: // ```python // with ( # comment @@ -48,41 +39,57 @@ impl FormatNodeRule for FormatStmtWith { }); let (parenthesized_comments, colon_comments) = dangling_comments.split_at(partition_point); - if !parenthesized_comments.is_empty() { - let joined = format_with(|f: &mut PyFormatter| { - f.join_comma_separated(item.body.first().unwrap().start()) - .nodes(&item.items) - .finish() - }); - - parenthesized("(", &joined, ")") - .with_dangling_comments(parenthesized_comments) - .fmt(f)?; - } else if are_with_items_parenthesized(item, f.context())? { - optional_parentheses(&format_with(|f| { - let mut joiner = f.join_comma_separated(item.body.first().unwrap().start()); - - for item in &item.items { - joiner.entry_with_line_separator( - item, - &item.format(), - in_parentheses_only_soft_line_break_or_space(), - ); - } - joiner.finish() - })) - .fmt(f)?; - } else { - f.join_with(format_args![text(","), space()]) - .entries(item.items.iter().formatted()) - .finish()?; - } - write!( f, [ - text(":"), - trailing_comments(colon_comments), + clause_header( + ClauseHeader::With(item), + colon_comments, + &format_with(|f| { + write!( + f, + [ + item.is_async + .then_some(format_args![text("async"), space()]), + text("with"), + space() + ] + )?; + + if !parenthesized_comments.is_empty() { + let joined = format_with(|f: &mut PyFormatter| { + f.join_comma_separated(item.body.first().unwrap().start()) + .nodes(&item.items) + .finish() + }); + + parenthesized("(", &joined, ")") + .with_dangling_comments(parenthesized_comments) + .fmt(f)?; + } else if are_with_items_parenthesized(item, f.context())? { + optional_parentheses(&format_with(|f| { + let mut joiner = + f.join_comma_separated(item.body.first().unwrap().start()); + + for item in &item.items { + joiner.entry_with_line_separator( + item, + &item.format(), + in_parentheses_only_soft_line_break_or_space(), + ); + } + joiner.finish() + })) + .fmt(f)?; + } else { + f.join_with(format_args![text(","), space()]) + .entries(item.items.iter().formatted()) + .finish()?; + } + + Ok(()) + }) + ), block_indent(&item.body.format()) ] ) diff --git a/crates/ruff_python_formatter/src/verbatim.rs b/crates/ruff_python_formatter/src/verbatim.rs index d7bfa9beaf..39fbac41b1 100644 --- a/crates/ruff_python_formatter/src/verbatim.rs +++ b/crates/ruff_python_formatter/src/verbatim.rs @@ -15,6 +15,7 @@ use ruff_text_size::{TextRange, TextSize}; use crate::comments::format::{empty_lines, format_comment}; use crate::comments::{leading_comments, trailing_comments, SourceComment}; use crate::prelude::*; +use crate::statement::clause::ClauseHeader; use crate::statement::suite::SuiteChildStatement; /// Disables formatting for all statements between the `first_suppressed` that has a leading `fmt: off` comment @@ -930,3 +931,28 @@ impl Format> for FormatSuppressedNode<'_> { ) } } + +#[cold] +pub(crate) fn write_suppressed_clause_header( + header: ClauseHeader, + f: &mut PyFormatter, +) -> FormatResult<()> { + // Write the outer comments and format the node as verbatim + write!( + f, + [verbatim_text( + header.range(f.context().source())?, + ContainsNewlines::Detect + ),] + )?; + + let comments = f.context().comments(); + header.visit(&mut |child| { + for comment in comments.leading_trailing_comments(child) { + comment.mark_formatted(); + } + comments.mark_verbatim_node_comments_formatted(child); + }); + + Ok(()) +} diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip8.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip8.py.snap deleted file mode 100644 index cb1c1a2cf9..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip8.py.snap +++ /dev/null @@ -1,292 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip8.py ---- -## Input - -```py -# Make sure a leading comment is not removed. -def some_func( unformatted, args ): # fmt: skip - print("I am some_func") - return 0 - # Make sure this comment is not removed. - - -# Make sure a leading comment is not removed. -async def some_async_func( unformatted, args): # fmt: skip - print("I am some_async_func") - await asyncio.sleep(1) - - -# Make sure a leading comment is not removed. -class SomeClass( Unformatted, SuperClasses ): # fmt: skip - def some_method( self, unformatted, args ): # fmt: skip - print("I am some_method") - return 0 - - async def some_async_method( self, unformatted, args ): # fmt: skip - print("I am some_async_method") - await asyncio.sleep(1) - - -# Make sure a leading comment is not removed. -if unformatted_call( args ): # fmt: skip - print("First branch") - # Make sure this is not removed. -elif another_unformatted_call( args ): # fmt: skip - print("Second branch") -else : # fmt: skip - print("Last branch") - - -while some_condition( unformatted, args ): # fmt: skip - print("Do something") - - -for i in some_iter( unformatted, args ): # fmt: skip - print("Do something") - - -async def test_async_for(): - async for i in some_async_iter( unformatted, args ): # fmt: skip - print("Do something") - - -try : # fmt: skip - some_call() -except UnformattedError as ex: # fmt: skip - handle_exception() -finally : # fmt: skip - finally_call() - - -with give_me_context( unformatted, args ): # fmt: skip - print("Do something") - - -async def test_async_with(): - async with give_me_async_context( unformatted, args ): # fmt: skip - print("Do something") -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,62 +1,62 @@ - # Make sure a leading comment is not removed. --def some_func( unformatted, args ): # fmt: skip -+def some_func(unformatted, args): # fmt: skip - print("I am some_func") - return 0 - # Make sure this comment is not removed. - - - # Make sure a leading comment is not removed. --async def some_async_func( unformatted, args): # fmt: skip -+async def some_async_func(unformatted, args): # fmt: skip - print("I am some_async_func") - await asyncio.sleep(1) - - - # Make sure a leading comment is not removed. --class SomeClass( Unformatted, SuperClasses ): # fmt: skip -- def some_method( self, unformatted, args ): # fmt: skip -+class SomeClass(Unformatted, SuperClasses): # fmt: skip -+ def some_method(self, unformatted, args): # fmt: skip - print("I am some_method") - return 0 - -- async def some_async_method( self, unformatted, args ): # fmt: skip -+ async def some_async_method(self, unformatted, args): # fmt: skip - print("I am some_async_method") - await asyncio.sleep(1) - - - # Make sure a leading comment is not removed. --if unformatted_call( args ): # fmt: skip -+if unformatted_call(args): # fmt: skip - print("First branch") - # Make sure this is not removed. --elif another_unformatted_call( args ): # fmt: skip -+elif another_unformatted_call(args): # fmt: skip - print("Second branch") --else : # fmt: skip -+else: # fmt: skip - print("Last branch") - - --while some_condition( unformatted, args ): # fmt: skip -+while some_condition(unformatted, args): # fmt: skip - print("Do something") - - --for i in some_iter( unformatted, args ): # fmt: skip -+for i in some_iter(unformatted, args): # fmt: skip - print("Do something") - - - 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") - - --try : # fmt: skip -+try: # fmt: skip - some_call() --except UnformattedError as ex: # fmt: skip -+except UnformattedError as ex: # fmt: skip - handle_exception() --finally : # fmt: skip -+finally: # fmt: skip - finally_call() - - --with give_me_context( unformatted, args ): # fmt: skip -+with give_me_context(unformatted, args): # fmt: skip - print("Do something") - - - 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") -``` - -## Ruff Output - -```py -# Make sure a leading comment is not removed. -def some_func(unformatted, args): # fmt: skip - print("I am some_func") - return 0 - # Make sure this comment is not removed. - - -# Make sure a leading comment is not removed. -async def some_async_func(unformatted, args): # fmt: skip - print("I am some_async_func") - await asyncio.sleep(1) - - -# Make sure a leading comment is not removed. -class SomeClass(Unformatted, SuperClasses): # fmt: skip - def some_method(self, unformatted, args): # fmt: skip - print("I am some_method") - return 0 - - async def some_async_method(self, unformatted, args): # fmt: skip - print("I am some_async_method") - await asyncio.sleep(1) - - -# Make sure a leading comment is not removed. -if unformatted_call(args): # fmt: skip - print("First branch") - # Make sure this is not removed. -elif another_unformatted_call(args): # fmt: skip - print("Second branch") -else: # fmt: skip - print("Last branch") - - -while some_condition(unformatted, args): # fmt: skip - print("Do something") - - -for i in some_iter(unformatted, args): # fmt: skip - print("Do something") - - -async def test_async_for(): - async for i in some_async_iter(unformatted, args): # fmt: skip - print("Do something") - - -try: # fmt: skip - some_call() -except UnformattedError as ex: # fmt: skip - handle_exception() -finally: # fmt: skip - finally_call() - - -with give_me_context(unformatted, args): # fmt: skip - print("Do something") - - -async def test_async_with(): - async with give_me_async_context(unformatted, args): # fmt: skip - print("Do something") -``` - -## Black Output - -```py -# Make sure a leading comment is not removed. -def some_func( unformatted, args ): # fmt: skip - print("I am some_func") - return 0 - # Make sure this comment is not removed. - - -# Make sure a leading comment is not removed. -async def some_async_func( unformatted, args): # fmt: skip - print("I am some_async_func") - await asyncio.sleep(1) - - -# Make sure a leading comment is not removed. -class SomeClass( Unformatted, SuperClasses ): # fmt: skip - def some_method( self, unformatted, args ): # fmt: skip - print("I am some_method") - return 0 - - async def some_async_method( self, unformatted, args ): # fmt: skip - print("I am some_async_method") - await asyncio.sleep(1) - - -# Make sure a leading comment is not removed. -if unformatted_call( args ): # fmt: skip - print("First branch") - # Make sure this is not removed. -elif another_unformatted_call( args ): # fmt: skip - print("Second branch") -else : # fmt: skip - print("Last branch") - - -while some_condition( unformatted, args ): # fmt: skip - print("Do something") - - -for i in some_iter( unformatted, args ): # fmt: skip - print("Do something") - - -async def test_async_for(): - async for i in some_async_iter( unformatted, args ): # fmt: skip - print("Do something") - - -try : # fmt: skip - some_call() -except UnformattedError as ex: # fmt: skip - handle_exception() -finally : # fmt: skip - finally_call() - - -with give_me_context( unformatted, args ): # fmt: skip - print("Do something") - - -async def test_async_with(): - async with give_me_async_context( unformatted, args ): # fmt: skip - print("Do something") -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__match.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__match.py.snap new file mode 100644 index 0000000000..0e6f84da78 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__match.py.snap @@ -0,0 +1,163 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/match.py +--- +## Input +```py +def http_error(status): + match status : # fmt: skip + case 400 : # fmt: skip + return "Bad request" + case 404: + return "Not found" + case 418: + return "I'm a teapot" + case _: + return "Something's wrong with the internet" + +# point is an (x, y) tuple +match point: + case (0, 0): # fmt: skip + print("Origin") + case (0, y): + print(f"Y={y}") + case (x, 0): + print(f"X={x}") + case (x, y): + print(f"X={x}, Y={y}") + case _: + raise ValueError("Not a point") + +class Point: + x: int + y: int + +def location(point): + match point: + case Point(x=0, y =0 ) : # fmt: skip + print("Origin is the point's location.") + case Point(x=0, y=y): + print(f"Y={y} and the point is on the y-axis.") + case Point(x=x, y=0): + print(f"X={x} and the point is on the x-axis.") + case Point(): + print("The point is located somewhere else on the plane.") + case _: + print("Not a point") + + +match points: + case []: + print("No points in the list.") + case [ + Point(0, 0) + ]: # fmt: skip + print("The origin is the only point in the list.") + case [Point(x, y)]: + print(f"A single point {x}, {y} is in the list.") + case [Point(0, y1), Point(0, y2)]: + print(f"Two points on the Y axis at {y1}, {y2} are in the list.") + case _: + print("Something else is found in the list.") + + +match test_variable: + case ( + 'warning', + code, + 40 + ): # fmt: skip + print("A warning has been received.") + case ('error', code, _): + print(f"An error {code} occurred.") + + +match point: + case Point(x, y) if x == y: # fmt: skip + print(f"The point is located on the diagonal Y=X at {x}.") + case Point(x, y): + print(f"Point is not on the diagonal.") +``` + +## Output +```py +def http_error(status): + match status : # fmt: skip + case 400 : # fmt: skip + return "Bad request" + case "NOT_YET_IMPLEMENTED_PatternMatchValue": + return "Not found" + case "NOT_YET_IMPLEMENTED_PatternMatchValue": + return "I'm a teapot" + case x as NOT_YET_IMPLEMENTED_PatternMatchAs: + return "Something's wrong with the internet" + + +# point is an (x, y) tuple +match point: + case (0, 0): # fmt: skip + print("Origin") + case [NOT_YET_IMPLEMENTED_PatternMatchSequence, 2]: + print(f"Y={y}") + case [NOT_YET_IMPLEMENTED_PatternMatchSequence, 2]: + print(f"X={x}") + case [NOT_YET_IMPLEMENTED_PatternMatchSequence, 2]: + print(f"X={x}, Y={y}") + case x as NOT_YET_IMPLEMENTED_PatternMatchAs: + raise ValueError("Not a point") + + +class Point: + x: int + y: int + + +def location(point): + match point: + case Point(x=0, y =0 ) : # fmt: skip + print("Origin is the point's location.") + case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + print(f"Y={y} and the point is on the y-axis.") + case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + print(f"X={x} and the point is on the x-axis.") + case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + print("The point is located somewhere else on the plane.") + case x as NOT_YET_IMPLEMENTED_PatternMatchAs: + print("Not a point") + + +match points: + case [NOT_YET_IMPLEMENTED_PatternMatchSequence, 2]: + print("No points in the list.") + case [ + Point(0, 0) + ]: # fmt: skip + print("The origin is the only point in the list.") + case [NOT_YET_IMPLEMENTED_PatternMatchSequence, 2]: + print(f"A single point {x}, {y} is in the list.") + case [NOT_YET_IMPLEMENTED_PatternMatchSequence, 2]: + print(f"Two points on the Y axis at {y1}, {y2} are in the list.") + case x as NOT_YET_IMPLEMENTED_PatternMatchAs: + print("Something else is found in the list.") + + +match test_variable: + case ( + 'warning', + code, + 40 + ): # fmt: skip + print("A warning has been received.") + case [NOT_YET_IMPLEMENTED_PatternMatchSequence, 2]: + print(f"An error {code} occurred.") + + +match point: + case Point(x, y) if x == y: # fmt: skip + print(f"The point is located on the diagonal Y=X at {x}.") + case NOT_YET_IMPLEMENTED_PatternMatchClass(0, 0): + print(f"Point is not on the diagonal.") +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__or_else.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__or_else.py.snap new file mode 100644 index 0000000000..4293169e6b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__or_else.py.snap @@ -0,0 +1,76 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/or_else.py +--- +## Input +```py +for item in container: + if search_something(item): + # Found it! + process(item) + break + # leading comment +else : #fmt: skip + # Didn't find anything.. + not_found_in_container() + + +while i < 10: + print(i) + +# leading comment +else : #fmt: skip + # Didn't find anything.. + print("I was already larger than 9") + + +try : # fmt: skip + some_call() +except Exception : # fmt: skip + pass +except : # fmt: skip + handle_exception() + +else : # fmt: skip + pass +finally : # fmt: skip + finally_call() +``` + +## Output +```py +for item in container: + if search_something(item): + # Found it! + process(item) + break +# leading comment +else : # fmt: skip + # Didn't find anything.. + not_found_in_container() + + +while i < 10: + print(i) + +# leading comment +else : # fmt: skip + # Didn't find anything.. + print("I was already larger than 9") + + +try : # fmt: skip + some_call() +except Exception : # fmt: skip + pass +except : # fmt: skip + handle_exception() + +else : # fmt: skip + pass +finally : # fmt: skip + finally_call() +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__parentheses.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__parentheses.py.snap new file mode 100644 index 0000000000..30d4872aed --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__parentheses.py.snap @@ -0,0 +1,54 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/parentheses.py +--- +## Input +```py +if ( + # a leading condition comment + len([1, 23, 3, 4, 5]) > 2 # trailing condition comment + # trailing own line comment +): # fmt: skip + pass + + +if ( # trailing open parentheses comment + # a leading condition comment + len([1, 23, 3, 4, 5]) > 2 +) and ((((y)))): # fmt: skip + pass + + +if ( # trailing open parentheses comment + # a leading condition comment + len([1, 23, 3, 4, 5]) > 2 +) and y: # fmt: skip + pass +``` + +## Output +```py +if ( + # a leading condition comment + len([1, 23, 3, 4, 5]) > 2 # trailing condition comment + # trailing own line comment +): # fmt: skip + pass + + +if ( # trailing open parentheses comment + # a leading condition comment + len([1, 23, 3, 4, 5]) > 2 +) and ((((y)))): # fmt: skip + pass + + +if ( # trailing open parentheses comment + # a leading condition comment + len([1, 23, 3, 4, 5]) > 2 +) and y: # fmt: skip + pass +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__type_params.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__type_params.py.snap new file mode 100644 index 0000000000..aa35d9abd8 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__type_params.py.snap @@ -0,0 +1,73 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/type_params.py +--- +## Input +```py +class TestTypeParam[ T ]: # fmt: skip + pass + +class TestTypeParam [ # trailing open paren comment + # leading comment + T # trailing type param comment + # trailing type param own line comment +]: # fmt: skip + pass + +class TestTrailingComment4[ + T +] ( # trailing arguments open parenthesis comment + # leading argument comment + A # trailing argument comment + # trailing argument own line comment +): # fmt: skip + pass + +def test [ + # comment + A, + + # another + + B, +] (): # fmt: skip + ... +``` + +## Output +```py +class TestTypeParam[ T ]: # fmt: skip + pass + + +class TestTypeParam [ # trailing open paren comment + # leading comment + T # trailing type param comment + # trailing type param own line comment +]: # fmt: skip + pass + + +class TestTrailingComment4[ + T +] ( # trailing arguments open parenthesis comment + # leading argument comment + A # trailing argument comment + # trailing argument own line comment +): # fmt: skip + pass + + +def test [ + # comment + A, + + # another + + B, +] (): # fmt: skip + ... +``` + + +