Skip to content

Commit bbece64

Browse files
committed
Enforce that the last statement in a loop body has a semicolon
1 parent a32c067 commit bbece64

File tree

9 files changed

+166
-7
lines changed

9 files changed

+166
-7
lines changed

src/expr.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,7 @@ pub(crate) fn rewrite_block_with_visitor(
596596
let mut visitor = FmtVisitor::from_context(context);
597597
visitor.block_indent = shape.indent;
598598
visitor.is_if_else_block = context.is_if_else_block();
599+
visitor.is_loop_block = context.is_loop_block();
599600
match (block.rules, label) {
600601
(ast::BlockCheckMode::Unsafe(..), _) | (ast::BlockCheckMode::Default, Some(_)) => {
601602
let snippet = context.snippet(block.span);
@@ -713,6 +714,7 @@ struct ControlFlow<'a> {
713714
allow_single_line: bool,
714715
// HACK: `true` if this is an `if` expression in an `else if`.
715716
nested_if: bool,
717+
is_loop: bool,
716718
span: Span,
717719
}
718720

@@ -784,6 +786,7 @@ impl<'a> ControlFlow<'a> {
784786
connector: " =",
785787
allow_single_line,
786788
nested_if,
789+
is_loop: false,
787790
span,
788791
}
789792
}
@@ -800,6 +803,7 @@ impl<'a> ControlFlow<'a> {
800803
connector: "",
801804
allow_single_line: false,
802805
nested_if: false,
806+
is_loop: true,
803807
span,
804808
}
805809
}
@@ -823,6 +827,7 @@ impl<'a> ControlFlow<'a> {
823827
connector: " =",
824828
allow_single_line: false,
825829
nested_if: false,
830+
is_loop: true,
826831
span,
827832
}
828833
}
@@ -849,6 +854,7 @@ impl<'a> ControlFlow<'a> {
849854
connector: " in",
850855
allow_single_line: false,
851856
nested_if: false,
857+
is_loop: true,
852858
span,
853859
}
854860
}
@@ -1162,8 +1168,10 @@ impl<'a> Rewrite for ControlFlow<'a> {
11621168
};
11631169
let block_str = {
11641170
let old_val = context.is_if_else_block.replace(self.else_block.is_some());
1171+
let old_is_loop = context.is_loop_block.replace(self.is_loop);
11651172
let result =
11661173
rewrite_block_with_visitor(context, "", self.block, None, None, block_shape, true);
1174+
context.is_loop_block.replace(old_is_loop);
11671175
context.is_if_else_block.replace(old_val);
11681176
result?
11691177
};

src/rewrite.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ pub(crate) struct RewriteContext<'a> {
114114
// When `is_if_else_block` is true, unindent the comment on top
115115
// of the `else` or `else if`.
116116
pub(crate) is_if_else_block: Cell<bool>,
117+
// When `is_loop_block` is true, we can more aggressively end the
118+
// last statement of the block with a semicolon.
119+
pub(crate) is_loop_block: Cell<bool>,
117120
// When rewriting chain, veto going multi line except the last element
118121
pub(crate) force_one_line_chain: Cell<bool>,
119122
pub(crate) snippet_provider: &'a SnippetProvider,
@@ -175,4 +178,8 @@ impl<'a> RewriteContext<'a> {
175178
pub(crate) fn is_if_else_block(&self) -> bool {
176179
self.is_if_else_block.get()
177180
}
181+
182+
pub(crate) fn is_loop_block(&self) -> bool {
183+
self.is_loop_block.get()
184+
}
178185
}

src/visitor.rs

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use rustc_ast::{ast, token::Delimiter, visit};
66
use rustc_span::{BytePos, Ident, Pos, Span, symbol};
77
use tracing::debug;
88

9-
use crate::attr::*;
109
use crate::comment::{CodeCharKind, CommentCodeSlices, contains_comment, rewrite_comment};
1110
use crate::config::{BraceStyle, Config, MacroSelector, StyleEdition};
1211
use crate::coverage::transform_missing_snippet;
@@ -25,8 +24,9 @@ use crate::spanned::Spanned;
2524
use crate::stmt::Stmt;
2625
use crate::utils::{
2726
self, contains_skip, count_newlines, depr_skip_annotation, format_safety, inner_attributes,
28-
last_line_width, mk_sp, ptr_vec_to_ref_vec, rewrite_ident, starts_with_newline, stmt_expr,
27+
last_line_width, mk_sp, ptr_vec_to_ref_vec, rewrite_ident, starts_with_newline,
2928
};
29+
use crate::{Edition, attr::*};
3030
use crate::{ErrorKind, FormatReport, FormattingError};
3131

