Skip to content

Commit 78d6b2e

Browse files
committed
Do not remove semicolon if it changes the block type
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).
1 parent e02c885 commit 78d6b2e

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)