unnecessary_semicolon: do not lint if it may cause borrow errors

Before edition 2024, some temporaries used in scrutinees in 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.
This commit is contained in:
Samuel Tardieu 2025-01-20 23:22:20 +01:00
parent 71ba2cf1e5
commit 9dca770aec
7 changed files with 243 additions and 6 deletions

View file

@ -973,6 +973,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
store.register_late_pass(|_| Box::new(unnecessary_literal_bound::UnnecessaryLiteralBound));
store.register_late_pass(move |_| Box::new(arbitrary_source_item_ordering::ArbitrarySourceItemOrdering::new(conf)));
store.register_late_pass(|_| Box::new(unneeded_struct_pattern::UnneededStructPattern));
store.register_late_pass(|_| Box::new(unnecessary_semicolon::UnnecessarySemicolon));
store.register_late_pass(|_| Box::<unnecessary_semicolon::UnnecessarySemicolon>::default());
// add lints here, do not remove this comment, it's used in `new_lint`
}

View file

@ -1,8 +1,10 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::leaks_droppable_temporary_with_limited_lifetime;
use rustc_errors::Applicability;
use rustc_hir::{ExprKind, MatchSource, Stmt, StmtKind};
use rustc_hir::{Block, ExprKind, HirId, MatchSource, Stmt, StmtKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass;
use rustc_session::impl_lint_pass;
use rustc_span::edition::Edition::Edition2021;
declare_clippy_lint! {
/// ### What it does
@ -33,10 +35,50 @@ declare_clippy_lint! {
"unnecessary semicolon after expression returning `()`"
}
declare_lint_pass!(UnnecessarySemicolon => [UNNECESSARY_SEMICOLON]);
#[derive(Default)]
pub struct UnnecessarySemicolon {
last_statements: Vec<HirId>,
}
impl LateLintPass<'_> for UnnecessarySemicolon {
fn check_stmt(&mut self, cx: &LateContext<'_>, stmt: &Stmt<'_>) {
impl_lint_pass!(UnnecessarySemicolon => [UNNECESSARY_SEMICOLON]);
impl UnnecessarySemicolon {
/// Enter or leave a block, remembering the last statement of the block.
fn handle_block(&mut self, cx: &LateContext<'_>, block: &Block<'_>, enter: bool) {
// Up to edition 2021, removing the semicolon of the last statement of a block
// may result in the scrutinee temporary values to live longer than the block
// variables. To avoid this problem, we do not lint the last statement of an
// expressionless block.
if cx.tcx.sess.edition() <= Edition2021
&& block.expr.is_none()
&& let Some(last_stmt) = block.stmts.last()
{
if enter {
self.last_statements.push(last_stmt.hir_id);
} else {
self.last_statements.pop();
}
}
}
/// Checks if `stmt` is the last statement in an expressionless block for edition ≤ 2021.
fn is_last_in_block(&self, stmt: &Stmt<'_>) -> bool {
self.last_statements
.last()
.is_some_and(|last_stmt_id| last_stmt_id == &stmt.hir_id)
}
}
impl<'tcx> LateLintPass<'tcx> for UnnecessarySemicolon {
fn check_block(&mut self, cx: &LateContext<'_>, block: &Block<'_>) {
self.handle_block(cx, block, true);
}
fn check_block_post(&mut self, cx: &LateContext<'_>, block: &Block<'_>) {
self.handle_block(cx, block, false);
}
fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &Stmt<'tcx>) {
// rustfmt already takes care of removing semicolons at the end
// of loops.
if let StmtKind::Semi(expr) = stmt.kind
@ -48,6 +90,10 @@ impl LateLintPass<'_> for UnnecessarySemicolon {
)
&& cx.typeck_results().expr_ty(expr) == cx.tcx.types.unit
{
if self.is_last_in_block(stmt) && leaks_droppable_temporary_with_limited_lifetime(cx, expr) {
return;
}
let semi_span = expr.span.shrink_to_hi().to(stmt.span.shrink_to_hi());
span_lint_and_sugg(
cx,