Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions compiler/rustc_lint/src/unused.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use rustc_ast::util::{classify, parser};
use rustc_ast::visit::{self, Visitor};
use rustc_ast::{self as ast, ExprKind, FnRetTy, ForLoop, HasAttrs as _, StmtKind};
use rustc_data_structures::fx::FxHashMap;
use rustc_errors::MultiSpan;
Expand Down Expand Up @@ -64,6 +65,7 @@ enum UnusedDelimsCtx {
FunctionArg,
MethodArg,
AssignedValue,
AssignedValueWithWildcard,
AssignedValueLetElse,
IfCond,
WhileCond,
Expand All @@ -85,9 +87,9 @@ impl From<UnusedDelimsCtx> for &'static str {
match ctx {
UnusedDelimsCtx::FunctionArg => "function argument",
UnusedDelimsCtx::MethodArg => "method argument",
UnusedDelimsCtx::AssignedValue | UnusedDelimsCtx::AssignedValueLetElse => {
"assigned value"
}
UnusedDelimsCtx::AssignedValue
| UnusedDelimsCtx::AssignedValueWithWildcard
| UnusedDelimsCtx::AssignedValueLetElse => "assigned value",
UnusedDelimsCtx::IfCond => "`if` condition",
UnusedDelimsCtx::WhileCond => "`while` condition",
UnusedDelimsCtx::ForIterExpr => "`for` iterator expression",
Expand All @@ -104,6 +106,32 @@ impl From<UnusedDelimsCtx> for &'static str {
}
}

fn expr_contains_wildcard(expr: &ast::Expr) -> bool {
struct WildcardVisitor {
found: bool,
}

impl<'ast> Visitor<'ast> for WildcardVisitor {
fn visit_expr(&mut self, expr: &'ast ast::Expr) {
if self.found {
return;
}
match &expr.kind {
ast::ExprKind::Underscore
| ast::ExprKind::Range(None, None, ast::RangeLimits::HalfOpen) => self.found = true,
ast::ExprKind::Struct(expr) if matches!(expr.rest, ast::StructRest::Rest(_)) => {
self.found = true;
}
_ => visit::walk_expr(self, expr),
}
}
}

let mut visitor = WildcardVisitor { found: false };
visitor.visit_expr(expr);
visitor.found
}

/// Used by both `UnusedParens` and `UnusedBraces` to prevent code duplication.
trait UnusedDelimLint {
const DELIM_STR: &'static str;
Expand Down Expand Up @@ -412,7 +440,15 @@ trait UnusedDelimLint {

Index(_, ref value, _) => (value, UnusedDelimsCtx::IndexExpr, false, None, None, false),

Assign(_, ref value, _) | AssignOp(.., ref value) => {
Assign(ref lhs, ref value, _) => {
let ctx = if expr_contains_wildcard(lhs) {
UnusedDelimsCtx::AssignedValueWithWildcard
} else {
UnusedDelimsCtx::AssignedValue
};
(value, ctx, false, None, None, false)
}
AssignOp(.., ref value) => {
(value, UnusedDelimsCtx::AssignedValue, false, None, None, false)
}
// either function/method call, or something this lint doesn't care about
Expand Down Expand Up @@ -1101,6 +1137,7 @@ impl UnusedDelimLint for UnusedBraces {
&& (ctx != UnusedDelimsCtx::AnonConst
|| (matches!(expr.kind, ast::ExprKind::Lit(_))
&& !expr.span.from_expansion()))
&& ctx != UnusedDelimsCtx::AssignedValueWithWildcard

@fmease fmease Nov 2, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's too coarse. Consider:

fn main() {
    let e = String::new();
    _ = { (e,) };
}

The braces are indeed unnecessary because the tuple construction (_,) already moves/drops e regardless of whether there are any { }. But now under your PR we no longer lint here.

The tuple construction is just an example.

Should at least be a FIXME. Not sure if it's worth blocking this PR on fixing this new class of false negatives...

View changes since the review

&& ctx != UnusedDelimsCtx::ClosureBody
&& !cx.sess().source_map().is_multiline(value.span)
&& value.attrs.is_empty()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//@ run-pass

#![deny(unused_braces)]

use std::cell::Cell;

struct Error;
struct NotCopy;

struct DropCounter<'a>(&'a Cell<usize>);

impl Drop for DropCounter<'_> {
fn drop(&mut self) {
self.0.set(self.0.get() + 1);
}
}

struct Pair<'a> {
first: DropCounter<'a>,
_second: DropCounter<'a>,
}

fn wildcard_assignment_moves_value() {
let e = NotCopy;
_ = { e };
}

fn tuple_wildcard_assignment_moves_value() {
let e = (NotCopy, Error);
(_, Error) = { e };
}

fn main() {
wildcard_assignment_moves_value();
tuple_wildcard_assignment_moves_value();

let drops = Cell::new(0);

let e = (DropCounter(&drops), DropCounter(&drops));
(..) = { e };
assert_eq!(drops.get(), 2);

let e = [DropCounter(&drops), DropCounter(&drops)];
[..] = { e };
assert_eq!(drops.get(), 4);

let e = (Error, DropCounter(&drops));
(Error, ..) = { e };
assert_eq!(drops.get(), 5);

let first;
let e = [DropCounter(&drops), DropCounter(&drops)];
[first, ..] = { e };
assert_eq!(drops.get(), 6);
drop(first);
assert_eq!(drops.get(), 7);

let e = (Error, [DropCounter(&drops), DropCounter(&drops)]);
(Error, [..]) = { e };
assert_eq!(drops.get(), 9);

let e = Pair { first: DropCounter(&drops), _second: DropCounter(&drops) };
Pair { .. } = { e };
assert_eq!(drops.get(), 11);

let first;
let e = Pair { first: DropCounter(&drops), _second: DropCounter(&drops) };
Pair { first, .. } = { e };
assert_eq!(drops.get(), 12);
drop(first);
assert_eq!(drops.get(), 13);
}
Loading