Skip to content

Commit c024c13

Browse files
authored
unnecessary_semicolon: do not lint if it may cause borrow errors (rust-lang#14049)
Before edition 2024, some temporaries used in scrutinees of a `match` used as the last expression of a block may outlive some referenced local variables. Prevent those cases from happening by checking that alive temporaries with significant drop do have a static lifetime. The check is performed only for edition 2021 and earlier, and for the last statement if it would become the last expression of the block. changelog: [`unnecessary_semicolon`]: prevent borrow errors in editions lower than 2024 r? @y21
2 parents 67e6bf3 + 9dca770 commit c024c13

9 files changed

+269
-28
lines changed

clippy_lints/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
973973
store.register_late_pass(|_| Box::new(unnecessary_literal_bound::UnnecessaryLiteralBound));
974974
store.register_late_pass(move |_| Box::new(arbitrary_source_item_ordering::ArbitrarySourceItemOrdering::new(conf)));
975975
store.register_late_pass(|_| Box::new(unneeded_struct_pattern::UnneededStructPattern));
976-
store.register_late_pass(|_| Box::new(unnecessary_semicolon::UnnecessarySemicolon));
976+
store.register_late_pass(|_| Box::<unnecessary_semicolon::UnnecessarySemicolon>::default());
977977
// add lints here, do not remove this comment, it's used in `new_lint`
978978
}

clippy_lints/src/returns.rs

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_hir_and_then};
22
use clippy_utils::source::{SpanRangeExt, snippet_with_context};
33
use clippy_utils::sugg::has_enclosing_paren;
4-
use clippy_utils::visitors::{Descend, for_each_expr, for_each_unconsumed_temporary};
4+
use clippy_utils::visitors::{Descend, for_each_expr};
55
use clippy_utils::{
6-
binary_expr_needs_parentheses, fn_def_id, is_from_proc_macro, is_inside_let_else, is_res_lang_ctor, path_res,
7-
path_to_local_id, span_contains_cfg, span_find_starting_semi,
6+
binary_expr_needs_parentheses, fn_def_id, is_from_proc_macro, is_inside_let_else, is_res_lang_ctor,
7+
leaks_droppable_temporary_with_limited_lifetime, path_res, path_to_local_id, span_contains_cfg,
8+
span_find_starting_semi,
89
};
910
use core::ops::ControlFlow;
1011
use rustc_ast::MetaItemInner;
@@ -389,22 +390,8 @@ fn check_final_expr<'tcx>(
389390
}
390391
};
391392