3232
/// Creates a string slice corresponding to the specified span.
@@ -78,6 +78,7 @@ pub(crate) struct FmtVisitor<'a> {
7878
pub(crate) block_indent: Indent,
7979
pub(crate) config: &'a Config,
8080
pub(crate) is_if_else_block: bool,
81+
pub(crate) is_loop_block: bool,
8182
pub(crate) snippet_provider: &'a SnippetProvider,
8283
pub(crate) line_number: usize,
8384
/// List of 1-based line ranges which were annotated with skip
@@ -231,11 +232,9 @@ impl<'b, 'a: 'b> FmtVisitor<'a> {
231232

232233
self.walk_block_stmts(b);
233234

234-
if !b.stmts.is_empty() {
235-
if let Some(expr) = stmt_expr(&b.stmts[b.stmts.len() - 1]) {
236-
if utils::semicolon_for_expr(&self.get_context(), expr) {
237-
self.push_str(";");
238-
}
235+
if let Some(stmt) = b.stmts.last() {
236+
if self.add_semi_on_last_block_stmt(stmt) {
237+
self.push_str(";");
239238
}
240239
}
241240

@@ -802,6 +801,7 @@ impl<'b, 'a: 'b> FmtVisitor<'a> {
802801
block_indent: Indent::empty(),
803802
config,
804803
is_if_else_block: false,
804+
is_loop_block: false,
805805
snippet_provider,
806806
line_number: 0,
807807
skipped_range: Rc::new(RefCell::new(vec![])),
@@ -1019,6 +1019,7 @@ impl<'b, 'a: 'b> FmtVisitor<'a> {
10191019
inside_macro: Rc::new(Cell::new(false)),
10201020
use_block: Cell::new(false),
10211021
is_if_else_block: Cell::new(false),
1022+
is_loop_block: Cell::new(false),
10221023
force_one_line_chain: Cell::new(false),
10231024
snippet_provider: self.snippet_provider,
10241025
macro_rewrite_failure: Cell::new(false),
@@ -1028,4 +1029,43 @@ impl<'b, 'a: 'b> FmtVisitor<'a> {
10281029
skipped_range: self.skipped_range.clone(),
10291030
}
10301031
}
1032+
1033+
fn add_semi_on_last_block_stmt(&self, stmt: &ast::Stmt) -> bool {
1034+
let ast::StmtKind::Expr(expr) = &stmt.kind else {
1035+
return false;
1036+
};
1037+
1038+
if self.is_macro_def {
1039+
return false;
1040+
}
1041+
1042+
match expr.kind {
1043+
ast::ExprKind::Ret(..) | ast::ExprKind::Continue(..) | ast::ExprKind::Break(..) => {
1044+
self.config.trailing_semicolon()
1045+
}
1046+
1047+
// TODO[reviewer-help]: This is roughly "does it end in a
1048+
// curly". There might be a helper for this, or cases I'm
1049+
// missing.
1050+
ast::ExprKind::Loop(..)
1051+
| ast::ExprKind::While(..)
1052+
| ast::ExprKind::ForLoop { .. }
1053+
| ast::ExprKind::Let(..)
1054+
| ast::ExprKind::If(..)
1055+
| ast::ExprKind::Match(..) => false,
1056+
1057+
_ => {
1058+
// Checking the edition as before 2024 the lack of a
1059+
// semicolon could impact temporary lifetimes[1].
1060+
//
1061+
// 1: https://rust-lang.github.io/rfcs/
1062+
// 3606-temporary-lifetimes-in-tail-expressions.html
1063+
let allowed_to_add_semi = self.is_loop_block
1064+
&& self.config.edition() >= Edition::Edition2024
1065+
&& self.config.style_edition() >= StyleEdition::Edition2027;
1066+
1067+
allowed_to_add_semi && self.config.trailing_semicolon()
1068+
}
1069+
}
1070+
}
10311071
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// rustfmt-edition: 2021
2+
// rustfmt-style_edition: 2027
3+
4+
fn main() {
5+
loop {
6+
println!()
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// rustfmt-edition: 2024
2+
// rustfmt-style_edition: 2024
3+
4+
fn main() {
5+
loop {
6+
println!()
7+
}
8+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// rustfmt-edition: 2024
2+
// rustfmt-style_edition: 2027
3+
4+
macro_rules! dummy {
5+
() => {};
6+
}
7+
8+
fn main() {
9+
for x in [1] {
10+
println!()
11+
}
12+
13+
while false {
14+
println!()
15+
}
16+
17+
while let Some('x') = None {
18+
println!()
19+
}
20+
21+
loop {
22+
println!()
23+
}
24+
25+
loop {
26+
dummy! {}
27+
}
28+
29+
loop {
30+
dummy!()
31+
}
32+
33+
loop {
34+
dummy![]
35+
}
36+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// rustfmt-edition: 2021
2+
// rustfmt-style_edition: 2027
3+
4+
fn main() {
5+
loop {
6+
println!()
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// rustfmt-edition: 2024
2+
// rustfmt-style_edition: 2024
3+
4+
fn main() {
5+
loop {
6+
println!()
7+
}
8+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// rustfmt-edition: 2024
2+
// rustfmt-style_edition: 2027
3+
4+
macro_rules! dummy {
5+
() => {};
6+
}
7+
8+
fn main() {
9+
for x in [1] {
10+
println!();
11+
}
12+
13+
while false {
14+
println!();
15+
}
16+
17+
while let Some('x') = None {
18+
println!();
19+
}
20+
21+
loop {
22+
println!();
23+
}
24+
25+
loop {
26+
dummy! {}
27+
}
28+
29+
loop {
30+
dummy!();
31+
}
32+
33+
loop {
34+
dummy![];
35+
}
36+
}

0 commit comments

Comments
 (0)