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 3 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
20 changes: 16 additions & 4 deletions Configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -2780,20 +2780,32 @@ 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**: `always`, `never`, `preserve`
compiler-errors marked this conversation as resolved.
Show resolved Hide resolved
- **Stable**: No (tracking issue: [#3378](https://github.com/rust-lang/rustfmt/issues/3378))

#### `true` (default):
#### `always` (default):
compiler-errors marked this conversation as resolved.
Show resolved Hide resolved
```rust
fn foo() -> usize {
return 0;
}
```

#### `false`:
#### `never`:
```rust
fn foo() -> usize {
return 0
}
```


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

fn bar() -> usize {
return 0
}
```
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
7 changes: 7 additions & 0 deletions src/config/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ pub enum ControlBraceStyle {
AlwaysNextLine,
}

#[config_type]
pub enum TrailingSemicolon {
Always,
Never,
Preserve,
}
ytmimi marked this conversation as resolved.
Show resolved Hide resolved

#[config_type]
/// How to indent.
pub enum IndentStyle {
Expand Down
6 changes: 4 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,9 @@ 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 expr_type == ExprType::Statement
&& semicolon_for_expr_extra_hacky_double_counted_spacing(context, expr)
{
Copy link
Member Author

Choose a reason for hiding this comment

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

I cannot remember what the bug policy here is, but ideally we'd just remove this adjustment to shape since it's already accounted for in format_stmt. That causes self-formatting test to fail because some expressions actually fit onto one line.

Copy link
Contributor

Choose a reason for hiding this comment

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

You can fix the shape adjustment if the fix is version gated to version=Two. You could updated the logic so that we only sub 1 when version=One is set,

Copy link
Member Author

Choose a reason for hiding this comment

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

I fixed it in Version=Two, added a test for one&two, and fixed the cases that changed rustfmt's own formatting (since rustfmt apparently uses Two! 😆)

Copy link
Contributor

Choose a reason for hiding this comment

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

responded to this in #6149 (comment)

shape.sub_width(1)?
} else {
shape
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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind showing an example where things are wrapping early, maybe even adding a version=One test case if we go the route of fixing things for version=Two.

Copy link
Member Author

Choose a reason for hiding this comment

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

fn main() {
    return hellooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo(arg);
}

The return statement line is exactly 100 chars long. When formatting the expression, we first subtract one from the shape here:

rustfmt/src/stmt.rs

Lines 119 to 128 in 7289391

ast::StmtKind::Expr(ref ex) | ast::StmtKind::Semi(ref ex) => {
let suffix = if semicolon_for_stmt(context, stmt, is_last_expr) {
";"
} else {
""
};
let shape = shape.sub_width(suffix.len())?;
format_expr(ex, expr_type, context, shape).map(|s| s + suffix)
}

(See let shape = shape.sub_width(suffix.len())?; )

And then once again here:

rustfmt/src/expr.rs

Lines 67 to 71 in 7289391

let shape = if expr_type == ExprType::Statement && semicolon_for_expr(context, expr) {
shape.sub_width(1)?
} else {
shape
};

(in format_expr).

In practice, for example, we need to format return helloo...o(arg) which is an expression that is 95 characters long (100 - 4 identation - 1 semicolon) but after subtracting from the shape twice, we only have have a budget of 94 characters, even though the whole expression should fit in the line.

I can add it as a version=One test.

Copy link
Member Author

Choose a reason for hiding this comment

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

For the record, i discovered this because it happens twice in practice in the rustfmt codebase, so the self-formatting test failed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, we use version=Two within rustfmt as a way to dogfood unstable formatting (at least that's my assumption)

#[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
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 (..) { .. })`",
));
))
};
}
20 changes: 10 additions & 10 deletions tests/target/configs/single_line_let_else_max_width/zero.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@ fn main() {
let Some(a) = opt else {};

let Some(b) = opt else {
return;
return
};

let Some(c) = opt else {
return;
return
};

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 @@ -32,23 +32,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 @@ -61,6 +61,6 @@ fn main() {
return Err(Error::new(
last_stmt.span(),
"expected last expression to be `Some(match (..) { .. })`",
));
))
};
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// rustfmt-trailing_semicolon: true
// rustfmt-trailing_semicolon: always

#![feature(loop_break_value)]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// rustfmt-trailing_semicolon: false
// rustfmt-trailing_semicolon: never

#![feature(loop_break_value)]

Expand Down
Loading
Loading