Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement trailing_semicolon = "Preserve" #6149

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions Configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -2780,18 +2780,29 @@ See also: [`match_block_trailing_comma`](#match_block_trailing_comma).

Add trailing semicolon after break, continue and return

- **Default value**: `true`
- **Possible values**: `true`, `false`
- **Default value**: `Preserve`
- **Possible values**: `Preserve`, `Always`, `Never`
- **Stable**: No (tracking issue: [#3378](https://github.com/rust-lang/rustfmt/issues/3378))

#### `true` (default):
#### `Preserve` (default):
```rust
fn foo() -> usize {
return 0;
}

fn bar() -> usize {
return 0
}
```
Comment on lines +2788 to +2796
Copy link
Contributor

@ytmimi ytmimi Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These aren't changes we necessarily need to make, but it might also be nice to include examples with break; and continue; to further demonstrate how this will work. Possibly adding a let-else example.


#### `Always`:
```rust
fn foo() -> usize {
return 0;
}
```

#### `false`:
#### `Never`:
```rust
fn foo() -> usize {
return 0
Expand Down
30 changes: 13 additions & 17 deletions src/closures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,26 +391,22 @@ pub(crate) fn rewrite_last_closure(

// We force to use block for the body of the closure for certain kinds of expressions.
if is_block_closure_forced(context, body) {
return rewrite_closure_with_block(body, &prefix, context, body_shape).map(
|body_str| {
match fn_decl.output {
ast::FnRetTy::Default(..) if body_str.lines().count() <= 7 => {
// If the expression can fit in a single line, we need not force block
// closure. However, if the closure has a return type, then we must
// keep the blocks.
match rewrite_closure_expr(body, &prefix, context, shape) {
Some(single_line_body_str)
if !single_line_body_str.contains('\n') =>
{
single_line_body_str
}
_ => body_str,
return rewrite_closure_with_block(body, &prefix, context, body_shape).map(|body_str| {
match fn_decl.output {
ast::FnRetTy::Default(..) if body_str.lines().count() <= 7 => {
// If the expression can fit in a single line, we need not force block
// closure. However, if the closure has a return type, then we must
// keep the blocks.
match rewrite_closure_expr(body, &prefix, context, shape) {
Some(single_line_body_str) if !single_line_body_str.contains('\n') => {
single_line_body_str
}
_ => body_str,
}
_ => body_str,
}
},
);
_ => body_str,
}
});
}

// When overflowing the closure which consists of a single control flow expression,
Expand Down
4 changes: 2 additions & 2 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ create_config! {
brace_style: BraceStyle, BraceStyle::SameLineWhere, false, "Brace style for items";
control_brace_style: ControlBraceStyle, ControlBraceStyle::AlwaysSameLine, false,
"Brace style for control flow constructs";
trailing_semicolon: bool, true, false,
trailing_semicolon: TrailingSemicolon, TrailingSemicolon::Preserve, false,
"Add trailing semicolon after break, continue and return";
trailing_comma: SeparatorTactic, SeparatorTactic::Vertical, false,
"How to handle trailing commas for lists";
Expand Down Expand Up @@ -673,7 +673,7 @@ force_multiline_blocks = false
fn_params_layout = "Tall"
brace_style = "SameLineWhere"
control_brace_style = "AlwaysSameLine"
trailing_semicolon = true
trailing_semicolon = "Preserve"
trailing_comma = "Vertical"
match_block_trailing_comma = false
blank_lines_upper_bound = 1
Expand Down
13 changes: 13 additions & 0 deletions src/config/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ pub enum ControlBraceStyle {
AlwaysNextLine,
}

#[config_type]
/// How to treat the semicolon that is optional for final diverging expressions
/// (`return`/`break`/`continue`).
pub enum TrailingSemicolon {
/// Always rewrite `return;` and `break;` expressions to have a trailing semicolon,
/// unless the block is a single-line block, e.g. `let PAT = e else { return }`.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of weird, but preexisting behavior, I believe.

I don't exactly understand what rustfmt does to avoid adding a semicolon for single-line-style blocks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are different kinds of blocks. What kinds of blocks are you referencing? Just let-else blocks?

To the best of my knowledge all let-else blocks are handled when rewriting ast::Local nodes. The code removes 1 from the shape to account for the trailing semicolon (line 127). Maybe it shouldn't?

rustfmt/src/items.rs

Lines 49 to 188 in 4b1596f

impl Rewrite for ast::Local {
fn rewrite(&self, context: &RewriteContext<'_>, shape: Shape) -> Option<String> {
debug!(
"Local::rewrite {:?} {} {:?}",
self, shape.width, shape.indent
);
skip_out_of_file_lines_range!(context, self.span);
if contains_skip(&self.attrs) {
return None;
}
let attrs_str = self.attrs.rewrite(context, shape)?;
let mut result = if attrs_str.is_empty() {
"let ".to_owned()
} else {
combine_strs_with_missing_comments(
context,
&attrs_str,
"let ",
mk_sp(
self.attrs.last().map(|a| a.span.hi()).unwrap(),
self.span.lo(),
),
shape,
false,
)?
};
let let_kw_offset = result.len() - "let ".len();
// 4 = "let ".len()
let pat_shape = shape.offset_left(4)?;
// 1 = ;
let pat_shape = pat_shape.sub_width(1)?;
let pat_str = self.pat.rewrite(context, pat_shape)?;
result.push_str(&pat_str);
// String that is placed within the assignment pattern and expression.
let infix = {
let mut infix = String::with_capacity(32);
if let Some(ref ty) = self.ty {
let separator = type_annotation_separator(context.config);
let ty_shape = if pat_str.contains('\n') {
shape.with_max_width(context.config)
} else {
shape
}
.offset_left(last_line_width(&result) + separator.len())?
// 2 = ` =`
.sub_width(2)?;
let rewrite = ty.rewrite(context, ty_shape)?;
infix.push_str(separator);
infix.push_str(&rewrite);
}
if self.kind.init().is_some() {
infix.push_str(" =");
}
infix
};
result.push_str(&infix);
if let Some((init, else_block)) = self.kind.init_else_opt() {
// 1 = trailing semicolon;
let nested_shape = shape.sub_width(1)?;
result = rewrite_assign_rhs(
context,
result,
init,
&RhsAssignKind::Expr(&init.kind, init.span),
nested_shape,
)?;
if let Some(block) = else_block {
let else_kw_span = init.span.between(block.span);
// Strip attributes and comments to check if newline is needed before the else
// keyword from the initializer part. (#5901)
let init_str = if context.config.version() == Version::Two {
&result[let_kw_offset..]
} else {
result.as_str()
};
let force_newline_else = pat_str.contains('\n')
|| !same_line_else_kw_and_brace(init_str, context, else_kw_span, nested_shape);
let else_kw = rewrite_else_kw_with_comments(
force_newline_else,
true,
context,
else_kw_span,
shape,
);
result.push_str(&else_kw);
// At this point we've written `let {pat} = {expr} else' into the buffer, and we
// want to calculate up front if there's room to write the divergent block on the
// same line. The available space varies based on indentation so we clamp the width
// on the smaller of `shape.width` and `single_line_let_else_max_width`.
let max_width =
std::cmp::min(shape.width, context.config.single_line_let_else_max_width());
// If available_space hits zero we know for sure this will be a multi-lined block
let assign_str_with_else_kw = if context.config.version() == Version::Two {
&result[let_kw_offset..]
} else {
result.as_str()
};
let available_space = max_width.saturating_sub(assign_str_with_else_kw.len());
let allow_single_line = !force_newline_else
&& available_space > 0
&& allow_single_line_let_else_block(assign_str_with_else_kw, block);
let mut rw_else_block =
rewrite_let_else_block(block, allow_single_line, context, shape)?;
let single_line_else = !rw_else_block.contains('\n');
// +1 for the trailing `;`
let else_block_exceeds_width = rw_else_block.len() + 1 > available_space;
if allow_single_line && single_line_else && else_block_exceeds_width {
// writing this on one line would exceed the available width
// so rewrite the else block over multiple lines.
rw_else_block = rewrite_let_else_block(block, false, context, shape)?;
}
result.push_str(&rw_else_block);
};
}
result.push(';');
Some(result)
}
}

