Skip to content

Commit d9a3982

Browse files
committed
fix: unused_unit suggests wrongly when unit never type fallback
1 parent d19c651 commit d9a3982

File tree

6 files changed

+192
-70
lines changed

6 files changed

+192
-70
lines changed

clippy_lints/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -770,7 +770,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
770770
store.register_early_pass(|| Box::new(formatting::Formatting));
771771
store.register_early_pass(|| Box::new(misc_early::MiscEarlyLints));
772772
store.register_late_pass(|_| Box::new(redundant_closure_call::RedundantClosureCall));
773-
store.register_early_pass(|| Box::new(unused_unit::UnusedUnit));
773+
store.register_late_pass(move |_| Box::new(unused_unit::UnusedUnit::new(conf)));
774774
store.register_late_pass(|_| Box::new(returns::Return));
775775
store.register_late_pass(move |tcx| Box::new(collapsible_if::CollapsibleIf::new(tcx, conf)));
776776
store.register_late_pass(|_| Box::new(items_after_statements::ItemsAfterStatements));

clippy_lints/src/unused_unit.rs

Lines changed: 140 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1+
use clippy_config::Conf;
12
use clippy_utils::diagnostics::span_lint_and_sugg;
3+
use clippy_utils::msrvs::{self, Msrv};
24
use clippy_utils::source::{SpanRangeExt, position_before_rarrow};
3-
use rustc_ast::visit::FnKind;
4-
use rustc_ast::{ClosureBinder, ast};
5+
use clippy_utils::{is_never_expr, is_unit_expr};
56
use rustc_errors::Applicability;
6-
use rustc_lint::{EarlyContext, EarlyLintPass};
7-
use rustc_session::declare_lint_pass;
7+
use rustc_hir::def_id::LocalDefId;
8+
use rustc_hir::intravisit::FnKind;
9+
use rustc_hir::{
10+
AssocItemConstraintKind, Block, Body, Expr, ExprKind, FnDecl, FnRetTy, GenericArgsParentheses, Node, PolyTraitRef,
11+
Term, Ty, TyKind,
12+
};
13+
use rustc_lexer::{TokenKind, tokenize};
14+
use rustc_lint::{LateContext, LateLintPass};
15+
use rustc_session::impl_lint_pass;
816
use rustc_span::{BytePos, Span};
917

