1
+ use clippy_config:: Conf ;
1
2
use clippy_utils:: diagnostics:: span_lint_and_sugg;
3
+ use clippy_utils:: msrvs:: { self , Msrv } ;
2
4
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} ;
5
6
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;
8
16
use rustc_span:: { BytePos , Span } ;
9
17
10
18
declare_clippy_lint ! {
@@ -32,39 +40,66 @@ declare_clippy_lint! {
32
40
"needless unit expression"
33
41
}
34
42
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
+ }
36
54
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 )
44
69
{
45
70
// 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
+ {
47
76
return ;
48
77
}
49
78
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) ;
51
90
}
52
91
}
53
92
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 ( )
62
98
{
63
- let sp = expr. span ;
64
99
span_lint_and_sugg (
65
100
cx,
66
101
UNUSED_UNIT ,
67
- sp ,
102
+ expr . span ,
68
103
"unneeded unit expression" ,
69
104
"remove the final `()`" ,
70
105
String :: new ( ) ,
@@ -73,40 +108,94 @@ impl EarlyLintPass for UnusedUnit {
73
108
}
74
109
}
75
110
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
+ ) ;
92
125
}
93
126
}
94
127
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 > ) {
96
129
let segments = & poly. trait_ref . path . segments ;
97
130
98
131
if segments. len ( ) == 1
99
132
&& [ "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)
104
142
{
105
- lint_unneeded_unit_return ( cx, ty , generic_args . span ) ;
143
+ lint_unneeded_unit_return ( cx, hir_ty . span , poly . span ) ;
106
144
}
107
145
}
108
146
}
109
147
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
+
110
199
// get the def site
111
200
#[ must_use]
112
201
fn get_def ( span : Span ) -> Option < Span > {
@@ -117,24 +206,15 @@ fn get_def(span: Span) -> Option<Span> {
117
206
}
118
207
}
119
208
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 ) {
130
210
let ( ret_span, appl) =
131
- span. with_hi ( ty . span . hi ( ) )
211
+ span. with_hi ( ty_span . hi ( ) )
132
212
. 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| {
135
215
(
136
216
#[ 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 ) ) ,
138
218
Applicability :: MachineApplicable ,
139
219
)
140
220
} )
0 commit comments