From 2a8d24dd4b1b0505f81d42c1be0046478e39f3ee Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Mon, 21 Aug 2023 08:02:23 +0100 Subject: [PATCH] Format function and class definitions into a single line if its body is an ellipsis (#6592) --- .../test/fixtures/ruff/statement/ellipsis.pyi | 102 +++++++++ .../fixtures/ruff/statement/top_level.pyi | 13 ++ .../other/except_handler_except_handler.rs | 4 +- .../src/other/match_case.rs | 4 +- .../src/statement/clause.rs | 56 ++++- .../src/statement/stmt_class_def.rs | 4 +- .../src/statement/stmt_for.rs | 6 +- .../src/statement/stmt_function_def.rs | 4 +- .../src/statement/stmt_if.rs | 6 +- .../src/statement/stmt_try.rs | 4 +- .../src/statement/stmt_while.rs | 6 +- .../src/statement/stmt_with.rs | 4 +- .../src/statement/suite.rs | 41 ++-- .../format@statement__ellipsis.pyi.snap | 193 ++++++++++++++++++ .../format@statement__top_level.pyi.snap | 58 +++--- 15 files changed, 445 insertions(+), 60 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ellipsis.pyi create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@statement__ellipsis.pyi.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ellipsis.pyi b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ellipsis.pyi new file mode 100644 index 0000000000..499ef0aacc --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ellipsis.pyi @@ -0,0 +1,102 @@ +"""Compound statements with no body should be written on one line.""" + +if True: + ... +elif True: + ... +else: + ... + +if True: + # comment + ... +elif True: + # comment + ... +else: + # comment + ... + +if True: + ... # comment +elif True: + ... # comment +else: + ... # comment + +for i in []: + ... +else: + ... + +for i in []: + # comment + ... +else: + # comment + ... + +for i in []: + ... # comment +else: + ... # comment + +while True: + ... +else: + ... + +while True: + # comment + ... +else: + # comment + ... + +while True: + ... # comment +else: + ... # comment + +with True: + ... + +with True: + # comment + ... + +with True: + ... # comment + +match x: + case 1: + ... + case 2: + # comment + ... + case 3: + ... # comment + +try: + ... +except: + ... +finally: + ... + +try: + # comment + ... +except: + # comment + ... +finally: + # comment + ... + +try: + ... # comment +except: + ... # comment +finally: + ... # comment \ No newline at end of file diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/top_level.pyi b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/top_level.pyi index 4c5a03d386..2f359bf108 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/top_level.pyi +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/top_level.pyi @@ -76,3 +76,16 @@ class Test2(A): def b(): ... # comment def c(): ... + +class EllipsisWithComment: + ... # comment + +def function_with_comment(): + ... # comment + +class EllispsisWithMultipleTrailing: # trailing class comment + ... # trailing ellipsis comment + +class EllipsisWithLeadingComment: + # leading + ... 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 c4c847da2f..acdf88fcd0 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 @@ -6,7 +6,7 @@ use crate::comments::SourceComment; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; -use crate::statement::clause::{clause_header, ClauseHeader}; +use crate::statement::clause::{clause_body, clause_header, ClauseHeader}; use crate::{FormatNodeRule, PyFormatter}; #[derive(Copy, Clone, Default)] @@ -86,7 +86,7 @@ impl FormatNodeRule for FormatExceptHandlerExceptHan Ok(()) }), ), - block_indent(&body.format()) + clause_body(body, dangling_comments), ] ) } diff --git a/crates/ruff_python_formatter/src/other/match_case.rs b/crates/ruff_python_formatter/src/other/match_case.rs index 008c1f325e..d638ac4a4f 100644 --- a/crates/ruff_python_formatter/src/other/match_case.rs +++ b/crates/ruff_python_formatter/src/other/match_case.rs @@ -6,7 +6,7 @@ use ruff_text_size::TextRange; use crate::comments::{leading_comments, SourceComment}; use crate::expression::parentheses::parenthesized; use crate::prelude::*; -use crate::statement::clause::{clause_header, ClauseHeader}; +use crate::statement::clause::{clause_body, clause_header, ClauseHeader}; use crate::{FormatError, FormatNodeRule, PyFormatter}; #[derive(Default)] @@ -57,7 +57,7 @@ impl FormatNodeRule for FormatMatchCase { Ok(()) }), ), - block_indent(&body.format()) + clause_body(body, dangling_item_comments), ] ) } diff --git a/crates/ruff_python_formatter/src/statement/clause.rs b/crates/ruff_python_formatter/src/statement/clause.rs index e52a342e22..465d0921d9 100644 --- a/crates/ruff_python_formatter/src/statement/clause.rs +++ b/crates/ruff_python_formatter/src/statement/clause.rs @@ -2,12 +2,13 @@ use crate::comments::{ leading_alternate_branch_comments, trailing_comments, SourceComment, SuppressionKind, }; use crate::prelude::*; +use crate::statement::suite::{contains_only_an_ellipsis, SuiteKind}; use crate::verbatim::write_suppressed_clause_header; -use ruff_formatter::{Argument, Arguments, FormatError}; +use ruff_formatter::{write, Argument, Arguments, FormatError}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::{ ElifElseClause, ExceptHandlerExceptHandler, MatchCase, Ranged, StmtClassDef, StmtFor, - StmtFunctionDef, StmtIf, StmtMatch, StmtTry, StmtWhile, StmtWith, + StmtFunctionDef, StmtIf, StmtMatch, StmtTry, StmtWhile, StmtWith, Suite, }; use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{TextRange, TextSize}; @@ -352,6 +353,57 @@ impl<'ast> Format> for FormatClauseHeader<'_, 'ast> { } } +pub(crate) struct FormatClauseBody<'a> { + body: &'a Suite, + kind: SuiteKind, + trailing_comments: &'a [SourceComment], +} + +impl<'a> FormatClauseBody<'a> { + #[must_use] + pub(crate) fn with_kind(mut self, kind: SuiteKind) -> Self { + self.kind = kind; + self + } +} + +pub(crate) fn clause_body<'a>( + body: &'a Suite, + trailing_comments: &'a [SourceComment], +) -> FormatClauseBody<'a> { + FormatClauseBody { + body, + kind: SuiteKind::default(), + trailing_comments, + } +} + +impl Format> for FormatClauseBody<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + if f.options().source_type().is_stub() + && contains_only_an_ellipsis(self.body, f.context().comments()) + && self.trailing_comments.is_empty() + { + write!( + f, + [ + space(), + self.body.format().with_options(self.kind), + hard_line_break() + ] + ) + } else { + write!( + f, + [ + trailing_comments(self.trailing_comments), + block_indent(&self.body.format().with_options(self.kind)) + ] + ) + } + } +} + /// Finds the range of `keyword` starting the search at `start_position`. Expects only comments and `(` between /// the `start_position` and the `keyword` token. fn find_keyword( 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 78b3408d71..a8f87c46ea 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -6,7 +6,7 @@ use crate::comments::{leading_comments, trailing_comments, SourceComment}; use crate::prelude::*; use crate::statement::suite::SuiteKind; -use crate::statement::clause::{clause_header, ClauseHeader}; +use crate::statement::clause::{clause_body, clause_header, ClauseHeader}; use crate::FormatNodeRule; #[derive(Default)] @@ -107,7 +107,7 @@ impl FormatNodeRule for FormatStmtClassDef { Ok(()) }), ), - block_indent(&body.format().with_options(SuiteKind::Class)) + clause_body(body, trailing_definition_comments).with_kind(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 e47065a3c3..67020e5344 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::{clause_header, ClauseHeader, ElseClause}; +use crate::statement::clause::{clause_body, clause_header, ClauseHeader, ElseClause}; use crate::FormatNodeRule; #[derive(Debug)] @@ -64,7 +64,7 @@ impl FormatNodeRule for FormatStmtFor { maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaks), ], ), - block_indent(&body.format()) + clause_body(body, trailing_condition_comments), ] )?; @@ -86,7 +86,7 @@ impl FormatNodeRule for FormatStmtFor { &text("else"), ) .with_leading_comments(leading, body.last()), - block_indent(&orelse.format()) + clause_body(orelse, trailing), ] )?; } 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 03e423b47c..764f3487c3 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -6,7 +6,7 @@ use crate::comments::SourceComment; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::{Parentheses, Parenthesize}; use crate::prelude::*; -use crate::statement::clause::{clause_header, ClauseHeader}; +use crate::statement::clause::{clause_body, clause_header, ClauseHeader}; use crate::statement::stmt_class_def::FormatDecorators; use crate::statement::suite::SuiteKind; use crate::FormatNodeRule; @@ -142,7 +142,7 @@ impl FormatNodeRule for FormatStmtFunctionDef { group(&format_inner).fmt(f) }), ), - block_indent(&body.format().with_options(SuiteKind::Function)) + clause_body(body, trailing_definition_comments).with_kind(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 0ff8a816b6..5f19c2cd6c 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_if.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_if.rs @@ -6,7 +6,7 @@ use crate::comments::SourceComment; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; -use crate::statement::clause::{clause_header, ClauseHeader}; +use crate::statement::clause::{clause_body, clause_header, ClauseHeader}; use crate::FormatNodeRule; #[derive(Default)] @@ -36,7 +36,7 @@ impl FormatNodeRule for FormatStmtIf { maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), ], ), - block_indent(&body.format()) + clause_body(body, trailing_colon_comment), ] )?; @@ -98,7 +98,7 @@ pub(crate) fn format_elif_else_clause( }), ) .with_leading_comments(leading_comments, last_node), - block_indent(&body.format()) + clause_body(body, trailing_colon_comment), ] ) } diff --git a/crates/ruff_python_formatter/src/statement/stmt_try.rs b/crates/ruff_python_formatter/src/statement/stmt_try.rs index e619496824..3c98d5531e 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_try.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_try.rs @@ -6,7 +6,7 @@ use crate::comments::leading_alternate_branch_comments; use crate::comments::SourceComment; use crate::other::except_handler_except_handler::ExceptHandlerKind; use crate::prelude::*; -use crate::statement::clause::{clause_header, ClauseHeader, ElseClause}; +use crate::statement::clause::{clause_body, clause_header, ClauseHeader, ElseClause}; use crate::statement::{FormatRefWithRule, Stmt}; use crate::{FormatNodeRule, PyFormatter}; @@ -138,7 +138,7 @@ fn format_case<'a>( [ clause_header(header, trailing_case_comments, &text(kind.keyword())) .with_leading_comments(leading_case_comments, previous_node), - block_indent(&body.format()) + clause_body(body, trailing_case_comments), ] )?; (None, rest) diff --git a/crates/ruff_python_formatter/src/statement/stmt_while.rs b/crates/ruff_python_formatter/src/statement/stmt_while.rs index 7507ac44e9..e409978b6c 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_while.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_while.rs @@ -6,7 +6,7 @@ use crate::comments::SourceComment; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; -use crate::statement::clause::{clause_header, ClauseHeader, ElseClause}; +use crate::statement::clause::{clause_body, clause_header, ClauseHeader, ElseClause}; use crate::FormatNodeRule; #[derive(Default)] @@ -43,7 +43,7 @@ impl FormatNodeRule for FormatStmtWhile { maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), ] ), - block_indent(&body.format()) + clause_body(body, trailing_condition_comments), ] )?; @@ -63,7 +63,7 @@ impl FormatNodeRule for FormatStmtWhile { &text("else") ) .with_leading_comments(leading, body.last()), - block_indent(&orelse.format()) + clause_body(orelse, trailing), ] )?; } diff --git a/crates/ruff_python_formatter/src/statement/stmt_with.rs b/crates/ruff_python_formatter/src/statement/stmt_with.rs index f789078285..55383ef564 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_with.rs @@ -11,7 +11,7 @@ use crate::expression::parentheses::{ }; use crate::other::commas; use crate::prelude::*; -use crate::statement::clause::{clause_header, ClauseHeader}; +use crate::statement::clause::{clause_body, clause_header, ClauseHeader}; use crate::{FormatNodeRule, PyFormatOptions}; #[derive(Default)] @@ -100,7 +100,7 @@ impl FormatNodeRule for FormatStmtWith { Ok(()) }) ), - block_indent(&item.body.format()) + clause_body(&item.body, colon_comments) ] ) } diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 8e46540cdc..c137dd8aa2 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -1,4 +1,4 @@ -use crate::comments::{leading_comments, trailing_comments}; +use crate::comments::{leading_comments, trailing_comments, Comments}; use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; use ruff_python_ast::helpers::is_compound_statement; use ruff_python_ast::node::AnyNodeRef; @@ -17,7 +17,7 @@ use crate::verbatim::{ }; /// Level at which the [`Suite`] appears in the source code. -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Default)] pub enum SuiteKind { /// Statements at the module level / top level TopLevel, @@ -29,6 +29,7 @@ pub enum SuiteKind { Class, /// Statements in any other body (e.g., `if` or `while`). + #[default] Other, } @@ -168,9 +169,15 @@ impl FormatRule> for FormatSuite { // ``` let class_sequences_with_ellipsis_only = preceding.as_class_def_stmt().is_some_and(|class| { - contains_only_an_ellipsis(&class.body) + contains_only_an_ellipsis( + &class.body, + f.context().comments(), + ) }) && following.as_class_def_stmt().is_some_and(|class| { - contains_only_an_ellipsis(&class.body) + contains_only_an_ellipsis( + &class.body, + f.context().comments(), + ) }); // Two subsequent functions where the preceding has an ellipsis only body @@ -180,7 +187,10 @@ impl FormatRule> for FormatSuite { // ``` let function_with_ellipsis = preceding.as_function_def_stmt().is_some_and(|function| { - contains_only_an_ellipsis(&function.body) + contains_only_an_ellipsis( + &function.body, + f.context().comments(), + ) }) && following.is_function_def_stmt(); // Don't add an empty line between two classes that have an `...` body only or after @@ -325,16 +335,19 @@ impl FormatRule> for FormatSuite { } } -/// Returns `true` if a function or class body contains only an ellipsis. -fn contains_only_an_ellipsis(body: &[Stmt]) -> bool { +/// Returns `true` if a function or class body contains only an ellipsis with no comments. +pub(crate) fn contains_only_an_ellipsis(body: &[Stmt], comments: &Comments) -> bool { match body { - [Stmt::Expr(ast::StmtExpr { value, .. })] => matches!( - value.as_ref(), - Expr::Constant(ast::ExprConstant { - value: Constant::Ellipsis, - .. - }) - ), + [Stmt::Expr(ast::StmtExpr { value, .. })] => { + let [node] = body else { return false; }; + matches!( + value.as_ref(), + Expr::Constant(ast::ExprConstant { + value: Constant::Ellipsis, + .. + }) + ) && !comments.has_leading(node) + } _ => false, } } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__ellipsis.pyi.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__ellipsis.pyi.snap new file mode 100644 index 0000000000..ebfdd97f3c --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__ellipsis.pyi.snap @@ -0,0 +1,193 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ellipsis.pyi +--- +## Input +```py +"""Compound statements with no body should be written on one line.""" + +if True: + ... +elif True: + ... +else: + ... + +if True: + # comment + ... +elif True: + # comment + ... +else: + # comment + ... + +if True: + ... # comment +elif True: + ... # comment +else: + ... # comment + +for i in []: + ... +else: + ... + +for i in []: + # comment + ... +else: + # comment + ... + +for i in []: + ... # comment +else: + ... # comment + +while True: + ... +else: + ... + +while True: + # comment + ... +else: + # comment + ... + +while True: + ... # comment +else: + ... # comment + +with True: + ... + +with True: + # comment + ... + +with True: + ... # comment + +match x: + case 1: + ... + case 2: + # comment + ... + case 3: + ... # comment + +try: + ... +except: + ... +finally: + ... + +try: + # comment + ... +except: + # comment + ... +finally: + # comment + ... + +try: + ... # comment +except: + ... # comment +finally: + ... # comment``` + +## Output +```py +"""Compound statements with no body should be written on one line.""" + +if True: ... +elif True: ... +else: ... + +if True: + # comment + ... +elif True: + # comment + ... +else: + # comment + ... + +if True: ... # comment +elif True: ... # comment +else: ... # comment + +for i in []: ... +else: ... + +for i in []: + # comment + ... +else: + # comment + ... + +for i in []: ... # comment +else: ... # comment + +while True: ... +else: ... + +while True: + # comment + ... +else: + # comment + ... + +while True: ... # comment +else: ... # comment + +with True: ... + +with True: + # comment + ... + +with True: ... # comment + +match x: + case "NOT_YET_IMPLEMENTED_PatternMatchValue": ... + case "NOT_YET_IMPLEMENTED_PatternMatchValue": + # comment + ... + case "NOT_YET_IMPLEMENTED_PatternMatchValue": ... # comment + +try: ... +except: ... +finally: ... + +try: + # comment + ... +except: + # comment + ... +finally: + # comment + ... + +try: ... # comment +except: ... # comment +finally: ... # comment +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__top_level.pyi.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__top_level.pyi.snap index 35fced5adf..2e301df56a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__top_level.pyi.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__top_level.pyi.snap @@ -82,6 +82,19 @@ class Test2(A): def b(): ... # comment def c(): ... + +class EllipsisWithComment: + ... # comment + +def function_with_comment(): + ... # comment + +class EllispsisWithMultipleTrailing: # trailing class comment + ... # trailing ellipsis comment + +class EllipsisWithLeadingComment: + # leading + ... ``` ## Output @@ -97,18 +110,13 @@ class B: def foo(): pass -class Del(expr_context): - ... -class Load(expr_context): - ... +class Del(expr_context): ... +class Load(expr_context): ... # Some comment. -class Other(expr_context): - ... -class Store(expr_context): - ... -class Foo(Bar): - ... +class Other(expr_context): ... +class Store(expr_context): ... +class Foo(Bar): ... class Baz(Qux): def __init__(self): @@ -123,29 +131,33 @@ class Quuz(Qux): def __init__(self): pass -def bar(): - ... -def baz(): - ... +def bar(): ... +def baz(): ... def quux(): """Some docstring.""" def quuz(): """Some docstring.""" -def a(): - ... +def a(): ... -class Test: - ... -class Test2(A): - ... +class Test: ... +class Test2(A): ... -def b(): - ... +def b(): ... # comment -def c(): +def c(): ... + +class EllipsisWithComment: ... # comment + +def function_with_comment(): ... # comment + +class EllispsisWithMultipleTrailing: # trailing class comment + ... # trailing ellipsis comment + +class EllipsisWithLeadingComment: + # leading ... ```