Support fmt: skip on compound statements (#6593)

This commit is contained in:
Micha Reiser 2023-08-17 08:05:41 +02:00 committed by GitHub
parent 4dc32a00d0
commit fa7442da2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1385 additions and 625 deletions

View file

@ -400,7 +400,6 @@ impl<'a> Comments<'a> {
}
/// Returns an iterator over the [leading](self#leading-comments) and [trailing comments](self#trailing-comments) of `node`.
#[allow(unused)]
pub(crate) fn leading_trailing_comments<T>(
&self,
node: T,

View file

@ -1,12 +1,14 @@
use crate::comments::{trailing_comments, SourceComment};
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::{FormatNodeRule, PyFormatter};
use ruff_formatter::FormatRuleWithOptions;
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::ExceptHandlerExceptHandler;
use crate::comments::SourceComment;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::clause::{clause_header, ClauseHeader};
use crate::{FormatNodeRule, PyFormatter};
#[derive(Copy, Clone, Default)]
pub enum ExceptHandlerKind {
#[default]
@ -49,32 +51,42 @@ impl FormatNodeRule<ExceptHandlerExceptHandler> for FormatExceptHandlerExceptHan
write!(
f,
[
text("except"),
match self.except_handler_kind {
ExceptHandlerKind::Regular => None,
ExceptHandlerKind::Starred => Some(text("*")),
}
]
)?;
clause_header(
ClauseHeader::ExceptHandler(item),
dangling_comments,
&format_with(|f| {
write!(
f,
[
text("except"),
match self.except_handler_kind {
ExceptHandlerKind::Regular => None,
ExceptHandlerKind::Starred => Some(text("*")),
}
]
)?;
if let Some(type_) = type_ {
write!(
f,
[
space(),
maybe_parenthesize_expression(type_, item, Parenthesize::IfBreaks)
]
)?;
if let Some(name) = name {
write!(f, [space(), text("as"), space(), name.format()])?;
}
}
write!(
f,
[
text(":"),
trailing_comments(dangling_comments),
block_indent(&body.format()),
if let Some(type_) = type_ {
write!(
f,
[
space(),
maybe_parenthesize_expression(
type_,
item,
Parenthesize::IfBreaks
)
]
)?;
if let Some(name) = name {
write!(f, [space(), text("as"), space(), name.format()])?;
}
}
Ok(())
}),
),
block_indent(&body.format())
]
)
}

View file

@ -3,9 +3,10 @@ use ruff_python_ast::{MatchCase, Pattern, Ranged};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::TextRange;
use crate::comments::{leading_comments, trailing_comments, SourceComment};
use crate::comments::{leading_comments, SourceComment};
use crate::expression::parentheses::parenthesized;
use crate::prelude::*;
use crate::statement::clause::{clause_header, ClauseHeader};
use crate::{FormatError, FormatNodeRule, PyFormatter};
#[derive(Default)]
@ -23,30 +24,39 @@ impl FormatNodeRule<MatchCase> for FormatMatchCase {
let comments = f.context().comments().clone();
let dangling_item_comments = comments.dangling_comments(item);
write!(f, [text("case"), space()])?;
let leading_pattern_comments = comments.leading_comments(pattern);
if !leading_pattern_comments.is_empty() {
parenthesized(
"(",
&format_args![leading_comments(leading_pattern_comments), pattern.format()],
")",
)
.fmt(f)?;
} else if is_match_case_pattern_parenthesized(item, pattern, f.context())? {
parenthesized("(", &pattern.format(), ")").fmt(f)?;
} else {
pattern.format().fmt(f)?;
}
if let Some(guard) = guard {
write!(f, [space(), text("if"), space(), guard.format()])?;
}
write!(
f,
[
text(":"),
trailing_comments(dangling_item_comments),
clause_header(
ClauseHeader::MatchCase(item),
dangling_item_comments,
&format_with(|f| {
write!(f, [text("case"), space()])?;
let leading_pattern_comments = comments.leading_comments(pattern);
if !leading_pattern_comments.is_empty() {
parenthesized(
"(",
&format_args![
leading_comments(leading_pattern_comments),
pattern.format()
],
")",
)
.fmt(f)?;
} else if is_match_case_pattern_parenthesized(item, pattern, f.context())? {
parenthesized("(", &pattern.format(), ")").fmt(f)?;
} else {
pattern.format().fmt(f)?;
}
if let Some(guard) = guard {
write!(f, [space(), text("if"), space(), guard.format()])?;
}
Ok(())
}),
),
block_indent(&body.format())
]
)

View file

@ -0,0 +1,407 @@
use crate::comments::{
leading_alternate_branch_comments, trailing_comments, SourceComment, SuppressionKind,
};
use crate::prelude::*;
use crate::verbatim::write_suppressed_clause_header;
use ruff_formatter::{Argument, Arguments, FormatError};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{
ElifElseClause, ExceptHandlerExceptHandler, MatchCase, Ranged, StmtClassDef, StmtFor,
StmtFunctionDef, StmtIf, StmtMatch, StmtTry, StmtWhile, StmtWith,
};
use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{TextRange, TextSize};
/// The header of a compound statement clause.
///
/// > A compound statement consists of one or more clauses. A clause consists of a header and a suite.
/// > The clause headers of a particular compound statement are all at the same indentation level.
/// > Each clause header begins with a uniquely identifying keyword and ends with a colon.
/// [source](https://docs.python.org/3/reference/compound_stmts.html#compound-statements)
#[derive(Copy, Clone)]
pub(crate) enum ClauseHeader<'a> {
Class(&'a StmtClassDef),
Function(&'a StmtFunctionDef),
If(&'a StmtIf),
ElifElse(&'a ElifElseClause),
Try(&'a StmtTry),
ExceptHandler(&'a ExceptHandlerExceptHandler),
TryFinally(&'a StmtTry),
Match(&'a StmtMatch),
MatchCase(&'a MatchCase),
For(&'a StmtFor),
While(&'a StmtWhile),
With(&'a StmtWith),
OrElse(ElseClause<'a>),
}
impl<'a> ClauseHeader<'a> {
/// The range from the clause keyword up to and including the final colon.
pub(crate) fn range(self, source: &str) -> FormatResult<TextRange> {
let keyword_range = self.first_keyword_range(source)?;
let mut last_child_end = None;
self.visit(&mut |child| last_child_end = Some(child.end()));
let end = match self {
ClauseHeader::Class(class) => Some(last_child_end.unwrap_or(class.name.end())),
ClauseHeader::Function(function) => Some(last_child_end.unwrap_or(function.name.end())),
ClauseHeader::ElifElse(_)
| ClauseHeader::Try(_)
| ClauseHeader::If(_)
| ClauseHeader::TryFinally(_)
| ClauseHeader::Match(_)
| ClauseHeader::MatchCase(_)
| ClauseHeader::For(_)
| ClauseHeader::While(_)
| ClauseHeader::With(_)
| ClauseHeader::OrElse(_) => last_child_end,
ClauseHeader::ExceptHandler(handler) => handler
.name
.as_ref()
.map(ruff_python_ast::Ranged::end)
.or(last_child_end),
};
let colon = colon_range(end.unwrap_or(keyword_range.end()), source)?;
Ok(TextRange::new(keyword_range.start(), colon.end()))
}
/// Visits the nodes in the case header.
pub(crate) fn visit<F>(self, visitor: &mut F)
where
F: FnMut(AnyNodeRef),
{
fn visit<'a, N, F>(node: N, visitor: &mut F)
where
N: Into<AnyNodeRef<'a>>,
F: FnMut(AnyNodeRef<'a>),
{
visitor(node.into());
}
match self {
ClauseHeader::Class(StmtClassDef {
type_params,
arguments,
range: _,
decorator_list: _,
name: _,
body: _,
}) => {
if let Some(type_params) = type_params.as_deref() {
visit(type_params, visitor);
}
if let Some(arguments) = arguments {
visit(arguments.as_ref(), visitor);
}
}
ClauseHeader::Function(StmtFunctionDef {
type_params,
parameters,
range: _,
is_async: _,
decorator_list: _,
name: _,
returns: _,
body: _,
}) => {
if let Some(type_params) = type_params.as_ref() {
visit(type_params, visitor);
}
visit(parameters.as_ref(), visitor);
}
ClauseHeader::If(StmtIf {
test,
range: _,
body: _,
elif_else_clauses: _,
}) => {
visit(test.as_ref(), visitor);
}
ClauseHeader::ElifElse(ElifElseClause {
test,
range: _,
body: _,
}) => {
if let Some(test) = test.as_ref() {
visit(test, visitor);
}
}
ClauseHeader::ExceptHandler(ExceptHandlerExceptHandler {
type_: type_expr,
range: _,
name: _,
body: _,
}) => {
if let Some(type_expr) = type_expr.as_deref() {
visit(type_expr, visitor);
}
}
ClauseHeader::Match(StmtMatch {
subject,
range: _,
cases: _,
}) => {
visit(subject.as_ref(), visitor);
}
ClauseHeader::MatchCase(MatchCase {
guard,
pattern,
range: _,
body: _,
}) => {
visit(pattern, visitor);
if let Some(guard) = guard.as_deref() {
visit(guard, visitor);
}
}
ClauseHeader::For(StmtFor {
target,
iter,
range: _,
is_async: _,
body: _,
orelse: _,
}) => {
visit(target.as_ref(), visitor);
visit(iter.as_ref(), visitor);
}
ClauseHeader::While(StmtWhile {
test,
range: _,
body: _,
orelse: _,
}) => {
visit(test.as_ref(), visitor);
}
ClauseHeader::With(StmtWith {
items,
range: _,
is_async: _,
body: _,
}) => {
for item in items {
visit(item, visitor);
}
}
ClauseHeader::Try(_) | ClauseHeader::TryFinally(_) | ClauseHeader::OrElse(_) => {}
}
}
/// Returns the range of the first keyword that marks the start of the clause header.
fn first_keyword_range(self, source: &str) -> FormatResult<TextRange> {
match self {
ClauseHeader::Class(header) => {
find_keyword(header.start(), SimpleTokenKind::Class, source)
}
ClauseHeader::Function(header) => {
let keyword = if header.is_async {
SimpleTokenKind::Async
} else {
SimpleTokenKind::Def
};
find_keyword(header.start(), keyword, source)
}
ClauseHeader::If(header) => find_keyword(header.start(), SimpleTokenKind::If, source),
ClauseHeader::ElifElse(ElifElseClause {
test: None, range, ..
}) => find_keyword(range.start(), SimpleTokenKind::Else, source),
ClauseHeader::ElifElse(ElifElseClause {
test: Some(_),
range,
..
}) => find_keyword(range.start(), SimpleTokenKind::Elif, source),
ClauseHeader::Try(header) => find_keyword(header.start(), SimpleTokenKind::Try, source),
ClauseHeader::ExceptHandler(header) => {
find_keyword(header.start(), SimpleTokenKind::Except, source)
}
ClauseHeader::TryFinally(header) => {
let last_statement = header
.orelse
.last()
.map(AnyNodeRef::from)
.or_else(|| header.handlers.last().map(AnyNodeRef::from))
.or_else(|| header.body.last().map(AnyNodeRef::from))
.unwrap();
find_keyword(last_statement.end(), SimpleTokenKind::Finally, source)
}
ClauseHeader::Match(header) => {
find_keyword(header.start(), SimpleTokenKind::Match, source)
}
ClauseHeader::MatchCase(header) => {
find_keyword(header.start(), SimpleTokenKind::Case, source)
}
ClauseHeader::For(header) => {
let keyword = if header.is_async {
SimpleTokenKind::Async
} else {
SimpleTokenKind::For
};
find_keyword(header.start(), keyword, source)
}
ClauseHeader::While(header) => {
find_keyword(header.start(), SimpleTokenKind::While, source)
}
ClauseHeader::With(header) => {
let keyword = if header.is_async {
SimpleTokenKind::Async
} else {
SimpleTokenKind::With
};
find_keyword(header.start(), keyword, source)
}
ClauseHeader::OrElse(header) => match header {
ElseClause::Try(try_stmt) => {
let last_statement = try_stmt
.handlers
.last()
.map(AnyNodeRef::from)
.or_else(|| try_stmt.body.last().map(AnyNodeRef::from))
.unwrap();
find_keyword(last_statement.end(), SimpleTokenKind::Else, source)
}
ElseClause::For(StmtFor { body, .. })
| ElseClause::While(StmtWhile { body, .. }) => {
find_keyword(body.last().unwrap().end(), SimpleTokenKind::Else, source)
}
},
}
}
}
#[derive(Copy, Clone)]
pub(crate) enum ElseClause<'a> {
Try(&'a StmtTry),
For(&'a StmtFor),
While(&'a StmtWhile),
}
pub(crate) struct FormatClauseHeader<'a, 'ast> {
header: ClauseHeader<'a>,
/// How to format the clause header
formatter: Argument<'a, PyFormatContext<'ast>>,
/// Leading comments coming before the branch, together with the previous node, if any. Only relevant
/// for alternate branches.
leading_comments: Option<(&'a [SourceComment], Option<AnyNodeRef<'a>>)>,
/// The trailing comments coming after the colon.
trailing_colon_comment: &'a [SourceComment],
}
/// Formats a clause header, handling the case where the clause header is suppressed and should not be formatted.
///
/// Calls the `formatter` to format the content of the `header`, except if the `trailing_colon_comment` is a `fmt: skip` suppression comment.
/// Takes care of formatting the `trailing_colon_comment` and adds the `:` at the end of the header.
pub(crate) fn clause_header<'a, 'ast, Content>(
header: ClauseHeader<'a>,
trailing_colon_comment: &'a [SourceComment],
formatter: &'a Content,
) -> FormatClauseHeader<'a, 'ast>
where
Content: Format<PyFormatContext<'ast>>,
{
FormatClauseHeader {
header,
formatter: Argument::new(formatter),
leading_comments: None,
trailing_colon_comment,
}
}
impl<'a> FormatClauseHeader<'a, '_> {
/// Sets the leading comments that precede an alternate branch.
#[must_use]
pub(crate) fn with_leading_comments<N>(
mut self,
comments: &'a [SourceComment],
last_node: Option<N>,
) -> Self
where
N: Into<AnyNodeRef<'a>>,
{
self.leading_comments = Some((comments, last_node.map(Into::into)));
self
}
}
impl<'ast> Format<PyFormatContext<'ast>> for FormatClauseHeader<'_, 'ast> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'ast>>) -> FormatResult<()> {
if let Some((leading_comments, last_node)) = self.leading_comments {
leading_alternate_branch_comments(leading_comments, last_node).fmt(f)?;
}
if SuppressionKind::has_skip_comment(self.trailing_colon_comment, f.context().source()) {
write_suppressed_clause_header(self.header, f)?;
} else {
f.write_fmt(Arguments::from(&self.formatter))?;
text(":").fmt(f)?;
}
trailing_comments(self.trailing_colon_comment).fmt(f)
}
}
/// Finds the range of `keyword` starting the search at `start_position`. Expects only comments and `(` between
/// the `start_position` and the `keyword` token.
fn find_keyword(
start_position: TextSize,
keyword: SimpleTokenKind,
source: &str,
) -> FormatResult<TextRange> {
let mut tokenizer = SimpleTokenizer::starts_at(start_position, source).skip_trivia();
match tokenizer.next() {
Some(token) if token.kind() == keyword => Ok(token.range()),
Some(other) => {
debug_assert!(
false,
"Expected the keyword token {keyword:?} but found the token {other:?} instead."
);
Err(FormatError::syntax_error(
"Expected the keyword token but found another token instead.",
))
}
None => {
debug_assert!(
false,
"Expected the keyword token {keyword:?} but reached the end of the source instead."
);
Err(FormatError::syntax_error(
"Expected the case header keyword token but reached the end of the source instead.",
))
}
}
}
/// Returns the range of the `:` ending the clause header or `Err` if the colon can't be found.
fn colon_range(after_keyword_or_condition: TextSize, source: &str) -> FormatResult<TextRange> {
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."))
}
}
}

View file

@ -3,6 +3,7 @@ use ruff_python_ast::Stmt;
use crate::prelude::*;
pub(super) mod clause;
pub(crate) mod stmt_ann_assign;
pub(crate) mod stmt_assert;
pub(crate) mod stmt_assign;

View file

@ -5,6 +5,8 @@ use ruff_python_trivia::lines_after_ignoring_trivia;
use crate::comments::{leading_comments, trailing_comments, SourceComment};
use crate::prelude::*;
use crate::statement::suite::SuiteKind;
use crate::statement::clause::{clause_header, ClauseHeader};
use crate::FormatNodeRule;
#[derive(Default)]
@ -30,78 +32,81 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
let (leading_definition_comments, trailing_definition_comments) =
dangling_comments.split_at(trailing_definition_comments_start);
FormatDecorators {
decorators: decorator_list,
leading_definition_comments,
}
.fmt(f)?;
write!(f, [text("class"), space(), name.format()])?;
if let Some(type_params) = type_params.as_deref() {
write!(f, [type_params.format()])?;
}
if let Some(arguments) = arguments.as_deref() {
// Drop empty the arguments node entirely (i.e., remove the parentheses) if it is empty,
// e.g., given:
// ```python
// class A():
// ...
// ```
//
// Format as:
// ```python
// class A:
// ...
// ```
//
// However, preserve any dangling end-of-line comments, e.g., given:
// ```python
// class A( # comment
// ):
// ...
//
// Format as:
// ```python
// class A: # comment
// ...
// ```
//
// However, the arguments contain any dangling own-line comments, we retain the
// parentheses, e.g., given:
// ```python
// class A( # comment
// # comment
// ):
// ...
// ```
//
// Format as:
// ```python
// class A( # comment
// # comment
// ):
// ...
// ```
if arguments.is_empty()
&& comments
.dangling_comments(arguments)
.iter()
.all(|comment| comment.line_position().is_end_of_line())
{
let dangling = comments.dangling_comments(arguments);
write!(f, [trailing_comments(dangling)])?;
} else {
write!(f, [arguments.format()])?;
}
}
write!(
f,
[
text(":"),
trailing_comments(trailing_definition_comments),
FormatDecorators {
decorators: decorator_list,
leading_definition_comments,
},
clause_header(
ClauseHeader::Class(item),
trailing_definition_comments,
&format_with(|f| {
write!(f, [text("class"), space(), name.format()])?;
if let Some(type_params) = type_params.as_deref() {
write!(f, [type_params.format()])?;
}
if let Some(arguments) = arguments.as_deref() {
// Drop empty the arguments node entirely (i.e., remove the parentheses) if it is empty,
// e.g., given:
// ```python
// class A():
// ...
// ```
//
// Format as:
// ```python
// class A:
// ...
// ```
//
// However, preserve any dangling end-of-line comments, e.g., given:
// ```python
// class A( # comment
// ):
// ...
//
// Format as:
// ```python
// class A: # comment
// ...
// ```
//
// However, the arguments contain any dangling own-line comments, we retain the
// parentheses, e.g., given:
// ```python
// class A( # comment
// # comment
// ):
// ...
// ```
//
// Format as:
// ```python
// class A( # comment
// # comment
// ):
// ...
// ```
if arguments.is_empty()
&& comments
.dangling_comments(arguments)
.iter()
.all(|comment| comment.line_position().is_end_of_line())
{
let dangling = comments.dangling_comments(arguments);
write!(f, [trailing_comments(dangling)])?;
} else {
write!(f, [arguments.format()])?;
}
}
Ok(())
}),
),
block_indent(&body.format().with_options(SuiteKind::Class))
]
)

View file

@ -1,11 +1,12 @@
use ruff_formatter::{format_args, write};
use ruff_python_ast::{Expr, Ranged, Stmt, StmtFor};
use crate::comments::{leading_alternate_branch_comments, trailing_comments, SourceComment};
use crate::comments::SourceComment;
use crate::expression::expr_tuple::TupleParentheses;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::clause::{clause_header, ClauseHeader, ElseClause};
use crate::FormatNodeRule;
#[derive(Debug)]
@ -49,16 +50,20 @@ impl FormatNodeRule<StmtFor> for FormatStmtFor {
write!(
f,
[
is_async.then_some(format_args![text("async"), space()]),
text("for"),
space(),
ExprTupleWithoutParentheses(target),
space(),
text("in"),
space(),
maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaks),
text(":"),
trailing_comments(trailing_condition_comments),
clause_header(
ClauseHeader::For(item),
trailing_condition_comments,
&format_args![
is_async.then_some(format_args![text("async"), space()]),
text("for"),
space(),
ExprTupleWithoutParentheses(target),
space(),
text("in"),
space(),
maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaks),
],
),
block_indent(&body.format())
]
)?;
@ -75,9 +80,12 @@ impl FormatNodeRule<StmtFor> for FormatStmtFor {
write!(
f,
[
leading_alternate_branch_comments(leading, body.last()),
text("else:"),
trailing_comments(trailing),
clause_header(
ClauseHeader::OrElse(ElseClause::For(item)),
trailing,
&text("else"),
)
.with_leading_comments(leading, body.last()),
block_indent(&orelse.format())
]
)?;

View file

@ -2,10 +2,11 @@ use ruff_formatter::write;
use ruff_python_ast::{Parameters, Ranged, StmtFunctionDef};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use crate::comments::{trailing_comments, SourceComment};
use crate::comments::SourceComment;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::{Parentheses, Parenthesize};
use crate::prelude::*;
use crate::statement::clause::{clause_header, ClauseHeader};
use crate::statement::stmt_class_def::FormatDecorators;
use crate::statement::suite::SuiteKind;
use crate::FormatNodeRule;
@ -35,105 +36,114 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
let (leading_definition_comments, trailing_definition_comments) =
dangling_comments.split_at(trailing_definition_comments_start);
FormatDecorators {
decorators: decorator_list,
leading_definition_comments,
}
.fmt(f)?;
if *is_async {
write!(f, [text("async"), space()])?;
}
write!(f, [text("def"), space(), name.format()])?;
if let Some(type_params) = type_params.as_ref() {
write!(f, [type_params.format()])?;
}
let format_inner = format_with(|f: &mut PyFormatter| {
write!(f, [parameters.format()])?;
if let Some(return_annotation) = returns.as_ref() {
write!(f, [space(), text("->"), space()])?;
if return_annotation.is_tuple_expr() {
let parentheses = if comments.has_leading_comments(return_annotation.as_ref()) {
Parentheses::Always
} else {
Parentheses::Never
};
write!(f, [return_annotation.format().with_options(parentheses)])?;
} else if comments.has_trailing_comments(return_annotation.as_ref()) {
// Intentionally parenthesize any return annotations with trailing comments.
// This avoids an instability in cases like:
// ```python
// def double(
// a: int
// ) -> (
// int # Hello
// ):
// pass
// ```
// If we allow this to break, it will be formatted as follows:
// ```python
// def double(
// a: int
// ) -> int: # Hello
// pass
// ```
// On subsequent formats, the `# Hello` will be interpreted as a dangling
// comment on a function, yielding:
// ```python
// def double(a: int) -> int: # Hello
// pass
// ```
// Ideally, we'd reach that final formatting in a single pass, but doing so
// requires that the parent be aware of how the child is formatted, which
// is challenging. As a compromise, we break those expressions to avoid an
// instability.
write!(
f,
[return_annotation.format().with_options(Parentheses::Always)]
)?;
} else {
write!(
f,
[maybe_parenthesize_expression(
return_annotation,
item,
if empty_parameters(parameters, f.context().source()) {
// If the parameters are empty, add parentheses if the return annotation
// breaks at all.
Parenthesize::IfBreaksOrIfRequired
} else {
// Otherwise, use our normal rules for parentheses, which allows us to break
// like:
// ```python
// def f(
// x,
// ) -> Tuple[
// int,
// int,
// ]:
// ...
// ```
Parenthesize::IfBreaks
},
)]
)?;
}
}
Ok(())
});
write!(f, [group(&format_inner)])?;
write!(
f,
[
text(":"),
trailing_comments(trailing_definition_comments),
FormatDecorators {
decorators: decorator_list,
leading_definition_comments,
},
clause_header(
ClauseHeader::Function(item),
trailing_definition_comments,
&format_with(|f| {
if *is_async {
write!(f, [text("async"), space()])?;
}
write!(f, [text("def"), space(), name.format()])?;
if let Some(type_params) = type_params.as_ref() {
write!(f, [type_params.format()])?;
}
let format_inner = format_with(|f: &mut PyFormatter| {
write!(f, [parameters.format()])?;
if let Some(return_annotation) = returns.as_ref() {
write!(f, [space(), text("->"), space()])?;
if return_annotation.is_tuple_expr() {
let parentheses = if comments
.has_leading_comments(return_annotation.as_ref())
{
Parentheses::Always
} else {
Parentheses::Never
};
write!(
f,
[return_annotation.format().with_options(parentheses)]
)?;
} else if comments.has_trailing_comments(return_annotation.as_ref())
{
// Intentionally parenthesize any return annotations with trailing comments.
// This avoids an instability in cases like:
// ```python
// def double(
// a: int
// ) -> (
// int # Hello
// ):
// pass
// ```
// If we allow this to break, it will be formatted as follows:
// ```python
// def double(
// a: int
// ) -> int: # Hello
// pass
// ```
// On subsequent formats, the `# Hello` will be interpreted as a dangling
// comment on a function, yielding:
// ```python
// def double(a: int) -> int: # Hello
// pass
// ```
// Ideally, we'd reach that final formatting in a single pass, but doing so
// requires that the parent be aware of how the child is formatted, which
// is challenging. As a compromise, we break those expressions to avoid an
// instability.
write!(
f,
[return_annotation
.format()
.with_options(Parentheses::Always)]
)?;
} else {
write!(
f,
[maybe_parenthesize_expression(
return_annotation,
item,
if empty_parameters(parameters, f.context().source()) {
// If the parameters are empty, add parentheses if the return annotation
// breaks at all.
Parenthesize::IfBreaksOrIfRequired
} else {
// Otherwise, use our normal rules for parentheses, which allows us to break
// like:
// ```python
// def f(
// x,
// ) -> Tuple[
// int,
// int,
// ]:
// ...
// ```
Parenthesize::IfBreaks
},
)]
)?;
}
}
Ok(())
});
group(&format_inner).fmt(f)
}),
),
block_indent(&body.format().with_options(SuiteKind::Function))
]
)

View file

@ -1,11 +1,13 @@
use crate::comments::{leading_alternate_branch_comments, trailing_comments, SourceComment};
use ruff_formatter::{format_args, write};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{ElifElseClause, StmtIf};
use crate::comments::SourceComment;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::clause::{clause_header, ClauseHeader};
use crate::FormatNodeRule;
use ruff_formatter::write;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{ElifElseClause, StmtIf};
#[derive(Default)]
pub struct FormatStmtIf;
@ -25,11 +27,15 @@ impl FormatNodeRule<StmtIf> for FormatStmtIf {
write!(
f,
[
text("if"),
space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
text(":"),
trailing_comments(trailing_colon_comment),
clause_header(
ClauseHeader::If(item),
trailing_colon_comment,
&format_args![
text("if"),
space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
],
),
block_indent(&body.format())
]
)?;
@ -70,26 +76,28 @@ pub(crate) fn format_elif_else_clause(
let trailing_colon_comment = comments.dangling_comments(item);
let leading_comments = comments.leading_comments(item);
leading_alternate_branch_comments(leading_comments, last_node).fmt(f)?;
if let Some(test) = test {
write!(
f,
[
text("elif"),
space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
]
)?;
} else {
text("else").fmt(f)?;
}
write!(
f,
[
text(":"),
trailing_comments(trailing_colon_comment),
clause_header(
ClauseHeader::ElifElse(item),
trailing_colon_comment,
&format_with(|f| {
if let Some(test) = test {
write!(
f,
[
text("elif"),
space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
]
)
} else {
text("else").fmt(f)
}
}),
)
.with_leading_comments(leading_comments, last_node),
block_indent(&body.format())
]
)

View file

@ -1,11 +1,12 @@
use ruff_formatter::{format_args, write};
use ruff_python_ast::StmtMatch;
use crate::comments::{leading_alternate_branch_comments, trailing_comments, SourceComment};
use crate::comments::{leading_alternate_branch_comments, SourceComment};
use crate::context::{NodeLevel, WithNodeLevel};
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::clause::{clause_header, ClauseHeader};
use crate::FormatNodeRule;
#[derive(Default)]
@ -25,16 +26,16 @@ impl FormatNodeRule<StmtMatch> for FormatStmtMatch {
// There can be at most one dangling comment after the colon in a match statement.
debug_assert!(dangling_item_comments.len() <= 1);
write!(
f,
[
clause_header(
ClauseHeader::Match(item),
dangling_item_comments,
&format_args![
text("match"),
space(),
maybe_parenthesize_expression(subject, item, Parenthesize::IfBreaks),
text(":"),
trailing_comments(dangling_item_comments)
]
)?;
],
)
.fmt(f)?;
let mut cases_iter = cases.iter();
let Some(first) = cases_iter.next() else {

View file

@ -1,11 +1,12 @@
use ruff_formatter::{write, FormatRuleWithOptions};
use ruff_python_ast::{ExceptHandler, Ranged, StmtTry, Suite};
use ruff_python_ast::{ExceptHandler, Ranged, StmtTry};
use crate::comments;
use crate::comments::leading_alternate_branch_comments;
use crate::comments::SourceComment;
use crate::comments::{leading_alternate_branch_comments, trailing_comments};
use crate::other::except_handler_except_handler::ExceptHandlerKind;
use crate::prelude::*;
use crate::statement::clause::{clause_header, ClauseHeader, ElseClause};
use crate::statement::{FormatRefWithRule, Stmt};
use crate::{FormatNodeRule, PyFormatter};
@ -55,8 +56,8 @@ impl FormatNodeRule<StmtTry> for FormatStmtTry {
let StmtTry {
body,
handlers,
orelse,
finalbody,
orelse: _,
finalbody: _,
is_star,
range: _,
} = item;
@ -64,7 +65,7 @@ impl FormatNodeRule<StmtTry> 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<StmtTry> for FormatStmtTry {
}
(previous_node, dangling_comments) =
format_case("else", orelse, previous_node, dangling_comments, f)?;
format_case(item, CaseKind::Else, previous_node, dangling_comments, f)?;
format_case("finally", finalbody, previous_node, dangling_comments, f)?;
format_case(item, CaseKind::Finally, previous_node, dangling_comments, f)?;
write!(f, [comments::dangling_comments(dangling_comments)])
}
@ -104,25 +105,39 @@ impl FormatNodeRule<StmtTry> for FormatStmtTry {
}
fn format_case<'a>(
name: &'static str,
body: &Suite,
try_statement: &StmtTry,
kind: CaseKind,
previous_node: Option<&Stmt>,
dangling_comments: &'a [SourceComment],
f: &mut PyFormatter,
) -> FormatResult<(Option<&'a Stmt>, &'a [SourceComment])> {
let body = match kind {
CaseKind::Try => &try_statement.body,
CaseKind::Else => &try_statement.orelse,
CaseKind::Finally => &try_statement.finalbody,
};
Ok(if let Some(last) = body.last() {
let case_comments_start =
dangling_comments.partition_point(|comment| comment.slice().end() <= last.end());
let (case_comments, rest) = dangling_comments.split_at(case_comments_start);
let partition_point =
case_comments.partition_point(|comment| comment.line_position().is_own_line());
let (leading_case_comments, trailing_case_comments) =
case_comments.split_at(partition_point);
let header = match kind {
CaseKind::Try => ClauseHeader::Try(try_statement),
CaseKind::Else => ClauseHeader::OrElse(ElseClause::Try(try_statement)),
CaseKind::Finally => ClauseHeader::TryFinally(try_statement),
};
write!(
f,
[
leading_alternate_branch_comments(&case_comments[..partition_point], previous_node),
text(name),
text(":"),
trailing_comments(&case_comments[partition_point..]),
clause_header(header, trailing_case_comments, &text(kind.keyword()))
.with_leading_comments(leading_case_comments, previous_node),
block_indent(&body.format())
]
)?;
@ -131,3 +146,20 @@ fn format_case<'a>(
(None, dangling_comments)
})
}
#[derive(Copy, Clone)]
enum CaseKind {
Try,
Else,
Finally,
}
impl CaseKind {
fn keyword(self) -> &'static str {
match self {
CaseKind::Try => "try",
CaseKind::Else => "else",
CaseKind::Finally => "finally",
}
}
}

View file

@ -1,11 +1,12 @@
use ruff_formatter::write;
use ruff_formatter::{format_args, write};
use ruff_python_ast::node::AstNode;
use ruff_python_ast::{Ranged, Stmt, StmtWhile};
use crate::comments::{leading_alternate_branch_comments, trailing_comments, SourceComment};
use crate::comments::SourceComment;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::clause::{clause_header, ClauseHeader, ElseClause};
use crate::FormatNodeRule;
#[derive(Default)]
@ -33,11 +34,15 @@ impl FormatNodeRule<StmtWhile> for FormatStmtWhile {
write!(
f,
[
text("while"),
space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
text(":"),
trailing_comments(trailing_condition_comments),
clause_header(
ClauseHeader::While(item),
trailing_condition_comments,
&format_args![
text("while"),
space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
]
),
block_indent(&body.format())
]
)?;
@ -52,9 +57,12 @@ impl FormatNodeRule<StmtWhile> for FormatStmtWhile {
write!(
f,
[
leading_alternate_branch_comments(leading, body.last()),
text("else:"),
trailing_comments(trailing),
clause_header(
ClauseHeader::OrElse(ElseClause::While(item)),
trailing,
&text("else")
)
.with_leading_comments(leading, body.last()),
block_indent(&orelse.format())
]
)?;

View file

@ -4,11 +4,12 @@ use ruff_python_ast::{Ranged, StmtWith};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::TextRange;
use crate::comments::{trailing_comments, SourceComment};
use crate::comments::SourceComment;
use crate::expression::parentheses::{
in_parentheses_only_soft_line_break_or_space, optional_parentheses, parenthesized,
};
use crate::prelude::*;
use crate::statement::clause::{clause_header, ClauseHeader};
use crate::FormatNodeRule;
#[derive(Default)]
@ -16,16 +17,6 @@ pub struct FormatStmtWith;
impl FormatNodeRule<StmtWith> for FormatStmtWith {
fn fmt_fields(&self, item: &StmtWith, f: &mut PyFormatter) -> FormatResult<()> {
write!(
f,
[
item.is_async
.then_some(format_args![text("async"), space()]),
text("with"),
space()
]
)?;
// The `with` statement can have one dangling comment on the open parenthesis, like:
// ```python
// with ( # comment
@ -48,41 +39,57 @@ impl FormatNodeRule<StmtWith> for FormatStmtWith {
});
let (parenthesized_comments, colon_comments) = dangling_comments.split_at(partition_point);
if !parenthesized_comments.is_empty() {
let joined = format_with(|f: &mut PyFormatter| {
f.join_comma_separated(item.body.first().unwrap().start())
.nodes(&item.items)
.finish()
});
parenthesized("(", &joined, ")")
.with_dangling_comments(parenthesized_comments)
.fmt(f)?;
} else if are_with_items_parenthesized(item, f.context())? {
optional_parentheses(&format_with(|f| {
let mut joiner = f.join_comma_separated(item.body.first().unwrap().start());
for item in &item.items {
joiner.entry_with_line_separator(
item,
&item.format(),
in_parentheses_only_soft_line_break_or_space(),
);
}
joiner.finish()
}))
.fmt(f)?;
} else {
f.join_with(format_args![text(","), space()])
.entries(item.items.iter().formatted())
.finish()?;
}
write!(
f,
[
text(":"),
trailing_comments(colon_comments),
clause_header(
ClauseHeader::With(item),
colon_comments,
&format_with(|f| {
write!(
f,
[
item.is_async
.then_some(format_args![text("async"), space()]),
text("with"),
space()
]
)?;
if !parenthesized_comments.is_empty() {
let joined = format_with(|f: &mut PyFormatter| {
f.join_comma_separated(item.body.first().unwrap().start())
.nodes(&item.items)
.finish()
});
parenthesized("(", &joined, ")")
.with_dangling_comments(parenthesized_comments)
.fmt(f)?;
} else if are_with_items_parenthesized(item, f.context())? {
optional_parentheses(&format_with(|f| {
let mut joiner =
f.join_comma_separated(item.body.first().unwrap().start());
for item in &item.items {
joiner.entry_with_line_separator(
item,
&item.format(),
in_parentheses_only_soft_line_break_or_space(),
);
}
joiner.finish()
}))
.fmt(f)?;
} else {
f.join_with(format_args![text(","), space()])
.entries(item.items.iter().formatted())
.finish()?;
}
Ok(())
})
),
block_indent(&item.body.format())
]
)

View file

@ -15,6 +15,7 @@ use ruff_text_size::{TextRange, TextSize};
use crate::comments::format::{empty_lines, format_comment};
use crate::comments::{leading_comments, trailing_comments, SourceComment};
use crate::prelude::*;
use crate::statement::clause::ClauseHeader;
use crate::statement::suite::SuiteChildStatement;
/// Disables formatting for all statements between the `first_suppressed` that has a leading `fmt: off` comment
@ -930,3 +931,28 @@ impl Format<PyFormatContext<'_>> for FormatSuppressedNode<'_> {
)
}
}
#[cold]
pub(crate) fn write_suppressed_clause_header(
header: ClauseHeader,
f: &mut PyFormatter,
) -> FormatResult<()> {
// Write the outer comments and format the node as verbatim
write!(
f,
[verbatim_text(
header.range(f.context().source())?,
ContainsNewlines::Detect
),]
)?;
let comments = f.context().comments();
header.visit(&mut |child| {
for comment in comments.leading_trailing_comments(child) {
comment.mark_formatted();
}
comments.mark_verbatim_node_comments_formatted(child);
});
Ok(())
}