Skip to content

Commit d79f862

Browse files
authored
Do not remove semicolon if it changes the block type (#14103)
Removing the semicolon of the last statement of an expressionless block may change the block type even if the statement's type is `()`. If the block type is `!` because of a systematic early return, typing it as `()` may make it incompatible with the expected type for the block (to which `!` is cast). Fix #14100 changelog: [`unnecessary_semicolon`]: do not remove semicolon if it could change the block type from `!` to `()`
2 parents e188c7d + 78d6b2e commit d79f862

4 files changed

+43
-13
lines changed

clippy_lints/src/unnecessary_semicolon.rs

+25-13
Original file line numberDiff line numberDiff line change
@@ -37,35 +37,33 @@ declare_clippy_lint! {
3737

3838
#[derive(Default)]
3939
pub struct UnnecessarySemicolon {
40-
last_statements: Vec<HirId>,
40+
last_statements: Vec<(HirId, bool)>,
4141
}
4242

4343
impl_lint_pass!(UnnecessarySemicolon => [UNNECESSARY_SEMICOLON]);
4444

4545
impl UnnecessarySemicolon {
4646
/// Enter or leave a block, remembering the last statement of the block.
4747
fn handle_block(&mut self, cx: &LateContext<'_>, block: &Block<'_>, enter: bool) {
48-
// Up to edition 2021, removing the semicolon of the last statement of a block
49-
// may result in the scrutinee temporary values to live longer than the block
50-
// variables. To avoid this problem, we do not lint the last statement of an
51-
// expressionless block.
52-
if cx.tcx.sess.edition() <= Edition2021
53-
&& block.expr.is_none()
48+
// The last statement of an expressionless block deserves a special treatment.
49+
if block.expr.is_none()
5450
&& let Some(last_stmt) = block.stmts.last()
5551
{
5652
if enter {
57-
self.last_statements.push(last_stmt.hir_id);
53+
let block_ty = cx.typeck_results().node_type(block.hir_id);
54+
self.last_statements.push((last_stmt.hir_id, block_ty.is_unit()));
5855
} else {
5956
self.last_statements.pop();
6057
}
6158
}
6259
}
6360

64-
/// Checks if `stmt` is the last statement in an expressionless block for edition ≤ 2021.
65-
fn is_last_in_block(&self, stmt: &Stmt<'_>) -> bool {
61+
/// Checks if `stmt` is the last statement in an expressionless block. In this case,
62+
/// return `Some` with a boolean which is `true` if the block type is `()`.
63+
fn is_last_in_block(&self, stmt: &Stmt<'_>) -> Option<bool> {
6664
self.last_statements
6765
.last()
68-
.is_some_and(|last_stmt_id| last_stmt_id == &stmt.hir_id)
66+
.and_then(|&(stmt_id, is_unit)| (stmt_id == stmt.hir_id).then_some(is_unit))
6967
}
7068
}
7169

@@ -90,8 +88,22 @@ impl<'tcx> LateLintPass<'tcx> for UnnecessarySemicolon {
9088
)
9189
&& cx.typeck_results().expr_ty(expr) == cx.tcx.types.unit
9290
{
93-
if self.is_last_in_block(stmt) && leaks_droppable_temporary_with_limited_lifetime(cx, expr) {
94-
return;
91+
if let Some(block_is_unit) = self.is_last_in_block(stmt) {
92+
if cx.tcx.sess.edition() <= Edition2021 && leaks_droppable_temporary_with_limited_lifetime(cx, expr) {
93+
// The expression contains temporaries with limited lifetimes in edition lower than 2024. Those may
94+
// survive until after the end of the current scope instead of until the end of the statement, so do
95+
// not lint this situation.
96+
return;
97+
}
98+
99+
if !block_is_unit {
100+
// Although the expression returns `()`, the block doesn't. This may happen if the expression
101+
// returns early in all code paths, such as a `return value` in the condition of an `if` statement,
102+
// in which case the block type would be `!`. Do not lint in this case, as the statement would
103+
// become the block expression; the block type would become `()` and this may not type correctly
104+
// if the expected type for the block is not `()`.
105+
return;
106+
}
95107
}
96108

97109
let semi_span = expr.span.shrink_to_hi().to(stmt.span.shrink_to_hi());

tests/ui/unnecessary_semicolon.edition2021.fixed

+6
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,9 @@ fn no_borrow_issue(a: u32, b: u32) {
5555
None => {},
5656
}
5757
}
58+
59+
fn issue14100() -> bool {
60+
// Removing the `;` would make the block type be `()` instead of `!`, and this could no longer be
61+
// cast into the `bool` function return type.
62+
if return true {};
63+
}

tests/ui/unnecessary_semicolon.edition2024.fixed

+6
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,9 @@ fn no_borrow_issue(a: u32, b: u32) {
5555
None => {},
5656
}
5757
}
58+
59+
fn issue14100() -> bool {
60+
// Removing the `;` would make the block type be `()` instead of `!`, and this could no longer be
61+
// cast into the `bool` function return type.
62+
if return true {};
63+
}

tests/ui/unnecessary_semicolon.rs

+6
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,9 @@ fn no_borrow_issue(a: u32, b: u32) {
5555
None => {},
5656
};
5757
}
58+
59+
fn issue14100() -> bool {
60+
// Removing the `;` would make the block type be `()` instead of `!`, and this could no longer be
61+
// cast into the `bool` function return type.
62+
if return true {};
63+
}

0 commit comments

Comments
 (0)