1018
declare_clippy_lint! {
@@ -32,39 +40,66 @@ declare_clippy_lint! {
3240
"needless unit expression"
3341
}
3442

35-
declare_lint_pass!(UnusedUnit => [UNUSED_UNIT]);
43+
pub struct UnusedUnit {
44+
msrv: Msrv,
45+
}
46+
47+
impl_lint_pass!(UnusedUnit => [UNUSED_UNIT]);
48+
49+
impl UnusedUnit {
50+
pub fn new(conf: &'static Conf) -> Self {
51+
Self { msrv: conf.msrv }
52+
}
53+
}
3654

37-
impl EarlyLintPass for UnusedUnit {
38-
fn check_fn(&mut self, cx: &EarlyContext<'_>, kind: FnKind<'_>, span: Span, _: ast::NodeId) {
39-
if let ast::FnRetTy::Ty(ref ty) = kind.decl().output
40-
&& let ast::TyKind::Tup(ref vals) = ty.kind
41-
&& vals.is_empty()
42-
&& !ty.span.from_expansion()
43-
&& get_def(span) == get_def(ty.span)
55+
impl<'tcx> LateLintPass<'tcx> for UnusedUnit {
56+
fn check_fn(
57+
&mut self,
58+
cx: &LateContext<'tcx>,
59+
kind: FnKind<'tcx>,
60+
decl: &'tcx FnDecl<'tcx>,
61+
body: &'tcx Body<'tcx>,
62+
span: Span,
63+
def_id: LocalDefId,
64+
) {
65+
if let FnRetTy::Return(hir_ty) = decl.output
66+
&& is_unit_ty(hir_ty)
67+
&& !hir_ty.span.from_expansion()
68+
&& get_def(span) == get_def(hir_ty.span)
4469
{
4570
// implicit types in closure signatures are forbidden when `for<...>` is present
46-
if let FnKind::Closure(&ClosureBinder::For { .. }, ..) = kind {
71+
if let FnKind::Closure = kind
72+
&& let Node::Expr(expr) = cx.tcx.hir_node_by_def_id(def_id)
73+
&& let ExprKind::Closure(closure) = expr.kind
74+
&& !closure.bound_generic_params.is_empty()
75+
{
4776
return;
4877
}
4978

50-
lint_unneeded_unit_return(cx, ty, span);
79+
// unit never type fallback is no longer supported since Rust 2024. For more information,
80+
// see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/never-type-fallback.html>
81+
if self.msrv.meets(cx, msrvs::UNIT_NEVER_TYPE_FALLBACK)
82+
&& let ExprKind::Block(block, _) = body.value.kind
83+
&& let Some(expr) = block.expr
84+
&& is_never_expr(cx, expr).is_some()
85+
{
86+
return;
87+
}
88+
89+
lint_unneeded_unit_return(cx, hir_ty.span, span);
5190
}
5291
}
5392

54-
fn check_block(&mut self, cx: &EarlyContext<'_>, block: &ast::Block) {
55-
if let Some(stmt) = block.stmts.last()
56-
&& let ast::StmtKind::Expr(ref expr) = stmt.kind
57-
&& is_unit_expr(expr)
58-
&& let ctxt = block.span.ctxt()
59-
&& stmt.span.ctxt() == ctxt
60-
&& expr.span.ctxt() == ctxt
61-
&& expr.attrs.is_empty()
93+
fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx Block<'tcx>) {
94+
if let Some(expr) = block.expr
95+
&& !expr.span.from_expansion()
96+
&& is_block_expr_unit(cx, block, expr)
97+
&& cx.tcx.hir_attrs(expr.hir_id).is_empty()
6298
{
63-
let sp = expr.span;
6499
span_lint_and_sugg(
65100
cx,
66101
UNUSED_UNIT,
67-
sp,
102+
expr.span,
68103
"unneeded unit expression",
69104
"remove the final `()`",
70105
String::new(),
@@ -73,40 +108,94 @@ impl EarlyLintPass for UnusedUnit {
73108
}
74109
}
75110

76-
fn check_expr(&mut self, cx: &EarlyContext<'_>, e: &ast::Expr) {
77-
match e.kind {
78-
ast::ExprKind::Ret(Some(ref expr)) | ast::ExprKind::Break(_, Some(ref expr)) => {
79-
if is_unit_expr(expr) && !expr.span.from_expansion() {
80-
span_lint_and_sugg(
81-
cx,
82-
UNUSED_UNIT,
83-
expr.span,
84-
"unneeded `()`",
85-
"remove the `()`",
86-
String::new(),
87-
Applicability::MachineApplicable,
88-
);
89-
}
90-
},
91-
_ => (),
111+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
112+
if let ExprKind::Ret(Some(expr)) | ExprKind::Break(_, Some(expr)) = expr.kind
113+
&& is_unit_expr(expr)
114+
&& !expr.span.from_expansion()
115+
{
116+
span_lint_and_sugg(
117+
cx,
118+
UNUSED_UNIT,
119+
expr.span,
120+
"unneeded `()`",
121+
"remove the `()`",
122+
String::new(),
123+
Applicability::MachineApplicable,
124+
);
92125
}
93126
}
94127

95-
fn check_poly_trait_ref(&mut self, cx: &EarlyContext<'_>, poly: &ast::PolyTraitRef) {
128+
fn check_poly_trait_ref(&mut self, cx: &LateContext<'tcx>, poly: &'tcx PolyTraitRef<'tcx>) {
96129
let segments = &poly.trait_ref.path.segments;
97130

98131
if segments.len() == 1
99132
&& ["Fn", "FnMut", "FnOnce"].contains(&segments[0].ident.name.as_str())
100-
&& let Some(args) = &segments[0].args
101-
&& let ast::GenericArgs::Parenthesized(generic_args) = &**args
102-
&& let ast::FnRetTy::Ty(ty) = &generic_args.output
103-
&& ty.kind.is_unit()
133+
&& let Some(args) = segments[0].args
134+
&& args.parenthesized == GenericArgsParentheses::ParenSugar
135+
&& let constraints = &args.constraints
136+
&& constraints.len() == 1
137+
&& constraints[0].ident.name.as_str() == "Output"
138+
&& let AssocItemConstraintKind::Equality { term: Term::Ty(hir_ty) } = constraints[0].kind
139+
&& !args.span_ext.between(poly.span.shrink_to_hi()).is_empty()
140+
&& !hir_ty.span.from_expansion()
141+
&& is_unit_ty(hir_ty)
104142
{
105-
lint_unneeded_unit_return(cx, ty, generic_args.span);
143+
lint_unneeded_unit_return(cx, hir_ty.span, poly.span);
106144
}
107145
}
108146
}
109147

148+
fn is_unit_ty(ty: &Ty<'_>) -> bool {
149+
matches!(ty.kind, TyKind::Tup([]))
150+
}
151+
152+
/// Check if the block expression is a unit expression by itself and is not the result of any
153+
/// complex expression. Currently, it checks for:
154+
/// - Empty block
155+
/// - Unit expression with nested parens
156+
/// - Unit expression from a passthrough macro
157+
///
158+
/// A passthrough macro refers to a macro that expands its input as-is. Currently it is very hard to
159+
/// detect it because its expansion result is completely from user code, making its `SyntaxContext`
160+
/// root. For more information, see <https://github.com/rust-lang/rust-clippy/issues/4076>
161+
fn is_block_expr_unit(cx: &LateContext<'_>, block: &Block<'_>, expr: &Expr<'_>) -> bool {
162+
// Check for empty block
163+
if matches!(expr.kind, ExprKind::Tup([])) {
164+
// Check for unit expression with nested parens
165+
let Ok(snippet) = cx.tcx.sess.source_map().span_to_snippet(expr.span) else {
166+
return false;
167+
};
168+
let mut has_first_paren = false;
169+
for token in tokenize(&snippet) {
170+
match token.kind {
171+
TokenKind::OpenParen if has_first_paren => return false,
172+
TokenKind::OpenParen => has_first_paren = true,
173+
_ => {},
174+
}
175+
}
176+
177+
// Check for passthrough macro by tokenizing the source before the unit expression
178+
let before = if let [.., stmt] = &block.stmts {
179+
stmt.span.between(expr.span)
180+
} else {
181+
block.span.with_lo(BytePos(block.span.lo().0 + 1)).until(expr.span)
182+
};
183+
let Ok(snippet) = cx.tcx.sess.source_map().span_to_snippet(before) else {
184+
return false;
185+
};
186+
for token in tokenize(&snippet) {
187+
match token.kind {
188+
TokenKind::LineComment { .. } | TokenKind::BlockComment { .. } | TokenKind::Whitespace => {},
189+
_ => return false,
190+
}
191+
}
192+
193+
return true;
194+
}
195+
196+
false
197+
}
198+
110199
// get the def site
111200
#[must_use]
112201
fn get_def(span: Span) -> Option<Span> {
@@ -117,24 +206,15 @@ fn get_def(span: Span) -> Option<Span> {
117206
}
118207
}
119208

120-
// is this expr a `()` unit?
121-
fn is_unit_expr(expr: &ast::Expr) -> bool {
122-
if let ast::ExprKind::Tup(ref vals) = expr.kind {
123-
vals.is_empty()
124-
} else {
125-
false
126-
}
127-
}
128-
129-
fn lint_unneeded_unit_return(cx: &EarlyContext<'_>, ty: &ast::Ty, span: Span) {
209+
fn lint_unneeded_unit_return(cx: &LateContext<'_>, ty_span: Span, span: Span) {
130210
let (ret_span, appl) =
131-
span.with_hi(ty.span.hi())
211+
span.with_hi(ty_span.hi())
132212
.get_source_text(cx)
133-
.map_or((ty.span, Applicability::MaybeIncorrect), |src| {
134-
position_before_rarrow(&src).map_or((ty.span, Applicability::MaybeIncorrect), |rpos| {
213+
.map_or((ty_span, Applicability::MaybeIncorrect), |src| {
214+
position_before_rarrow(&src).map_or((ty_span, Applicability::MaybeIncorrect), |rpos| {
135215
(
136216
#[expect(clippy::cast_possible_truncation)]
137-
ty.span.with_lo(BytePos(span.lo().0 + rpos as u32)),
217+
ty_span.with_lo(BytePos(span.lo().0 + rpos as u32)),
138218
Applicability::MachineApplicable,
139219
)
140220
})

clippy_utils/src/msrvs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ macro_rules! msrv_aliases {
2323
// names may refer to stabilized feature flags or library items
2424
msrv_aliases! {
2525
1,87,0 { OS_STR_DISPLAY, INT_MIDPOINT }
26-
1,85,0 { UINT_FLOAT_MIDPOINT }
26+
1,85,0 { UINT_FLOAT_MIDPOINT, UNIT_NEVER_TYPE_FALLBACK }
2727
1,84,0 { CONST_OPTION_AS_SLICE, MANUAL_DANGLING_PTR }
2828
1,83,0 { CONST_EXTERN_FN, CONST_FLOAT_BITS_CONV, CONST_FLOAT_CLASSIFY, CONST_MUT_REFS, CONST_UNWRAP }
2929
1,82,0 { IS_NONE_OR, REPEAT_N, RAW_REF_OP }

tests/ui/unused_unit.fixed

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,24 @@ mod issue9949 {
120120
()
121121
}
122122
}
123+
124+
#[clippy::msrv = "1.85"]
125+
mod issue14577 {
126+
trait Unit {}
127+
impl Unit for () {}
128+
129+
fn run<R: Unit>(f: impl FnOnce() -> R) {
130+
f();
131+
}
132+
133+
fn bar() {
134+
run(|| -> () { todo!() });
135+
}
136+
137+
struct UnitStruct;
138+
impl UnitStruct {
139+
fn apply<F: for<'c> Fn(&'c mut Self)>(&mut self, f: F) {
140+
todo!()
141+
}
142+
}
143+
}

tests/ui/unused_unit.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,24 @@ mod issue9949 {
120120
()
121121
}
122122
}
123+
124+
#[clippy::msrv = "1.85"]
125+
mod issue14577 {
126+
trait Unit {}
127+
impl Unit for () {}
128+
129+
fn run<R: Unit>(f: impl FnOnce() -> R) {
130+
f();
131+
}
132+
133+
fn bar() {
134+
run(|| -> () { todo!() });
135+
}
136+
137+
struct UnitStruct;
138+
impl UnitStruct {
139+
fn apply<F: for<'c> Fn(&'c mut Self)>(&mut self, f: F) {
140+
todo!()
141+
}
142+
}
143+
}

tests/ui/unused_unit.stderr

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
error: unneeded unit return type
2-
--> tests/ui/unused_unit.rs:20:58
2+
--> tests/ui/unused_unit.rs:20:28
33
|
44
LL | pub fn get_unit<F: Fn() -> (), G>(&self, f: F, _g: G) -> ()
5-
| ^^^^^^ help: remove the `-> ()`
5+
| ^^^^^^ help: remove the `-> ()`
66
|
77
note: the lint level is defined here
88
--> tests/ui/unused_unit.rs:13:9
99
|
1010
LL | #![deny(clippy::unused_unit)]
1111
| ^^^^^^^^^^^^^^^^^^^
1212

13-
error: unneeded unit return type
14-
--> tests/ui/unused_unit.rs:20:28
15-
|
16-
LL | pub fn get_unit<F: Fn() -> (), G>(&self, f: F, _g: G) -> ()
17-
| ^^^^^^ help: remove the `-> ()`
18-
1913
error: unneeded unit return type
2014
--> tests/ui/unused_unit.rs:23:18
2115
|
2216
LL | where G: Fn() -> () {
2317
| ^^^^^^ help: remove the `-> ()`
2418

19+
error: unneeded unit return type
20+
--> tests/ui/unused_unit.rs:20:58
21+
|
22+
LL | pub fn get_unit<F: Fn() -> (), G>(&self, f: F, _g: G) -> ()
23+
| ^^^^^^ help: remove the `-> ()`
24+
2525
error: unneeded unit return type
2626
--> tests/ui/unused_unit.rs:25:26
2727
|

0 commit comments

Comments
 (0)