diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/compound_one_liners.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/compound_one_liners.py new file mode 100644 index 0000000000..3f4d7eec81 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/compound_one_liners.py @@ -0,0 +1,149 @@ +# Test cases for fmt: skip on compound statements that fit on one line + +# Basic single-line compound statements +def simple_func(): return "hello" # fmt: skip +if True: print("condition met") # fmt: skip +for i in range(5): print(i) # fmt: skip +while x < 10: x += 1 # fmt: skip + +# With expressions that would normally trigger formatting +def long_params(a, b, c, d, e, f, g): return a + b + c + d + e + f + g # fmt: skip +if some_very_long_condition_that_might_wrap: do_something_else_that_is_long() # fmt: skip + +# Nested compound statements (outer should be preserved) +if True: + for i in range(10): print(i) # fmt: skip + +# Multiple statements in body (should not apply - multiline) +if True: + x = 1 + y = 2 # fmt: skip + +# With decorators - decorated function on one line +@overload +def decorated_func(x: int) -> str: return str(x) # fmt: skip + +@property +def prop_method(self): return self._value # fmt: skip + +# Class definitions on one line +class SimpleClass: pass # fmt: skip +class GenericClass(Generic[T]): pass # fmt: skip + +# Try/except blocks +try: risky_operation() # fmt: skip +except ValueError: handle_error() # fmt: skip +except: handle_any_error() # fmt: skip +else: success_case() # fmt: skip +finally: cleanup() # fmt: skip + +# Match statements (Python 3.10+) +match value: + case 1: print("one") # fmt: skip + case _: print("other") # fmt: skip + +# With statements +with open("file.txt") as f: content = f.read() # fmt: skip +with context_manager() as cm: result = cm.process() # fmt: skip + +# Async variants +async def async_func(): return await some_call() # fmt: skip +async for item in async_iterator(): await process(item) # fmt: skip +async with async_context() as ctx: await ctx.work() # fmt: skip + +# Complex expressions that would normally format +def complex_expr(): return [x for x in range(100) if x % 2 == 0 and x > 50] # fmt: skip +if condition_a and condition_b or (condition_c and not condition_d): execute_complex_logic() # fmt: skip + +# Edge case: comment positioning +def func_with_comment(): # some comment + return "value" # fmt: skip + +# Edge case: multiple fmt: skip (only last one should matter) +def multiple_skip(): return "test" # fmt: skip # fmt: skip + +# Should NOT be affected (already multiline) +def multiline_func(): + return "this should format normally" + +if long_condition_that_spans \ + and continues_on_next_line: + print("multiline condition") + +# Mix of skipped and non-skipped +for i in range(10): print(f"item {i}") # fmt: skip +for j in range(5): + print(f"formatted item {j}") + +# With trailing comma that would normally be removed +def trailing_comma_func(a, b, c,): return a + b + c # fmt: skip + +# Dictionary/list comprehensions +def dict_comp(): return {k: v for k, v in items.items() if v is not None} # fmt: skip +def list_comp(): return [x * 2 for x in numbers if x > threshold_value] # fmt: skip + +# Lambda in one-liner +def with_lambda(): return lambda x, y, z: x + y + z if all([x, y, z]) else None # fmt: skip + +# String formatting that would normally be reformatted +def format_string(): return f"Hello {name}, you have {count} items in your cart totaling ${total:.2f}" # fmt: skip + +# loop else clauses +for i in range(2): print(i) # fmt: skip +else: print("this") # fmt: skip + + +while foo(): print(i) # fmt: skip +else: print("this") # fmt: skip + +# again but only the first skip +for i in range(2): print(i) # fmt: skip +else: print("this") + + +while foo(): print(i) # fmt: skip +else: print("this") + +# again but only the second skip +for i in range(2): print(i) +else: print("this") # fmt: skip + + +while foo(): print(i) +else: print("this") # fmt: skip + +# multiple statements in body +if True: print("this"); print("that") # fmt: skip + +# Examples with more comments + +try: risky_operation() # fmt: skip +# leading 1 +except ValueError: handle_error() # fmt: skip +# leading 2 +except: handle_any_error() # fmt: skip +# leading 3 +else: success_case() # fmt: skip +# leading 4 +finally: cleanup() # fmt: skip +# trailing + +# multi-line before colon (should remain as is) +if ( + long_condition +): a + b # fmt: skip + +# over-indented comment example +# See https://github.com/astral-sh/ruff/pull/20633#issuecomment-3453288910 +# and https://github.com/astral-sh/ruff/pull/21185 + +for x in it: foo() + # comment +else: bar() # fmt: skip + + +if this( + 'is a long', + # commented + 'condition' +): with_a_skip # fmt: skip 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 aefaaec182..442634896c 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 @@ -7,7 +7,7 @@ use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::preview::is_remove_parens_around_except_types_enabled; -use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; +use crate::statement::clause::{ClauseHeader, clause}; use crate::statement::suite::SuiteKind; #[derive(Copy, Clone, Default)] @@ -55,77 +55,68 @@ impl FormatNodeRule for FormatExceptHandlerExceptHan write!( f, - [ - clause_header( - ClauseHeader::ExceptHandler(item), - dangling_comments, - &format_with(|f: &mut PyFormatter| { - write!( - f, - [ - token("except"), - match except_handler_kind { - ExceptHandlerKind::Regular => None, - ExceptHandlerKind::Starred => Some(token("*")), - } - ] - )?; + [clause( + ClauseHeader::ExceptHandler(item), + &format_with(|f: &mut PyFormatter| { + write!( + f, + [ + token("except"), + match except_handler_kind { + ExceptHandlerKind::Regular => None, + ExceptHandlerKind::Starred => Some(token("*")), + } + ] + )?; - match type_.as_deref() { - // For tuples of exception types without an `as` name and on 3.14+, the - // parentheses are optional. - // - // ```py - // try: - // ... - // except BaseException, Exception: # Ok - // ... - // ``` - Some(Expr::Tuple(tuple)) - if f.options().target_version() >= PythonVersion::PY314 - && is_remove_parens_around_except_types_enabled( - f.context(), - ) - && name.is_none() => - { - write!( - f, - [ - space(), - tuple - .format() - .with_options(TupleParentheses::NeverPreserve) - ] - )?; - } - Some(type_) => { - write!( - f, - [ - space(), - maybe_parenthesize_expression( - type_, - item, - Parenthesize::IfBreaks - ) - ] - )?; - if let Some(name) = name { - write!(f, [space(), token("as"), space(), name.format()])?; - } - } - _ => {} + match type_.as_deref() { + // For tuples of exception types without an `as` name and on 3.14+, the + // parentheses are optional. + // + // ```py + // try: + // ... + // except BaseException, Exception: # Ok + // ... + // ``` + Some(Expr::Tuple(tuple)) + if f.options().target_version() >= PythonVersion::PY314 + && is_remove_parens_around_except_types_enabled(f.context()) + && name.is_none() => + { + write!( + f, + [ + space(), + tuple.format().with_options(TupleParentheses::NeverPreserve) + ] + )?; } + Some(type_) => { + write!( + f, + [ + space(), + maybe_parenthesize_expression( + type_, + item, + Parenthesize::IfBreaks + ) + ] + )?; + if let Some(name) = name { + write!(f, [space(), token("as"), space(), name.format()])?; + } + } + _ => {} + } - Ok(()) - }), - ), - clause_body( - body, - SuiteKind::other(self.last_suite_in_statement), - dangling_comments - ), - ] + Ok(()) + }), + dangling_comments, + body, + SuiteKind::other(self.last_suite_in_statement), + )] ) } } diff --git a/crates/ruff_python_formatter/src/other/match_case.rs b/crates/ruff_python_formatter/src/other/match_case.rs index 130efd01cd..3221f133db 100644 --- a/crates/ruff_python_formatter/src/other/match_case.rs +++ b/crates/ruff_python_formatter/src/other/match_case.rs @@ -5,7 +5,7 @@ use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::pattern::maybe_parenthesize_pattern; use crate::prelude::*; -use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; +use crate::statement::clause::{ClauseHeader, clause}; use crate::statement::suite::SuiteKind; #[derive(Default)] @@ -46,23 +46,18 @@ impl FormatNodeRule for FormatMatchCase { write!( f, - [ - clause_header( - ClauseHeader::MatchCase(item), - dangling_item_comments, - &format_args![ - token("case"), - space(), - maybe_parenthesize_pattern(pattern, item), - format_guard - ], - ), - clause_body( - body, - SuiteKind::other(self.last_suite_in_statement), - dangling_item_comments - ), - ] + [clause( + ClauseHeader::MatchCase(item), + &format_args![ + token("case"), + space(), + maybe_parenthesize_pattern(pattern, item), + format_guard + ], + dangling_item_comments, + body, + SuiteKind::other(self.last_suite_in_statement), + )] ) } } diff --git a/crates/ruff_python_formatter/src/statement/clause.rs b/crates/ruff_python_formatter/src/statement/clause.rs index 1554c30d0f..70b092567a 100644 --- a/crates/ruff_python_formatter/src/statement/clause.rs +++ b/crates/ruff_python_formatter/src/statement/clause.rs @@ -5,11 +5,12 @@ use ruff_python_ast::{ StmtIf, StmtMatch, StmtTry, StmtWhile, StmtWith, Suite, }; use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer}; +use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::comments::{SourceComment, leading_alternate_branch_comments, trailing_comments}; use crate::statement::suite::{SuiteKind, as_only_an_ellipsis}; -use crate::verbatim::write_suppressed_clause_header; +use crate::verbatim::{verbatim_text, write_suppressed_clause_header}; use crate::{has_skip_comment, prelude::*}; /// The header of a compound statement clause. @@ -36,7 +37,41 @@ pub(crate) enum ClauseHeader<'a> { OrElse(ElseClause<'a>), } -impl ClauseHeader<'_> { +impl<'a> ClauseHeader<'a> { + /// Returns the last child in the clause body immediately following this clause header. + /// + /// For most clauses, this is the last statement in + /// the primary body. For clauses like `try`, it specifically returns the last child + /// in the `try` body, not the `except`/`else`/`finally` clauses. + /// + /// This is similar to [`ruff_python_ast::AnyNodeRef::last_child_in_body`] + /// but restricted to the clause. + pub(crate) fn last_child_in_clause(self) -> Option> { + match self { + ClauseHeader::Class(StmtClassDef { body, .. }) + | ClauseHeader::Function(StmtFunctionDef { body, .. }) + | ClauseHeader::If(StmtIf { body, .. }) + | ClauseHeader::ElifElse(ElifElseClause { body, .. }) + | ClauseHeader::Try(StmtTry { body, .. }) + | ClauseHeader::MatchCase(MatchCase { body, .. }) + | ClauseHeader::For(StmtFor { body, .. }) + | ClauseHeader::While(StmtWhile { body, .. }) + | ClauseHeader::With(StmtWith { body, .. }) + | ClauseHeader::ExceptHandler(ExceptHandlerExceptHandler { body, .. }) + | ClauseHeader::OrElse( + ElseClause::Try(StmtTry { orelse: body, .. }) + | ElseClause::For(StmtFor { orelse: body, .. }) + | ElseClause::While(StmtWhile { orelse: body, .. }), + ) + | ClauseHeader::TryFinally(StmtTry { + finalbody: body, .. + }) => body.last().map(AnyNodeRef::from), + ClauseHeader::Match(StmtMatch { cases, .. }) => cases + .last() + .and_then(|case| case.body.last().map(AnyNodeRef::from)), + } + } + /// 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)?; @@ -338,6 +373,28 @@ impl ClauseHeader<'_> { } } +impl<'a> From> for AnyNodeRef<'a> { + fn from(value: ClauseHeader<'a>) -> Self { + match value { + ClauseHeader::Class(stmt_class_def) => stmt_class_def.into(), + ClauseHeader::Function(stmt_function_def) => stmt_function_def.into(), + ClauseHeader::If(stmt_if) => stmt_if.into(), + ClauseHeader::ElifElse(elif_else_clause) => elif_else_clause.into(), + ClauseHeader::Try(stmt_try) => stmt_try.into(), + ClauseHeader::ExceptHandler(except_handler_except_handler) => { + except_handler_except_handler.into() + } + ClauseHeader::TryFinally(stmt_try) => stmt_try.into(), + ClauseHeader::Match(stmt_match) => stmt_match.into(), + ClauseHeader::MatchCase(match_case) => match_case.into(), + ClauseHeader::For(stmt_for) => stmt_for.into(), + ClauseHeader::While(stmt_while) => stmt_while.into(), + ClauseHeader::With(stmt_with) => stmt_with.into(), + ClauseHeader::OrElse(else_clause) => else_clause.into(), + } + } +} + #[derive(Copy, Clone)] pub(crate) enum ElseClause<'a> { Try(&'a StmtTry), @@ -345,6 +402,16 @@ pub(crate) enum ElseClause<'a> { While(&'a StmtWhile), } +impl<'a> From> for AnyNodeRef<'a> { + fn from(value: ElseClause<'a>) -> Self { + match value { + ElseClause::Try(stmt_try) => stmt_try.into(), + ElseClause::For(stmt_for) => stmt_for.into(), + ElseClause::While(stmt_while) => stmt_while.into(), + } + } +} + pub(crate) struct FormatClauseHeader<'a, 'ast> { header: ClauseHeader<'a>, /// How to format the clause header @@ -378,22 +445,6 @@ where } } -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 { @@ -423,13 +474,13 @@ impl<'ast> Format> for FormatClauseHeader<'_, 'ast> { } } -pub(crate) struct FormatClauseBody<'a> { +struct FormatClauseBody<'a> { body: &'a Suite, kind: SuiteKind, trailing_comments: &'a [SourceComment], } -pub(crate) fn clause_body<'a>( +fn clause_body<'a>( body: &'a Suite, kind: SuiteKind, trailing_comments: &'a [SourceComment], @@ -465,6 +516,84 @@ impl Format> for FormatClauseBody<'_> { } } +pub(crate) struct FormatClause<'a, 'ast> { + header: ClauseHeader<'a>, + /// How to format the clause header + 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], + body: &'a Suite, + kind: SuiteKind, +} + +impl<'a, 'ast> FormatClause<'a, 'ast> { + /// 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 + } + + fn clause_header(&self) -> FormatClauseHeader<'a, 'ast> { + FormatClauseHeader { + header: self.header, + formatter: self.header_formatter, + leading_comments: self.leading_comments, + trailing_colon_comment: self.trailing_colon_comment, + } + } + + fn clause_body(&self) -> FormatClauseBody<'a> { + clause_body(self.body, self.kind, self.trailing_colon_comment) + } +} + +/// Formats a clause, handling the case where the compound +/// statement lies on a single line with `# fmt: skip` and +/// should be suppressed. +pub(crate) fn clause<'a, 'ast, Content>( + header: ClauseHeader<'a>, + header_formatter: &'a Content, + trailing_colon_comment: &'a [SourceComment], + body: &'a Suite, + kind: SuiteKind, +) -> FormatClause<'a, 'ast> +where + Content: Format>, +{ + FormatClause { + header, + header_formatter: Argument::new(header_formatter), + leading_comments: None, + trailing_colon_comment, + body, + kind, + } +} + +impl<'ast> Format> for FormatClause<'_, 'ast> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + match should_suppress_clause(self, f)? { + SuppressClauseHeader::Yes { + last_child_in_clause, + } => write_suppressed_clause(self, f, last_child_in_clause), + SuppressClauseHeader::No => { + write!(f, [self.clause_header(), self.clause_body()]) + } + } + } +} + /// Finds the range of `keyword` starting the search at `start_position`. /// /// If the start position is at the end of the previous statement, the @@ -587,3 +716,96 @@ fn colon_range(after_keyword_or_condition: TextSize, source: &str) -> FormatResu } } } + +fn should_suppress_clause<'a>( + clause: &FormatClause<'a, '_>, + f: &mut Formatter>, +) -> FormatResult> { + let source = f.context().source(); + + let Some(last_child_in_clause) = clause.header.last_child_in_clause() else { + return Ok(SuppressClauseHeader::No); + }; + + // Early return if we don't have a skip comment + // to avoid computing header range in the common case + if !has_skip_comment( + f.context().comments().trailing(last_child_in_clause), + source, + ) { + return Ok(SuppressClauseHeader::No); + } + + let clause_start = clause.header.range(source)?.end(); + + let clause_range = TextRange::new(clause_start, last_child_in_clause.end()); + + // Only applies to clauses on a single line + if source.contains_line_break(clause_range) { + return Ok(SuppressClauseHeader::No); + } + + Ok(SuppressClauseHeader::Yes { + last_child_in_clause, + }) +} + +#[cold] +fn write_suppressed_clause( + clause: &FormatClause, + f: &mut Formatter>, + last_child_in_clause: AnyNodeRef, +) -> FormatResult<()> { + if let Some((leading_comments, last_node)) = clause.leading_comments { + leading_alternate_branch_comments(leading_comments, last_node).fmt(f)?; + } + + let header = clause.header; + let clause_start = header.first_keyword_range(f.context().source())?.start(); + + let comments = f.context().comments().clone(); + + let clause_end = last_child_in_clause.end(); + + // Write the outer comments and format the node as verbatim + write!( + f, + [ + source_position(clause_start), + verbatim_text(TextRange::new(clause_start, clause_end)), + source_position(clause_end), + trailing_comments(comments.trailing(last_child_in_clause)), + hard_line_break() + ] + )?; + + // We mark comments in the header as formatted as in + // the implementation of [`write_suppressed_clause_header`]. + // + // Note that the header may be multi-line and contain + // various comments since we only require that the range + // starting at the _colon_ and ending at the `# fmt: skip` + // fits on one line. + header.visit(&mut |child| { + for comment in comments.leading_trailing(child) { + comment.mark_formatted(); + } + comments.mark_verbatim_node_comments_formatted(child); + }); + + // Similarly we mark the comments in the body as formatted. + // Note that the trailing comments for the last child in the + // body have already been handled above. + for stmt in clause.body { + comments.mark_verbatim_node_comments_formatted(stmt.into()); + } + + Ok(()) +} + +enum SuppressClauseHeader<'a> { + No, + Yes { + last_child_in_clause: AnyNodeRef<'a>, + }, +} 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 9e84c76580..941fc43aa3 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -8,7 +8,7 @@ use crate::comments::format::{ }; use crate::comments::{SourceComment, leading_comments, trailing_comments}; use crate::prelude::*; -use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; +use crate::statement::clause::{ClauseHeader, clause}; use crate::statement::suite::SuiteKind; #[derive(Default)] @@ -65,9 +65,8 @@ impl FormatNodeRule for FormatStmtClassDef { decorators: decorator_list, leading_definition_comments, }, - clause_header( + clause( ClauseHeader::Class(item), - trailing_definition_comments, &format_with(|f| { write!(f, [token("class"), space(), name.format()])?; @@ -132,8 +131,10 @@ impl FormatNodeRule for FormatStmtClassDef { Ok(()) }), + trailing_definition_comments, + body, + SuiteKind::Class, ), - clause_body(body, SuiteKind::Class, trailing_definition_comments), ] )?; diff --git a/crates/ruff_python_formatter/src/statement/stmt_for.rs b/crates/ruff_python_formatter/src/statement/stmt_for.rs index 17517fc20a..d8e1cd082e 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_for.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_for.rs @@ -6,7 +6,7 @@ use crate::expression::expr_tuple::TupleParentheses; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; -use crate::statement::clause::{ClauseHeader, ElseClause, clause_body, clause_header}; +use crate::statement::clause::{ClauseHeader, ElseClause, clause}; use crate::statement::suite::SuiteKind; #[derive(Debug)] @@ -50,27 +50,22 @@ impl FormatNodeRule for FormatStmtFor { write!( f, - [ - clause_header( - ClauseHeader::For(item), - trailing_condition_comments, - &format_args![ - is_async.then_some(format_args![token("async"), space()]), - token("for"), - space(), - ExprTupleWithoutParentheses(target), - space(), - token("in"), - space(), - maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaks), - ], - ), - clause_body( - body, - SuiteKind::other(orelse.is_empty()), - trailing_condition_comments - ), - ] + [clause( + ClauseHeader::For(item), + &format_args![ + is_async.then_some(format_args![token("async"), space()]), + token("for"), + space(), + ExprTupleWithoutParentheses(target), + space(), + token("in"), + space(), + maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaks), + ], + trailing_condition_comments, + body, + SuiteKind::other(orelse.is_empty()), + ),] )?; if orelse.is_empty() { @@ -84,15 +79,14 @@ impl FormatNodeRule for FormatStmtFor { write!( f, - [ - clause_header( - ClauseHeader::OrElse(ElseClause::For(item)), - trailing, - &token("else"), - ) - .with_leading_comments(leading, body.last()), - clause_body(orelse, SuiteKind::other(true), trailing), - ] + [clause( + ClauseHeader::OrElse(ElseClause::For(item)), + &token("else"), + trailing, + orelse, + SuiteKind::other(true), + ) + .with_leading_comments(leading, body.last())] )?; } 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 86e2003efd..61b333c72d 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -4,7 +4,7 @@ use crate::comments::format::{ use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::{Parentheses, Parenthesize}; use crate::prelude::*; -use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; +use crate::statement::clause::{ClauseHeader, clause}; use crate::statement::stmt_class_def::FormatDecorators; use crate::statement::suite::SuiteKind; use ruff_formatter::write; @@ -60,12 +60,13 @@ impl FormatNodeRule for FormatStmtFunctionDef { decorators: decorator_list, leading_definition_comments, }, - clause_header( + clause( ClauseHeader::Function(item), - trailing_definition_comments, &format_with(|f| format_function_header(f, item)), + trailing_definition_comments, + body, + SuiteKind::Function, ), - clause_body(body, SuiteKind::Function, trailing_definition_comments), ] )?; diff --git a/crates/ruff_python_formatter/src/statement/stmt_if.rs b/crates/ruff_python_formatter/src/statement/stmt_if.rs index 9b080ddc6e..899c03c054 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_if.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_if.rs @@ -5,7 +5,7 @@ use ruff_text_size::Ranged; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; -use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; +use crate::statement::clause::{ClauseHeader, clause}; use crate::statement::suite::SuiteKind; #[derive(Default)] @@ -26,22 +26,17 @@ impl FormatNodeRule for FormatStmtIf { write!( f, - [ - clause_header( - ClauseHeader::If(item), - trailing_colon_comment, - &format_args![ - token("if"), - space(), - maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), - ], - ), - clause_body( - body, - SuiteKind::other(elif_else_clauses.is_empty()), - trailing_colon_comment - ), - ] + [clause( + ClauseHeader::If(item), + &format_args![ + token("if"), + space(), + maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), + ], + trailing_colon_comment, + body, + SuiteKind::other(elif_else_clauses.is_empty()), + )] )?; let mut last_node = body.last().unwrap().into(); @@ -81,9 +76,8 @@ pub(crate) fn format_elif_else_clause( write!( f, [ - clause_header( + clause( ClauseHeader::ElifElse(item), - trailing_colon_comment, &format_with(|f: &mut PyFormatter| { f.options() .source_map_generation() @@ -103,9 +97,11 @@ pub(crate) fn format_elif_else_clause( token("else").fmt(f) } }), + trailing_colon_comment, + body, + suite_kind, ) .with_leading_comments(leading_comments, last_node), - clause_body(body, suite_kind, trailing_colon_comment), f.options() .source_map_generation() .is_enabled() diff --git a/crates/ruff_python_formatter/src/statement/stmt_try.rs b/crates/ruff_python_formatter/src/statement/stmt_try.rs index 411be5b339..2ea085b67d 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_try.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_try.rs @@ -9,7 +9,7 @@ use crate::other::except_handler_except_handler::{ ExceptHandlerKind, FormatExceptHandlerExceptHandler, }; use crate::prelude::*; -use crate::statement::clause::{ClauseHeader, ElseClause, clause_body, clause_header}; +use crate::statement::clause::{ClauseHeader, ElseClause, clause}; use crate::statement::suite::SuiteKind; use crate::statement::{FormatRefWithRule, Stmt}; @@ -154,15 +154,14 @@ fn format_case<'a>( write!( f, - [ - clause_header(header, trailing_case_comments, &token(kind.keyword())) - .with_leading_comments(leading_case_comments, previous_node), - clause_body( - body, - SuiteKind::other(last_suite_in_statement), - trailing_case_comments - ), - ] + [clause( + header, + &token(kind.keyword()), + trailing_case_comments, + body, + SuiteKind::other(last_suite_in_statement), + ) + .with_leading_comments(leading_case_comments, previous_node),] )?; (Some(last), rest) } else { diff --git a/crates/ruff_python_formatter/src/statement/stmt_while.rs b/crates/ruff_python_formatter/src/statement/stmt_while.rs index dc5593ded0..fdcc2e4c9a 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_while.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_while.rs @@ -5,7 +5,7 @@ use ruff_text_size::Ranged; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; -use crate::statement::clause::{ClauseHeader, ElseClause, clause_body, clause_header}; +use crate::statement::clause::{ClauseHeader, ElseClause, clause}; use crate::statement::suite::SuiteKind; #[derive(Default)] @@ -33,22 +33,17 @@ impl FormatNodeRule for FormatStmtWhile { write!( f, - [ - clause_header( - ClauseHeader::While(item), - trailing_condition_comments, - &format_args![ - token("while"), - space(), - maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), - ] - ), - clause_body( - body, - SuiteKind::other(orelse.is_empty()), - trailing_condition_comments - ), - ] + [clause( + ClauseHeader::While(item), + &format_args![ + token("while"), + space(), + maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), + ], + trailing_condition_comments, + body, + SuiteKind::other(orelse.is_empty()), + )] )?; if !orelse.is_empty() { @@ -60,15 +55,14 @@ impl FormatNodeRule for FormatStmtWhile { write!( f, - [ - clause_header( - ClauseHeader::OrElse(ElseClause::While(item)), - trailing, - &token("else") - ) - .with_leading_comments(leading, body.last()), - clause_body(orelse, SuiteKind::other(true), trailing), - ] + [clause( + ClauseHeader::OrElse(ElseClause::While(item)), + &token("else"), + trailing, + orelse, + SuiteKind::other(true), + ) + .with_leading_comments(leading, body.last()),] )?; } diff --git a/crates/ruff_python_formatter/src/statement/stmt_with.rs b/crates/ruff_python_formatter/src/statement/stmt_with.rs index 4c9cbc0b3f..3ef8e52a23 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_with.rs @@ -13,7 +13,7 @@ use crate::expression::parentheses::{ use crate::other::commas; use crate::other::with_item::WithItemLayout; use crate::prelude::*; -use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; +use crate::statement::clause::{ClauseHeader, clause}; use crate::statement::suite::SuiteKind; #[derive(Default)] @@ -46,106 +46,103 @@ impl FormatNodeRule for FormatStmtWith { write!( f, - [ - clause_header( - ClauseHeader::With(with_stmt), - colon_comments, - &format_with(|f| { - write!( - f, - [ - with_stmt - .is_async - .then_some(format_args![token("async"), space()]), - token("with"), - space() - ] - )?; + [clause( + ClauseHeader::With(with_stmt), + &format_with(|f| { + write!( + f, + [ + with_stmt + .is_async + .then_some(format_args![token("async"), space()]), + token("with"), + space() + ] + )?; - let layout = WithItemsLayout::from_statement( - with_stmt, - f.context(), - parenthesized_comments, - )?; + let layout = WithItemsLayout::from_statement( + with_stmt, + f.context(), + parenthesized_comments, + )?; - match layout { - WithItemsLayout::SingleWithTarget(single) => { - optional_parentheses(&single.format().with_options( - WithItemLayout::ParenthesizedContextManagers { single: true }, - )) - .fmt(f) - } - - WithItemsLayout::SingleWithoutTarget(single) => single - .format() - .with_options(WithItemLayout::SingleWithoutTarget) - .fmt(f), - - WithItemsLayout::SingleParenthesizedContextManager(single) => single - .format() - .with_options(WithItemLayout::SingleParenthesizedContextManager) - .fmt(f), - - WithItemsLayout::ParenthesizeIfExpands => { - parenthesize_if_expands(&format_with(|f| { - let mut joiner = f.join_comma_separated( - with_stmt.body.first().unwrap().start(), - ); - - for item in &with_stmt.items { - joiner.entry_with_line_separator( - item, - &item.format().with_options( - WithItemLayout::ParenthesizedContextManagers { - single: with_stmt.items.len() == 1, - }, - ), - soft_line_break_or_space(), - ); - } - joiner.finish() - })) - .fmt(f) - } - - WithItemsLayout::Python38OrOlder => f - .join_with(format_args![token(","), space()]) - .entries(with_stmt.items.iter().map(|item| { - item.format().with_options(WithItemLayout::Python38OrOlder { - single: with_stmt.items.len() == 1, - }) - })) - .finish(), - - WithItemsLayout::Parenthesized => parenthesized( - "(", - &format_with(|f: &mut PyFormatter| { - let mut joiner = f.join_comma_separated( - with_stmt.body.first().unwrap().start(), - ); - - for item in &with_stmt.items { - joiner.entry( - item, - &item.format().with_options( - WithItemLayout::ParenthesizedContextManagers { - single: with_stmt.items.len() == 1, - }, - ), - ); - } - - joiner.finish() - }), - ")", - ) - .with_dangling_comments(parenthesized_comments) - .fmt(f), + match layout { + WithItemsLayout::SingleWithTarget(single) => { + optional_parentheses(&single.format().with_options( + WithItemLayout::ParenthesizedContextManagers { single: true }, + )) + .fmt(f) } - }) - ), - clause_body(&with_stmt.body, SuiteKind::other(true), colon_comments) - ] + + WithItemsLayout::SingleWithoutTarget(single) => single + .format() + .with_options(WithItemLayout::SingleWithoutTarget) + .fmt(f), + + WithItemsLayout::SingleParenthesizedContextManager(single) => single + .format() + .with_options(WithItemLayout::SingleParenthesizedContextManager) + .fmt(f), + + WithItemsLayout::ParenthesizeIfExpands => { + parenthesize_if_expands(&format_with(|f| { + let mut joiner = + f.join_comma_separated(with_stmt.body.first().unwrap().start()); + + for item in &with_stmt.items { + joiner.entry_with_line_separator( + item, + &item.format().with_options( + WithItemLayout::ParenthesizedContextManagers { + single: with_stmt.items.len() == 1, + }, + ), + soft_line_break_or_space(), + ); + } + joiner.finish() + })) + .fmt(f) + } + + WithItemsLayout::Python38OrOlder => f + .join_with(format_args![token(","), space()]) + .entries(with_stmt.items.iter().map(|item| { + item.format().with_options(WithItemLayout::Python38OrOlder { + single: with_stmt.items.len() == 1, + }) + })) + .finish(), + + WithItemsLayout::Parenthesized => parenthesized( + "(", + &format_with(|f: &mut PyFormatter| { + let mut joiner = + f.join_comma_separated(with_stmt.body.first().unwrap().start()); + + for item in &with_stmt.items { + joiner.entry( + item, + &item.format().with_options( + WithItemLayout::ParenthesizedContextManagers { + single: with_stmt.items.len() == 1, + }, + ), + ); + } + + joiner.finish() + }), + ")", + ) + .with_dangling_comments(parenthesized_comments) + .fmt(f), + } + }), + colon_comments, + &with_stmt.body, + SuiteKind::other(true), + )] ) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__fmtskip10.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__fmtskip10.py.snap index 81404baffc..4590f43c7f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__fmtskip10.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__fmtskip10.py.snap @@ -20,24 +20,16 @@ b = [c for c in "A very long string that would normally generate some kind of co ```diff --- Black +++ Ruff -@@ -1,8 +1,14 @@ --def foo(): return "mock" # fmt: skip --if True: print("yay") # fmt: skip --for i in range(10): print(i) # fmt: skip -+def foo(): -+ return "mock" # fmt: skip +@@ -1,8 +1,10 @@ + def foo(): return "mock" # fmt: skip + + -+if True: -+ print("yay") # fmt: skip -+for i in range(10): -+ print(i) # fmt: skip + if True: print("yay") # fmt: skip + for i in range(10): print(i) # fmt: skip -j = 1 # fmt: skip --while j < 10: j += 1 # fmt: skip +j = 1 # fmt: skip -+while j < 10: -+ j += 1 # fmt: skip + while j < 10: j += 1 # fmt: skip -b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip +b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip @@ -46,18 +38,14 @@ b = [c for c in "A very long string that would normally generate some kind of co ## Ruff Output ```python -def foo(): - return "mock" # fmt: skip +def foo(): return "mock" # fmt: skip -if True: - print("yay") # fmt: skip -for i in range(10): - print(i) # fmt: skip +if True: print("yay") # fmt: skip +for i in range(10): print(i) # fmt: skip j = 1 # fmt: skip -while j < 10: - j += 1 # fmt: skip +while j < 10: j += 1 # fmt: skip b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__compound_one_liners.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__compound_one_liners.py.snap new file mode 100644 index 0000000000..fc6365c787 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__compound_one_liners.py.snap @@ -0,0 +1,341 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/compound_one_liners.py +--- +## Input +```python +# Test cases for fmt: skip on compound statements that fit on one line + +# Basic single-line compound statements +def simple_func(): return "hello" # fmt: skip +if True: print("condition met") # fmt: skip +for i in range(5): print(i) # fmt: skip +while x < 10: x += 1 # fmt: skip + +# With expressions that would normally trigger formatting +def long_params(a, b, c, d, e, f, g): return a + b + c + d + e + f + g # fmt: skip +if some_very_long_condition_that_might_wrap: do_something_else_that_is_long() # fmt: skip + +# Nested compound statements (outer should be preserved) +if True: + for i in range(10): print(i) # fmt: skip + +# Multiple statements in body (should not apply - multiline) +if True: + x = 1 + y = 2 # fmt: skip + +# With decorators - decorated function on one line +@overload +def decorated_func(x: int) -> str: return str(x) # fmt: skip + +@property +def prop_method(self): return self._value # fmt: skip + +# Class definitions on one line +class SimpleClass: pass # fmt: skip +class GenericClass(Generic[T]): pass # fmt: skip + +# Try/except blocks +try: risky_operation() # fmt: skip +except ValueError: handle_error() # fmt: skip +except: handle_any_error() # fmt: skip +else: success_case() # fmt: skip +finally: cleanup() # fmt: skip + +# Match statements (Python 3.10+) +match value: + case 1: print("one") # fmt: skip + case _: print("other") # fmt: skip + +# With statements +with open("file.txt") as f: content = f.read() # fmt: skip +with context_manager() as cm: result = cm.process() # fmt: skip + +# Async variants +async def async_func(): return await some_call() # fmt: skip +async for item in async_iterator(): await process(item) # fmt: skip +async with async_context() as ctx: await ctx.work() # fmt: skip + +# Complex expressions that would normally format +def complex_expr(): return [x for x in range(100) if x % 2 == 0 and x > 50] # fmt: skip +if condition_a and condition_b or (condition_c and not condition_d): execute_complex_logic() # fmt: skip + +# Edge case: comment positioning +def func_with_comment(): # some comment + return "value" # fmt: skip + +# Edge case: multiple fmt: skip (only last one should matter) +def multiple_skip(): return "test" # fmt: skip # fmt: skip + +# Should NOT be affected (already multiline) +def multiline_func(): + return "this should format normally" + +if long_condition_that_spans \ + and continues_on_next_line: + print("multiline condition") + +# Mix of skipped and non-skipped +for i in range(10): print(f"item {i}") # fmt: skip +for j in range(5): + print(f"formatted item {j}") + +# With trailing comma that would normally be removed +def trailing_comma_func(a, b, c,): return a + b + c # fmt: skip + +# Dictionary/list comprehensions +def dict_comp(): return {k: v for k, v in items.items() if v is not None} # fmt: skip +def list_comp(): return [x * 2 for x in numbers if x > threshold_value] # fmt: skip + +# Lambda in one-liner +def with_lambda(): return lambda x, y, z: x + y + z if all([x, y, z]) else None # fmt: skip + +# String formatting that would normally be reformatted +def format_string(): return f"Hello {name}, you have {count} items in your cart totaling ${total:.2f}" # fmt: skip + +# loop else clauses +for i in range(2): print(i) # fmt: skip +else: print("this") # fmt: skip + + +while foo(): print(i) # fmt: skip +else: print("this") # fmt: skip + +# again but only the first skip +for i in range(2): print(i) # fmt: skip +else: print("this") + + +while foo(): print(i) # fmt: skip +else: print("this") + +# again but only the second skip +for i in range(2): print(i) +else: print("this") # fmt: skip + + +while foo(): print(i) +else: print("this") # fmt: skip + +# multiple statements in body +if True: print("this"); print("that") # fmt: skip + +# Examples with more comments + +try: risky_operation() # fmt: skip +# leading 1 +except ValueError: handle_error() # fmt: skip +# leading 2 +except: handle_any_error() # fmt: skip +# leading 3 +else: success_case() # fmt: skip +# leading 4 +finally: cleanup() # fmt: skip +# trailing + +# multi-line before colon (should remain as is) +if ( + long_condition +): a + b # fmt: skip + +# over-indented comment example +# See https://github.com/astral-sh/ruff/pull/20633#issuecomment-3453288910 +# and https://github.com/astral-sh/ruff/pull/21185 + +for x in it: foo() + # comment +else: bar() # fmt: skip + + +if this( + 'is a long', + # commented + 'condition' +): with_a_skip # fmt: skip +``` + +## Output +```python +# Test cases for fmt: skip on compound statements that fit on one line + +# Basic single-line compound statements +def simple_func(): return "hello" # fmt: skip + + +if True: print("condition met") # fmt: skip +for i in range(5): print(i) # fmt: skip +while x < 10: x += 1 # fmt: skip + + +# With expressions that would normally trigger formatting +def long_params(a, b, c, d, e, f, g): return a + b + c + d + e + f + g # fmt: skip + + +if some_very_long_condition_that_might_wrap: do_something_else_that_is_long() # fmt: skip + +# Nested compound statements (outer should be preserved) +if True: + for i in range(10): print(i) # fmt: skip + +# Multiple statements in body (should not apply - multiline) +if True: + x = 1 + y = 2 # fmt: skip + + +# With decorators - decorated function on one line +@overload +def decorated_func(x: int) -> str: return str(x) # fmt: skip + + +@property +def prop_method(self): return self._value # fmt: skip + + +# Class definitions on one line +class SimpleClass: pass # fmt: skip + + +class GenericClass(Generic[T]): pass # fmt: skip + + +# Try/except blocks +try: risky_operation() # fmt: skip +except ValueError: handle_error() # fmt: skip +except: handle_any_error() # fmt: skip +else: success_case() # fmt: skip +finally: cleanup() # fmt: skip + +# Match statements (Python 3.10+) +match value: + case 1: print("one") # fmt: skip + case _: print("other") # fmt: skip + +# With statements +with open("file.txt") as f: content = f.read() # fmt: skip +with context_manager() as cm: result = cm.process() # fmt: skip + + +# Async variants +async def async_func(): return await some_call() # fmt: skip + + +async for item in async_iterator(): await process(item) # fmt: skip +async with async_context() as ctx: await ctx.work() # fmt: skip + + +# Complex expressions that would normally format +def complex_expr(): return [x for x in range(100) if x % 2 == 0 and x > 50] # fmt: skip + + +if condition_a and condition_b or (condition_c and not condition_d): execute_complex_logic() # fmt: skip + + +# Edge case: comment positioning +def func_with_comment(): # some comment + return "value" # fmt: skip + + +# Edge case: multiple fmt: skip (only last one should matter) +def multiple_skip(): return "test" # fmt: skip # fmt: skip + + +# Should NOT be affected (already multiline) +def multiline_func(): + return "this should format normally" + + +if long_condition_that_spans and continues_on_next_line: + print("multiline condition") + +# Mix of skipped and non-skipped +for i in range(10): print(f"item {i}") # fmt: skip +for j in range(5): + print(f"formatted item {j}") + + +# With trailing comma that would normally be removed +def trailing_comma_func(a, b, c,): return a + b + c # fmt: skip + + +# Dictionary/list comprehensions +def dict_comp(): return {k: v for k, v in items.items() if v is not None} # fmt: skip + + +def list_comp(): return [x * 2 for x in numbers if x > threshold_value] # fmt: skip + + +# Lambda in one-liner +def with_lambda(): return lambda x, y, z: x + y + z if all([x, y, z]) else None # fmt: skip + + +# String formatting that would normally be reformatted +def format_string(): return f"Hello {name}, you have {count} items in your cart totaling ${total:.2f}" # fmt: skip + + +# loop else clauses +for i in range(2): print(i) # fmt: skip +else: print("this") # fmt: skip + + +while foo(): print(i) # fmt: skip +else: print("this") # fmt: skip + +# again but only the first skip +for i in range(2): print(i) # fmt: skip +else: + print("this") + + +while foo(): print(i) # fmt: skip +else: + print("this") + +# again but only the second skip +for i in range(2): + print(i) +else: print("this") # fmt: skip + + +while foo(): + print(i) +else: print("this") # fmt: skip + +# multiple statements in body +if True: print("this"); print("that") # fmt: skip + +# Examples with more comments + +try: risky_operation() # fmt: skip +# leading 1 +except ValueError: handle_error() # fmt: skip +# leading 2 +except: handle_any_error() # fmt: skip +# leading 3 +else: success_case() # fmt: skip +# leading 4 +finally: cleanup() # fmt: skip +# trailing + +# multi-line before colon (should remain as is) +if ( + long_condition +): a + b # fmt: skip + +# over-indented comment example +# See https://github.com/astral-sh/ruff/pull/20633#issuecomment-3453288910 +# and https://github.com/astral-sh/ruff/pull/21185 + +for x in it: + foo() +# comment +else: bar() # fmt: skip + + +if this( + 'is a long', + # commented + 'condition' +): with_a_skip # fmt: skip +``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__docstrings.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__docstrings.py.snap index d473520c8a..13d1fe8adc 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__docstrings.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_skip__docstrings.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/docstrings.py -snapshot_kind: text --- ## Input ```python