## Summary This PR updates the formatter to preserve trailing semicolon for Jupyter Notebooks. The motivation behind the change is that semicolons in notebooks are typically used to hide the output, for example when plotting. This is highlighted in the linked issue. The conditions required as to when the trailing semicolon should be preserved are: 1. It should be a top-level statement which is last in the module. 2. For statement, it can be either assignment, annotated assignment, or augmented assignment. Here, the target should only be a single identifier i.e., multiple assignments or tuple unpacking isn't considered. 3. For expression, it can be any. ## Test Plan Add a new integration test in `ruff_cli`. The test notebook basically acts as a document as to which trailing semicolons are to be preserved. fixes: #8254
136 lines
4.0 KiB
Rust
136 lines
4.0 KiB
Rust
use ruff_formatter::{format_args, write, FormatError};
|
|
use ruff_python_ast::{Expr, StmtAssign};
|
|
|
|
use crate::comments::{SourceComment, SuppressionKind};
|
|
use crate::context::{NodeLevel, WithNodeLevel};
|
|
use crate::expression::parentheses::{Parentheses, Parenthesize};
|
|
use crate::expression::{has_own_parentheses, maybe_parenthesize_expression};
|
|
use crate::prelude::*;
|
|
use crate::statement::trailing_semicolon;
|
|
|
|
#[derive(Default)]
|
|
pub struct FormatStmtAssign;
|
|
|
|
impl FormatNodeRule<StmtAssign> for FormatStmtAssign {
|
|
fn fmt_fields(&self, item: &StmtAssign, f: &mut PyFormatter) -> FormatResult<()> {
|
|
let StmtAssign {
|
|
range: _,
|
|
targets,
|
|
value,
|
|
} = item;
|
|
|
|
let (first, rest) = targets.split_first().ok_or(FormatError::syntax_error(
|
|
"Expected at least on assignment target",
|
|
))?;
|
|
|
|
write!(
|
|
f,
|
|
[
|
|
first.format(),
|
|
space(),
|
|
token("="),
|
|
space(),
|
|
FormatTargets { targets: rest }
|
|
]
|
|
)?;
|
|
|
|
write!(
|
|
f,
|
|
[maybe_parenthesize_expression(
|
|
value,
|
|
item,
|
|
Parenthesize::IfBreaks
|
|
)]
|
|
)?;
|
|
|
|
if f.options().source_type().is_ipynb()
|
|
&& f.context().node_level().is_last_top_level_statement()
|
|
&& rest.is_empty()
|
|
&& first.is_name_expr()
|
|
&& trailing_semicolon(item.into(), f.context().source()).is_some()
|
|
{
|
|
token(";").fmt(f)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn is_suppressed(
|
|
&self,
|
|
trailing_comments: &[SourceComment],
|
|
context: &PyFormatContext,
|
|
) -> bool {
|
|
SuppressionKind::has_skip_comment(trailing_comments, context.source())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct FormatTargets<'a> {
|
|
targets: &'a [Expr],
|
|
}
|
|
|
|
impl Format<PyFormatContext<'_>> for FormatTargets<'_> {
|
|
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
|
if let Some((first, rest)) = self.targets.split_first() {
|
|
let comments = f.context().comments();
|
|
|
|
let parenthesize = if comments.has_leading(first) {
|
|
ParenthesizeTarget::Always
|
|
} else if has_own_parentheses(first, f.context()).is_some() {
|
|
ParenthesizeTarget::Never
|
|
} else {
|
|
ParenthesizeTarget::IfBreaks
|
|
};
|
|
|
|
let group_id = if parenthesize == ParenthesizeTarget::Never {
|
|
Some(f.group_id("assignment_parentheses"))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let format_first = format_with(|f: &mut PyFormatter| {
|
|
let mut f = WithNodeLevel::new(NodeLevel::Expression(group_id), f);
|
|
match parenthesize {
|
|
ParenthesizeTarget::Always => {
|
|
write!(f, [first.format().with_options(Parentheses::Always)])
|
|
}
|
|
ParenthesizeTarget::Never => {
|
|
write!(f, [first.format().with_options(Parentheses::Never)])
|
|
}
|
|
ParenthesizeTarget::IfBreaks => {
|
|
write!(
|
|
f,
|
|
[
|
|
if_group_breaks(&token("(")),
|
|
soft_block_indent(&first.format().with_options(Parentheses::Never)),
|
|
if_group_breaks(&token(")"))
|
|
]
|
|
)
|
|
}
|
|
}
|
|
});
|
|
|
|
write!(
|
|
f,
|
|
[group(&format_args![
|
|
format_first,
|
|
space(),
|
|
token("="),
|
|
space(),
|
|
FormatTargets { targets: rest }
|
|
])
|
|
.with_group_id(group_id)]
|
|
)
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum ParenthesizeTarget {
|
|
Always,
|
|
Never,
|
|
IfBreaks,
|
|
}
|