Copy link
Contributor

@ytmimi ytmimi Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure all block rewriting eventually calls into rewrite_block_inner, which eventually delegates to a few other block rewrite functions, e.g rewrite_empty_block or rewrite_single_line_block, and I'm fairly certain none of the block rewriting code handles trailing semicolons.

Always,
/// Always return `return` and `break` expressions to remove the trailing semicolon.
Copy link
Contributor

@ytmimi ytmimi Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just want to avoid any confusion with the docs. Since this is the Never variant it might be better to not use the word "Always" in the description. One potential rewording:

/// Never add a trailing semicolon. This removes trailing semicolon's from `return` and `break` expressions.

Also, this won't remove them from continue;?

Never,
/// Preserve an existing trailing semicolon if it exists.
Preserve,
}

#[config_type]
/// How to indent.
pub enum IndentStyle {
Expand Down
7 changes: 5 additions & 2 deletions src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use crate::types::{rewrite_path, PathContext};
use crate::utils::{
colon_spaces, contains_skip, count_newlines, filtered_str_fits, first_line_ends_with,
inner_attributes, last_line_extendable, last_line_width, mk_sp, outer_attributes,
semicolon_for_expr, unicode_str_width, wrap_str,
semicolon_for_expr_extra_hacky_double_counted_spacing, unicode_str_width, wrap_str,
};
use crate::vertical::rewrite_with_alignment;
use crate::visitor::FmtVisitor;
Expand Down Expand Up @@ -64,7 +64,10 @@ pub(crate) fn format_expr(
if contains_skip(&*expr.attrs) {
return Some(context.snippet(expr.span()).to_owned());
}
let shape = if expr_type == ExprType::Statement && semicolon_for_expr(context, expr) {
let shape = if context.config.version() == Version::One
&& expr_type == ExprType::Statement
&& semicolon_for_expr_extra_hacky_double_counted_spacing(context, expr)
{
shape.sub_width(1)?
} else {
shape
Expand Down
7 changes: 1 addition & 6 deletions src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,12 +240,7 @@ fn rewrite_macro_inner(
} = match parse_macro_args(context, ts, style, is_forced_bracket) {
Some(args) => args,
None => {
return return_macro_parse_failure_fallback(
context,
shape.indent,
position,
mac.span(),
);
return return_macro_parse_failure_fallback(context, shape.indent, position, mac.span());
}
};

Expand Down
14 changes: 12 additions & 2 deletions src/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::rewrite::{Rewrite, RewriteContext};
use crate::shape::Shape;
use crate::source_map::LineRangeUtils;
use crate::spanned::Spanned;
use crate::utils::semicolon_for_stmt;
use crate::utils::{semicolon_for_expr, semicolon_for_stmt};

pub(crate) struct Stmt<'a> {
inner: &'a ast::Stmt,
Expand Down Expand Up @@ -116,7 +116,7 @@ fn format_stmt(

let result = match stmt.kind {
ast::StmtKind::Local(ref local) => local.rewrite(context, shape),
ast::StmtKind::Expr(ref ex) | ast::StmtKind::Semi(ref ex) => {
ast::StmtKind::Semi(ref ex) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this the cause of the double semicolon counting?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the comment below; I pointed out the reason for the the double semicolon counting in more detail.

I split these two branches because we need to preserve semicolons only if we have a StmtKind::Semi, and not if we have a StmtKind::Expr.

let suffix = if semicolon_for_stmt(context, stmt, is_last_expr) {
";"
} else {
Expand All @@ -126,6 +126,16 @@ fn format_stmt(
let shape = shape.sub_width(suffix.len())?;
format_expr(ex, expr_type, context, shape).map(|s| s + suffix)
}
ast::StmtKind::Expr(ref ex) => {
let suffix = if semicolon_for_expr(context, ex) {
";"
} else {
""
};

let shape = shape.sub_width(suffix.len())?;
format_expr(ex, expr_type, context, shape).map(|s| s + suffix)
}
ast::StmtKind::MacCall(..) | ast::StmtKind::Item(..) | ast::StmtKind::Empty => None,
};
result.and_then(|res| recover_comment_removed(res, stmt.span(), context))
Expand Down
43 changes: 39 additions & 4 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use rustc_span::{sym, symbol, BytePos, LocalExpnId, Span, Symbol, SyntaxContext}
use unicode_width::UnicodeWidthStr;

use crate::comment::{filter_normal_code, CharClasses, FullCodeCharKind, LineClasses};
use crate::config::{Config, Version};
use crate::config::{Config, TrailingSemicolon, Version};
use crate::rewrite::RewriteContext;
use crate::shape::{Indent, Shape};

Expand Down Expand Up @@ -273,15 +273,43 @@ pub(crate) fn contains_skip(attrs: &[Attribute]) -> bool {
#[inline]
pub(crate) fn semicolon_for_expr(context: &RewriteContext<'_>, expr: &ast::Expr) -> bool {
// Never try to insert semicolons on expressions when we're inside
// a macro definition - this can prevent the macro from compiling
// a macro definition - this can prevent the macro from compiling
// when used in expression position
if context.is_macro_def {
return false;
}

match expr.kind {
ast::ExprKind::Ret(..) | ast::ExprKind::Continue(..) | ast::ExprKind::Break(..) => {
context.config.trailing_semicolon()
match context.config.trailing_semicolon() {
TrailingSemicolon::Always => true,
TrailingSemicolon::Never | TrailingSemicolon::Preserve => false,
}
}
_ => false,
}
}

/// Previously, we used to have `trailing_semicolon = Always` enabled, and due to
/// a bug between `format_stmt` and `format_expr`, we used to subtract the size of
/// `;` *TWICE* from the shape. This means that an expr that would fit onto a line
/// of, e.g. 99 (limit 100) after subtracting one for the semicolon would still be
/// wrapped.
///
/// This function reimplements the old heuristic of double counting the "phantom"
/// semicolon that should have already been accounted for, to not break existing
/// formatting with the `trailing_semicolon = Preserve` behavior for Version = One.
#[inline]
pub(crate) fn semicolon_for_expr_extra_hacky_double_counted_spacing(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this function if we're version gating the fix and only applying the old behavior for version=One?

context: &RewriteContext<'_>,
expr: &ast::Expr,
) -> bool {
match expr.kind {
ast::ExprKind::Ret(..) | ast::ExprKind::Continue(..) | ast::ExprKind::Break(..) => {
match context.config.trailing_semicolon() {
TrailingSemicolon::Always | TrailingSemicolon::Preserve => true,
TrailingSemicolon::Never => false,
}
}
_ => false,
}
Expand All @@ -301,7 +329,14 @@ pub(crate) fn semicolon_for_stmt(
ast::ExprKind::Break(..) | ast::ExprKind::Continue(..) | ast::ExprKind::Ret(..) => {
// The only time we can skip the semi-colon is if the config option is set to false
// **and** this is the last expr (even though any following exprs are unreachable)
context.config.trailing_semicolon() || !is_last_expr
if !is_last_expr {
true
} else {
match context.config.trailing_semicolon() {
TrailingSemicolon::Always | TrailingSemicolon::Preserve => true,
TrailingSemicolon::Never => false,
}
}
}
_ => true,
},
Expand Down
5 changes: 5 additions & 0 deletions tests/source/return-expr-version-one.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// rustfmt-version: One

fn main() {
return hellooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo(arg);
}
14 changes: 7 additions & 7 deletions tests/target/configs/single_line_let_else_max_width/100.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ fn main() {

let Some(c) = opt else {
// a comment should always force the block to be multi-lined
return;
return
};

let Some(c) = opt else {
/* a comment should always force the block to be multi-lined */
return;
return
};

let Some(d) = some_very_very_very_very_long_name else { return };
Expand All @@ -26,23 +26,23 @@ fn main() {
range: _,
}) = slice.as_ref()
else {
return;
return
};

let Some((base_place, current)) = self.lower_expr_as_place(current, *base, true)? else {
return Ok(None);
return Ok(None)
};

let Some(doc_attr) = variant
.attrs
.iter()
.find(|attr| attr.path().is_ident("doc"))
else {
return Err(Error::new(variant.span(), r#"expected a doc comment"#));
return Err(Error::new(variant.span(), r#"expected a doc comment"#))
};

let Some((base_place, current)) = self.lower_expr_as_place(current, *base, true) else {
return Ok(None);
return Ok(None)
};

let Stmt::Expr(
Expand All @@ -55,6 +55,6 @@ fn main() {
return Err(Error::new(
last_stmt.span(),
"expected last expression to be `Some(match (..) { .. })`",
));
))
};
}
16 changes: 8 additions & 8 deletions tests/target/configs/single_line_let_else_max_width/50.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ fn main() {

let Some(c) = opt else {
// a comment should always force the block to be multi-lined
return;
return
};

let Some(c) = opt else {
/* a comment should always force the block to be multi-lined */
return;
return
};

let Some(d) = some_very_very_very_very_long_name else {
return;
return
};

let Expr::Slice(ast::ExprSlice {
Expand All @@ -28,23 +28,23 @@ fn main() {
range: _,
}) = slice.as_ref()
else {
return;
return
};

let Some((base_place, current)) = self.lower_expr_as_place(current, *base, true)? else {
return Ok(None);
return Ok(None)
};

let Some(doc_attr) = variant
.attrs
.iter()
.find(|attr| attr.path().is_ident("doc"))
else {
return Err(Error::new(variant.span(), r#"expected a doc comment"#));
return Err(Error::new(variant.span(), r#"expected a doc comment"#))
};

let Some((base_place, current)) = self.lower_expr_as_place(current, *base, true) else {
return Ok(None);
return Ok(None)
};

let Stmt::Expr(
Expand All @@ -57,6 +57,6 @@ fn main() {
return Err(Error::new(
last_stmt.span(),
"expected last expression to be `Some(match (..) { .. })`",
));
))
};
}
Loading
Loading