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 00000000000000..0ff31d43836ad6 --- /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 00000000000000..c2dccee81e5473 --- /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/src/other/except_handler_except_handler.rs b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs index 2f3524ba23db84..15ddd1d1fe6587 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,7 +1,8 @@ -use crate::comments::{trailing_comments, SourceComment}; +use crate::comments::{trailing_comments, SourceComment, SuppressionKind}; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; +use crate::verbatim::SuppressedClauseHeader; use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::FormatRuleWithOptions; use ruff_formatter::{write, Buffer, FormatResult}; @@ -46,33 +47,39 @@ impl FormatNodeRule for FormatExceptHandlerExceptHan let comments_info = f.context().comments().clone(); let dangling_comments = comments_info.dangling_comments(item); - write!( - f, - [ - text("except"), - match self.except_handler_kind { - ExceptHandlerKind::Regular => None, - ExceptHandlerKind::Starred => Some(text("*")), - } - ] - )?; - - if let Some(type_) = type_ { + if SuppressionKind::has_skip_comment(dangling_comments, f.context().source()) { + SuppressedClauseHeader::ExceptHandler(item).fmt(f)?; + } else { write!( f, [ - space(), - maybe_parenthesize_expression(type_, item, Parenthesize::IfBreaks) + text("except"), + match self.except_handler_kind { + ExceptHandlerKind::Regular => None, + ExceptHandlerKind::Starred => Some(text("*")), + } ] )?; - if let Some(name) = name { - write!(f, [space(), text("as"), space(), name.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()])?; + } } + + text(":").fmt(f)?; } + write!( f, [ - text(":"), trailing_comments(dangling_comments), 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 1f60c163f07654..b1265f185a97ce 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, trailing_comments, SourceComment, SuppressionKind}; use crate::expression::parentheses::parenthesized; use crate::prelude::*; +use crate::verbatim::SuppressedClauseHeader; use crate::{FormatError, FormatNodeRule, PyFormatter}; #[derive(Default)] @@ -23,29 +24,35 @@ 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)?; + if SuppressionKind::has_skip_comment(dangling_item_comments, f.context().source()) { + SuppressedClauseHeader::MatchCase(item).fmt(f)?; } else { - pattern.format().fmt(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()])?; + } - if let Some(guard) = guard { - write!(f, [space(), text("if"), space(), guard.format()])?; + text(":").fmt(f)?; } write!( f, [ - text(":"), trailing_comments(dangling_item_comments), block_indent(&body.format()) ] 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 3b4a1f2ac01658..81ba2c66a4651b 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -2,9 +2,10 @@ use ruff_formatter::write; use ruff_python_ast::{Decorator, Ranged, StmtClassDef}; use ruff_python_trivia::lines_after_ignoring_trivia; -use crate::comments::{leading_comments, trailing_comments, SourceComment}; +use crate::comments::{leading_comments, trailing_comments, SourceComment, SuppressionKind}; use crate::prelude::*; use crate::statement::suite::SuiteKind; +use crate::verbatim::SuppressedClauseHeader; use crate::FormatNodeRule; #[derive(Default)] @@ -36,71 +37,76 @@ impl FormatNodeRule for FormatStmtClassDef { } .fmt(f)?; - write!(f, [text("class"), space(), name.format()])?; + if SuppressionKind::has_skip_comment(trailing_definition_comments, f.context().source()) { + SuppressedClauseHeader::Class(item).fmt(f)?; + } else { + write!(f, [text("class"), space(), name.format()])?; - if let Some(type_params) = type_params.as_deref() { - write!(f, [type_params.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()])?; + 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(":"),])?; } write!( f, [ - text(":"), trailing_comments(trailing_definition_comments), 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 22bc0017cf0ee2..19cc162aaa1390 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_for.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_for.rs @@ -1,11 +1,14 @@ 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::{ + leading_alternate_branch_comments, trailing_comments, SourceComment, SuppressionKind, +}; use crate::expression::expr_tuple::TupleParentheses; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; +use crate::verbatim::{OrElseParent, SuppressedClauseHeader}; use crate::FormatNodeRule; #[derive(Debug)] @@ -46,18 +49,28 @@ impl FormatNodeRule for FormatStmtFor { let (trailing_condition_comments, or_else_comments) = dangling_comments.split_at(or_else_comments_start); + if SuppressionKind::has_skip_comment(trailing_condition_comments, f.context().source()) { + SuppressedClauseHeader::For(item).fmt(f)?; + } else { + 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(":"), + ] + )?; + } + 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), block_indent(&body.format()) ] @@ -72,14 +85,18 @@ impl FormatNodeRule for FormatStmtFor { or_else_comments.partition_point(|comment| comment.line_position().is_own_line()); let (leading, trailing) = or_else_comments.split_at(trailing_start); + leading_alternate_branch_comments(leading, body.last()).fmt(f)?; + + if SuppressionKind::has_skip_comment(trailing_condition_comments, f.context().source()) + { + SuppressedClauseHeader::OrElse(OrElseParent::For(item)).fmt(f)?; + } else { + text("else:").fmt(f)?; + } + write!( f, - [ - leading_alternate_branch_comments(leading, body.last()), - text("else:"), - trailing_comments(trailing), - block_indent(&orelse.format()) - ] + [trailing_comments(trailing), 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 5897c4cab088ee..59a574e6c93c6d 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -2,12 +2,13 @@ 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::{trailing_comments, SourceComment, SuppressionKind}; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::{Parentheses, Parenthesize}; use crate::prelude::*; use crate::statement::stmt_class_def::FormatDecorators; use crate::statement::suite::SuiteKind; +use crate::verbatim::SuppressedClauseHeader; use crate::FormatNodeRule; #[derive(Default)] @@ -41,98 +42,102 @@ impl FormatNodeRule for FormatStmtFunctionDef { } .fmt(f)?; - if *is_async { - write!(f, [text("async"), space()])?; - } + if SuppressionKind::has_skip_comment(trailing_definition_comments, f.context().source()) { + SuppressedClauseHeader::Function(item).fmt(f)?; + } else { + if *is_async { + write!(f, [text("async"), space()])?; + } - write!(f, [text("def"), space(), name.format()])?; + write!(f, [text("def"), space(), name.format()])?; - if let Some(type_params) = type_params.as_ref() { - write!(f, [type_params.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()])?; + 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 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 + if return_annotation.is_tuple_expr() { + let parentheses = + if comments.has_leading_comments(return_annotation.as_ref()) { + Parentheses::Always } else { - // Otherwise, use our normal rules for parentheses, which allows us to break - // like: - // ```python - // def f( - // x, - // ) -> Tuple[ - // int, - // int, - // ]: - // ... - // ``` - Parenthesize::IfBreaks - }, - )] - )?; + 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(()) - }); + Ok(()) + }); - write!(f, [group(&format_inner)])?; + write!(f, [group(&format_inner), text(":"),])?; + } write!( f, [ - text(":"), trailing_comments(trailing_definition_comments), 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 35596b3c322cc8..895602a3a8cf36 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_if.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_if.rs @@ -1,7 +1,10 @@ -use crate::comments::{leading_alternate_branch_comments, trailing_comments, SourceComment}; +use crate::comments::{ + leading_alternate_branch_comments, trailing_comments, SourceComment, SuppressionKind, +}; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; +use crate::verbatim::SuppressedClauseHeader; use crate::FormatNodeRule; use ruff_formatter::write; use ruff_python_ast::node::AnyNodeRef; @@ -22,13 +25,23 @@ impl FormatNodeRule for FormatStmtIf { let comments = f.context().comments().clone(); let trailing_colon_comment = comments.dangling_comments(item); + if SuppressionKind::has_skip_comment(trailing_colon_comment, f.context().source()) { + SuppressedClauseHeader::If(item).fmt(f)?; + } else { + write!( + f, + [ + text("if"), + space(), + maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), + text(":"), + ] + )?; + } + write!( f, [ - text("if"), - space(), - maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), - text(":"), trailing_comments(trailing_colon_comment), block_indent(&body.format()) ] @@ -72,23 +85,28 @@ pub(crate) fn format_elif_else_clause( 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), - ] - )?; + if SuppressionKind::has_skip_comment(trailing_colon_comment, f.context().source()) { + SuppressedClauseHeader::ElifElse(item).fmt(f)?; } else { - text("else").fmt(f)?; + if let Some(test) = test { + write!( + f, + [ + text("elif"), + space(), + maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), + ] + )?; + } else { + text("else").fmt(f)?; + } + + text(":").fmt(f)?; } write!( f, [ - text(":"), trailing_comments(trailing_colon_comment), 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 09347947d8e1ce..27ba2b0e832ad7 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_match.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_match.rs @@ -1,11 +1,14 @@ 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, trailing_comments, SourceComment, SuppressionKind, +}; use crate::context::{NodeLevel, WithNodeLevel}; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; +use crate::verbatim::SuppressedClauseHeader; use crate::FormatNodeRule; #[derive(Default)] @@ -25,16 +28,21 @@ 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, - [ - text("match"), - space(), - maybe_parenthesize_expression(subject, item, Parenthesize::IfBreaks), - text(":"), - trailing_comments(dangling_item_comments) - ] - )?; + if SuppressionKind::has_skip_comment(dangling_item_comments, f.context().source()) { + SuppressedClauseHeader::Match(item).fmt(f)?; + } else { + write!( + f, + [ + 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 7213a3a027cac8..f80f465652bd0f 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_try.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_try.rs @@ -1,12 +1,13 @@ 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::SourceComment; use crate::comments::{leading_alternate_branch_comments, trailing_comments}; +use crate::comments::{SourceComment, SuppressionKind}; use crate::other::except_handler_except_handler::ExceptHandlerKind; use crate::prelude::*; use crate::statement::{FormatRefWithRule, Stmt}; +use crate::verbatim::{OrElseParent, SuppressedClauseHeader}; use crate::{FormatNodeRule, PyFormatter}; #[derive(Default)] @@ -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,45 @@ 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); + + leading_alternate_branch_comments(leading_case_comments, previous_node).fmt(f)?; + + if SuppressionKind::has_skip_comment(trailing_case_comments, f.context().source()) { + let header = match kind { + CaseKind::Try => SuppressedClauseHeader::Try(try_statement), + CaseKind::Else => SuppressedClauseHeader::OrElse(OrElseParent::Try(try_statement)), + CaseKind::Finally => SuppressedClauseHeader::TryFinally(try_statement), + }; + + header.fmt(f)?; + } else { + write!(f, [text(kind.keyword()), text(":")])?; + } write!( f, [ - leading_alternate_branch_comments(&case_comments[..partition_point], previous_node), - text(name), - text(":"), - trailing_comments(&case_comments[partition_point..]), + trailing_comments(trailing_case_comments), block_indent(&body.format()) ] )?; @@ -131,3 +152,19 @@ fn format_case<'a>( (None, dangling_comments) }) } + +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 0508278667ddc1..a99e83c70d2073 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_while.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_while.rs @@ -2,10 +2,13 @@ use ruff_formatter::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::{ + leading_alternate_branch_comments, trailing_comments, SourceComment, SuppressionKind, +}; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; +use crate::verbatim::{OrElseParent, SuppressedClauseHeader}; use crate::FormatNodeRule; #[derive(Default)] @@ -30,13 +33,23 @@ impl FormatNodeRule for FormatStmtWhile { let (trailing_condition_comments, or_else_comments) = dangling_comments.split_at(or_else_comments_start); + if SuppressionKind::has_skip_comment(trailing_condition_comments, f.context().source()) { + SuppressedClauseHeader::While(item).fmt(f)?; + } else { + write!( + f, + [ + text("while"), + space(), + maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), + text(":"), + ] + )?; + } + write!( f, [ - text("while"), - space(), - maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), - text(":"), trailing_comments(trailing_condition_comments), block_indent(&body.format()) ] @@ -49,14 +62,17 @@ impl FormatNodeRule for FormatStmtWhile { or_else_comments.partition_point(|comment| comment.line_position().is_own_line()); let (leading, trailing) = or_else_comments.split_at(trailing_start); + leading_alternate_branch_comments(leading, body.last()).fmt(f)?; + + if SuppressionKind::has_skip_comment(trailing, f.context().source()) { + SuppressedClauseHeader::OrElse(OrElseParent::While(item)).fmt(f)?; + } else { + text("else:").fmt(f)?; + } + write!( f, - [ - leading_alternate_branch_comments(leading, body.last()), - text("else:"), - trailing_comments(trailing), - block_indent(&orelse.format()) - ] + [trailing_comments(trailing), 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 fb08c27f768f99..18003407ed7ced 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::{trailing_comments, SourceComment, SuppressionKind}; use crate::expression::parentheses::{ in_parentheses_only_soft_line_break_or_space, optional_parentheses, parenthesized, }; use crate::prelude::*; +use crate::verbatim::SuppressedClauseHeader; 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,40 +39,55 @@ 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() - }); + if SuppressionKind::has_skip_comment(colon_comments, f.context().source()) { + SuppressedClauseHeader::With(item).fmt(f)?; + } else { + 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) + 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 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()); + } else { + f.join_with(format_args![text(","), space()]) + .entries(item.items.iter().formatted()) + .finish()?; + } - 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()?; + text(":").fmt(f)?; } write!( f, [ - text(":"), trailing_comments(colon_comments), block_indent(&item.body.format()) ] diff --git a/crates/ruff_python_formatter/src/verbatim.rs b/crates/ruff_python_formatter/src/verbatim.rs index c96e8f373636d3..533cc345d230ef 100644 --- a/crates/ruff_python_formatter/src/verbatim.rs +++ b/crates/ruff_python_formatter/src/verbatim.rs @@ -5,10 +5,13 @@ use unicode_width::UnicodeWidthStr; use ruff_formatter::{write, FormatError}; use ruff_python_ast::node::AnyNodeRef; -use ruff_python_ast::{Ranged, Stmt}; +use ruff_python_ast::{ + ElifElseClause, ExceptHandlerExceptHandler, MatchCase, Ranged, Stmt, StmtClassDef, StmtFor, + StmtFunctionDef, StmtIf, StmtMatch, StmtTry, StmtWhile, StmtWith, +}; use ruff_python_parser::lexer::{lex_starts_at, LexResult}; use ruff_python_parser::{Mode, Tok}; -use ruff_python_trivia::lines_before; +use ruff_python_trivia::{lines_before, SimpleToken, SimpleTokenKind, SimpleTokenizer}; use ruff_source_file::Locator; use ruff_text_size::{TextRange, TextSize}; @@ -929,3 +932,294 @@ impl Format> for FormatSuppressedNode<'_> { ) } } + +#[derive(Copy, Clone)] +pub(crate) enum SuppressedClauseHeader<'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(OrElseParent<'a>), +} + +impl<'a> SuppressedClauseHeader<'a> { + fn range(self, source: &str) -> FormatResult { + let keyword_range = self.keyword_range(source)?; + + let mut last_child_end = None; + + self.visit_children(&mut |child| last_child_end = Some(child.end())); + + let end = match self { + SuppressedClauseHeader::Class(class) => { + Some(last_child_end.unwrap_or(class.name.end())) + } + SuppressedClauseHeader::Function(function) => { + Some(last_child_end.unwrap_or(function.name.end())) + } + SuppressedClauseHeader::ElifElse(_) + | SuppressedClauseHeader::Try(_) + | SuppressedClauseHeader::If(_) + | SuppressedClauseHeader::TryFinally(_) + | SuppressedClauseHeader::Match(_) + | SuppressedClauseHeader::MatchCase(_) + | SuppressedClauseHeader::For(_) + | SuppressedClauseHeader::While(_) + | SuppressedClauseHeader::With(_) + | SuppressedClauseHeader::OrElse(_) => last_child_end, + + SuppressedClauseHeader::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())) + } + + // Needs to know child nodes. + // Could use normal lexer to get header end since we know that it is rare + fn visit_children(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 { + SuppressedClauseHeader::Class(class) => { + if let Some(type_params) = &class.type_params { + visit(type_params.as_ref(), visitor); + } + + if let Some(arguments) = &class.arguments { + visit(arguments.as_ref(), visitor); + } + } + SuppressedClauseHeader::Function(function) => { + visit(function.parameters.as_ref(), visitor); + if let Some(type_params) = function.type_params.as_ref() { + visit(type_params, visitor); + } + } + SuppressedClauseHeader::If(if_stmt) => { + visit(if_stmt.test.as_ref(), visitor); + } + SuppressedClauseHeader::ElifElse(clause) => { + if let Some(test) = clause.test.as_ref() { + visit(test, visitor); + } + } + + SuppressedClauseHeader::ExceptHandler(handler) => { + if let Some(ty) = handler.type_.as_deref() { + visit(ty, visitor); + } + } + SuppressedClauseHeader::Match(match_stmt) => { + visit(match_stmt.subject.as_ref(), visitor); + } + SuppressedClauseHeader::MatchCase(match_case) => { + visit(&match_case.pattern, visitor); + + if let Some(guard) = match_case.guard.as_deref() { + visit(guard, visitor); + } + } + SuppressedClauseHeader::For(for_stmt) => { + visit(for_stmt.target.as_ref(), visitor); + visit(for_stmt.iter.as_ref(), visitor); + } + SuppressedClauseHeader::While(while_stmt) => { + visit(while_stmt.test.as_ref(), visitor); + } + SuppressedClauseHeader::With(with_stmt) => { + for item in &with_stmt.items { + visit(item, visitor); + } + } + SuppressedClauseHeader::Try(_) + | SuppressedClauseHeader::TryFinally(_) + | SuppressedClauseHeader::OrElse(_) => {} + } + } + + fn keyword_range(self, source: &str) -> FormatResult { + match self { + SuppressedClauseHeader::Class(header) => { + find_keyword(header.start(), SimpleTokenKind::Class, source) + } + SuppressedClauseHeader::Function(header) => { + let keyword = if header.is_async { + SimpleTokenKind::Async + } else { + SimpleTokenKind::Def + }; + find_keyword(header.start(), keyword, source) + } + SuppressedClauseHeader::If(header) => { + find_keyword(header.start(), SimpleTokenKind::If, source) + } + SuppressedClauseHeader::ElifElse(ElifElseClause { + test: None, range, .. + }) => find_keyword(range.start(), SimpleTokenKind::Else, source), + SuppressedClauseHeader::ElifElse(ElifElseClause { + test: Some(_), + range, + .. + }) => find_keyword(range.start(), SimpleTokenKind::Elif, source), + SuppressedClauseHeader::Try(header) => { + find_keyword(header.start(), SimpleTokenKind::Try, source) + } + SuppressedClauseHeader::ExceptHandler(header) => { + find_keyword(header.start(), SimpleTokenKind::Except, source) + } + SuppressedClauseHeader::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) + } + SuppressedClauseHeader::Match(header) => { + find_keyword(header.start(), SimpleTokenKind::Match, source) + } + SuppressedClauseHeader::MatchCase(header) => { + find_keyword(header.start(), SimpleTokenKind::Case, source) + } + SuppressedClauseHeader::For(header) => { + let keyword = if header.is_async { + SimpleTokenKind::Async + } else { + SimpleTokenKind::For + }; + find_keyword(header.start(), keyword, source) + } + SuppressedClauseHeader::While(header) => { + find_keyword(header.start(), SimpleTokenKind::While, source) + } + SuppressedClauseHeader::With(header) => { + let keyword = if header.is_async { + SimpleTokenKind::Async + } else { + SimpleTokenKind::With + }; + + find_keyword(header.start(), keyword, source) + } + SuppressedClauseHeader::OrElse(header) => match header { + OrElseParent::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) + } + OrElseParent::For(StmtFor { body, .. }) + | OrElseParent::While(StmtWhile { body, .. }) => { + find_keyword(body.last().unwrap().end(), SimpleTokenKind::Else, source) + } + }, + } + } +} + +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.", + )) + } + } +} + +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.")) + } + } +} + +#[derive(Copy, Clone)] +pub(crate) enum OrElseParent<'a> { + Try(&'a StmtTry), + For(&'a StmtFor), + While(&'a StmtWhile), +} + +impl Format> for SuppressedClauseHeader<'_> { + #[cold] + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + // Write the outer comments and format the node as verbatim + write!( + f, + [verbatim_text( + self.range(f.context().source())?, + ContainsNewlines::Detect + ),] + )?; + + let comments = f.context().comments(); + self.visit_children(&mut |child| 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 cb1c1a2cf953a0..00000000000000 --- 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 00000000000000..cd6eed61deacb0 --- /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_Pattern: + return "Not found" + case NOT_YET_IMPLEMENTED_Pattern: + return "I'm a teapot" + case NOT_YET_IMPLEMENTED_Pattern: + 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_Pattern: + print(f"Y={y}") + case NOT_YET_IMPLEMENTED_Pattern: + print(f"X={x}") + case NOT_YET_IMPLEMENTED_Pattern: + print(f"X={x}, Y={y}") + case NOT_YET_IMPLEMENTED_Pattern: + 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_Pattern: + print(f"Y={y} and the point is on the y-axis.") + case NOT_YET_IMPLEMENTED_Pattern: + print(f"X={x} and the point is on the x-axis.") + case NOT_YET_IMPLEMENTED_Pattern: + print("The point is located somewhere else on the plane.") + case NOT_YET_IMPLEMENTED_Pattern: + print("Not a point") + + +match points: + case NOT_YET_IMPLEMENTED_Pattern: + 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_Pattern: + print(f"A single point {x}, {y} is in the list.") + case NOT_YET_IMPLEMENTED_Pattern: + print(f"Two points on the Y axis at {y1}, {y2} are in the list.") + case NOT_YET_IMPLEMENTED_Pattern: + 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_Pattern: + 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_Pattern: + 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 00000000000000..21bad10de4353c --- /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() +``` + + +