392-
if let Some(inner) = inner {
393-
if for_each_unconsumed_temporary(cx, inner, |temporary_ty| {
394-
if temporary_ty.has_significant_drop(cx.tcx, cx.typing_env())
395-
&& temporary_ty
396-
.walk()
397-
.any(|arg| matches!(arg.unpack(), GenericArgKind::Lifetime(re) if !re.is_static()))
398-
{
399-
ControlFlow::Break(())
400-
} else {
401-
ControlFlow::Continue(())
402-
}
403-
})
404-
.is_break()
405-
{
406-
return;
407-
}
393+
if inner.is_some_and(|inner| leaks_droppable_temporary_with_limited_lifetime(cx, inner)) {
394+
return;
408395
}
409396

410397
if ret_span.from_expansion() || is_from_proc_macro(cx, expr) {

clippy_lints/src/unnecessary_semicolon.rs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::leaks_droppable_temporary_with_limited_lifetime;
23
use rustc_errors::Applicability;
3-
use rustc_hir::{ExprKind, MatchSource, Stmt, StmtKind};
4+
use rustc_hir::{Block, ExprKind, HirId, MatchSource, Stmt, StmtKind};
45
use rustc_lint::{LateContext, LateLintPass};
5-
use rustc_session::declare_lint_pass;
6+
use rustc_session::impl_lint_pass;
7+
use rustc_span::edition::Edition::Edition2021;
68

79
declare_clippy_lint! {
810
/// ### What it does
@@ -33,10 +35,50 @@ declare_clippy_lint! {
3335
"unnecessary semicolon after expression returning `()`"
3436
}
3537

36-
declare_lint_pass!(UnnecessarySemicolon => [UNNECESSARY_SEMICOLON]);
38+
#[derive(Default)]
39+
pub struct UnnecessarySemicolon {
40+
last_statements: Vec<HirId>,
41+
}
42+
43+
impl_lint_pass!(UnnecessarySemicolon => [UNNECESSARY_SEMICOLON]);
44+
45+
impl UnnecessarySemicolon {
46+
/// Enter or leave a block, remembering the last statement of the block.
47+
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()
54+
&& let Some(last_stmt) = block.stmts.last()
55+
{
56+
if enter {
57+
self.last_statements.push(last_stmt.hir_id);
58+
} else {
59+
self.last_statements.pop();
60+
}
61+
}
62+
}
63+
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 {
66+
self.last_statements
67+
.last()
68+
.is_some_and(|last_stmt_id| last_stmt_id == &stmt.hir_id)
69+
}
70+
}
71+
72+
impl<'tcx> LateLintPass<'tcx> for UnnecessarySemicolon {
73+
fn check_block(&mut self, cx: &LateContext<'_>, block: &Block<'_>) {
74+
self.handle_block(cx, block, true);
75+
}
3776

38-
impl LateLintPass<'_> for UnnecessarySemicolon {
39-
fn check_stmt(&mut self, cx: &LateContext<'_>, stmt: &Stmt<'_>) {
77+
fn check_block_post(&mut self, cx: &LateContext<'_>, block: &Block<'_>) {
78+
self.handle_block(cx, block, false);
79+
}
80+
81+
fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &Stmt<'tcx>) {
4082
// rustfmt already takes care of removing semicolons at the end
4183
// of loops.
4284
if let StmtKind::Semi(expr) = stmt.kind
@@ -48,6 +90,10 @@ impl LateLintPass<'_> for UnnecessarySemicolon {
4890
)
4991
&& cx.typeck_results().expr_ty(expr) == cx.tcx.types.unit
5092
{
93+
if self.is_last_in_block(stmt) && leaks_droppable_temporary_with_limited_lifetime(cx, expr) {
94+
return;
95+
}
96+
5197
let semi_span = expr.span.shrink_to_hi().to(stmt.span.shrink_to_hi());
5298
span_lint_and_sugg(
5399
cx,

clippy_utils/src/lib.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,15 @@ use rustc_middle::ty::adjustment::{Adjust, Adjustment, AutoBorrow};
117117
use rustc_middle::ty::fast_reject::SimplifiedType;
118118
use rustc_middle::ty::layout::IntegerExt;
119119
use rustc_middle::ty::{
120-
self as rustc_ty, Binder, BorrowKind, ClosureKind, EarlyBinder, FloatTy, GenericArgsRef, IntTy, Ty, TyCtxt,
121-
TypeVisitableExt, UintTy, UpvarCapture,
120+
self as rustc_ty, Binder, BorrowKind, ClosureKind, EarlyBinder, FloatTy, GenericArgKind, GenericArgsRef, IntTy, Ty,
121+
TyCtxt, TypeVisitableExt, UintTy, UpvarCapture,
122122
};
123123
use rustc_span::hygiene::{ExpnKind, MacroKind};
124124
use rustc_span::source_map::SourceMap;
125125
use rustc_span::symbol::{Ident, Symbol, kw};
126126
use rustc_span::{InnerSpan, Span, sym};
127127
use rustc_target::abi::Integer;
128-
use visitors::Visitable;
128+
use visitors::{Visitable, for_each_unconsumed_temporary};
129129

130130
use crate::consts::{ConstEvalCtxt, Constant, mir_to_const};
131131
use crate::higher::Range;
@@ -3465,3 +3465,20 @@ pub fn is_receiver_of_method_call(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool
34653465
}
34663466
false
34673467
}
3468+
3469+
/// Returns true if `expr` creates any temporary whose type references a non-static lifetime and has
3470+
/// a significant drop and does not consume it.
3471+
pub fn leaks_droppable_temporary_with_limited_lifetime<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> bool {
3472+
for_each_unconsumed_temporary(cx, expr, |temporary_ty| {
3473+
if temporary_ty.has_significant_drop(cx.tcx, cx.typing_env())
3474+
&& temporary_ty
3475+
.walk()
3476+
.any(|arg| matches!(arg.unpack(), GenericArgKind::Lifetime(re) if !re.is_static()))
3477+
{
3478+
ControlFlow::Break(())
3479+
} else {
3480+
ControlFlow::Continue(())
3481+
}
3482+
})
3483+
.is_break()
3484+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//@revisions: edition2021 edition2024
2+
//@[edition2021] edition:2021
3+
//@[edition2024] edition:2024
4+
5+
#![warn(clippy::unnecessary_semicolon)]
6+
#![feature(postfix_match)]
7+
#![allow(clippy::single_match)]
8+
9+
fn no_lint(mut x: u32) -> Option<u32> {
10+
Some(())?;
11+
12+
{
13+
let y = 3;
14+
dbg!(x + y)
15+
};
16+
17+
{
18+
let (mut a, mut b) = (10, 20);
19+
(a, b) = (b + 1, a + 1);
20+
}
21+
22+
Some(0)
23+
}
24+
25+
fn main() {
26+
let mut a = 3;
27+
if a == 2 {
28+
println!("This is weird");
29+
}
30+
//~^ ERROR: unnecessary semicolon
31+
32+
a.match {
33+
3 => println!("three"),
34+
_ => println!("not three"),
35+
}
36+
//~^ ERROR: unnecessary semicolon
37+
}
38+
39+
// This is a problem in edition 2021 and below
40+
fn borrow_issue() {
41+
let v = std::cell::RefCell::new(Some(vec![1]));
42+
match &*v.borrow() {
43+
Some(v) => {
44+
dbg!(v);
45+
},
46+
None => {},
47+
};
48+
}
49+
50+
fn no_borrow_issue(a: u32, b: u32) {
51+
match Some(a + b) {
52+
Some(v) => {
53+
dbg!(v);
54+
},
55+
None => {},
56+
}
57+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
error: unnecessary semicolon
2+
--> tests/ui/unnecessary_semicolon.rs:29:6
3+
|
4+
LL | };
5+
| ^ help: remove
6+
|
7+
= note: `-D clippy::unnecessary-semicolon` implied by `-D warnings`
8+
= help: to override `-D warnings` add `#[allow(clippy::unnecessary_semicolon)]`
9+
10+
error: unnecessary semicolon
11+
--> tests/ui/unnecessary_semicolon.rs:35:6
12+
|
13+
LL | };
14+
| ^ help: remove
15+
16+
error: unnecessary semicolon
17+
--> tests/ui/unnecessary_semicolon.rs:56:6
18+
|
19+
LL | };
20+
| ^ help: remove
21+
22+
error: aborting due to 3 previous errors
23+
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//@revisions: edition2021 edition2024
2+
//@[edition2021] edition:2021
3+
//@[edition2024] edition:2024
4+
5+
#![warn(clippy::unnecessary_semicolon)]
6+
#![feature(postfix_match)]
7+
#![allow(clippy::single_match)]
8+
9+
fn no_lint(mut x: u32) -> Option<u32> {
10+
Some(())?;
11+
12+
{
13+
let y = 3;
14+
dbg!(x + y)
15+
};
16+
17+
{
18+
let (mut a, mut b) = (10, 20);
19+
(a, b) = (b + 1, a + 1);
20+
}
21+
22+
Some(0)
23+
}
24+
25+
fn main() {
26+
let mut a = 3;
27+
if a == 2 {
28+
println!("This is weird");
29+
}
30+
//~^ ERROR: unnecessary semicolon
31+
32+
a.match {
33+
3 => println!("three"),
34+
_ => println!("not three"),
35+
}
36+
//~^ ERROR: unnecessary semicolon
37+
}
38+
39+
// This is a problem in edition 2021 and below
40+
fn borrow_issue() {
41+
let v = std::cell::RefCell::new(Some(vec![1]));
42+
match &*v.borrow() {
43+
Some(v) => {
44+
dbg!(v);
45+
},
46+
None => {},
47+
}
48+
}
49+
50+
fn no_borrow_issue(a: u32, b: u32) {
51+
match Some(a + b) {
52+
Some(v) => {
53+
dbg!(v);
54+
},
55+
None => {},
56+
}
57+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
error: unnecessary semicolon
2+
--> tests/ui/unnecessary_semicolon.rs:29:6
3+
|
4+
LL | };
5+
| ^ help: remove
6+
|
7+
= note: `-D clippy::unnecessary-semicolon` implied by `-D warnings`
8+
= help: to override `-D warnings` add `#[allow(clippy::unnecessary_semicolon)]`
9+
10+
error: unnecessary semicolon
11+
--> tests/ui/unnecessary_semicolon.rs:35:6
12+
|
13+
LL | };
14+
| ^ help: remove
15+
16+
error: unnecessary semicolon
17+
--> tests/ui/unnecessary_semicolon.rs:47:6
18+
|
19+
LL | };
20+
| ^ help: remove
21+
22+
error: unnecessary semicolon
23+
--> tests/ui/unnecessary_semicolon.rs:56:6
24+
|
25+
LL | };
26+
| ^ help: remove
27+
28+
error: aborting due to 4 previous errors
29+

tests/ui/unnecessary_semicolon.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
//@revisions: edition2021 edition2024
2+
//@[edition2021] edition:2021
3+
//@[edition2024] edition:2024
4+
15
#![warn(clippy::unnecessary_semicolon)]
26
#![feature(postfix_match)]
7+
#![allow(clippy::single_match)]
38

49
fn no_lint(mut x: u32) -> Option<u32> {
510
Some(())?;
@@ -30,3 +35,23 @@ fn main() {
3035
};
3136
//~^ ERROR: unnecessary semicolon
3237
}
38+
39+
// This is a problem in edition 2021 and below
40+
fn borrow_issue() {
41+
let v = std::cell::RefCell::new(Some(vec![1]));
42+
match &*v.borrow() {
43+
Some(v) => {
44+
dbg!(v);
45+
},
46+
None => {},
47+
};
48+
}
49+
50+
fn no_borrow_issue(a: u32, b: u32) {
51+
match Some(a + b) {
52+
Some(v) => {
53+
dbg!(v);
54+
},
55+
None => {},
56+
};
57+
}

0 commit comments

Comments
 (0)