@@ -3,8 +3,11 @@ use clippy_utils::macros::{is_panic, root_macro_call_first_node};
3
3
use clippy_utils:: ty:: is_type_diagnostic_item;
4
4
use clippy_utils:: visitors:: Visitable ;
5
5
use clippy_utils:: { is_in_test_function, method_chain_args} ;
6
- use rustc_hir:: intravisit:: { self , FnKind , Visitor } ;
7
- use rustc_hir:: { AnonConst , Body , Expr , FnDecl , Item , ItemKind } ;
6
+ use rustc_data_structures:: fx:: FxHashSet ;
7
+ use rustc_hir:: def:: Res ;
8
+ use rustc_hir:: def_id:: DefId ;
9
+ use rustc_hir:: intravisit:: { self , Visitor } ;
10
+ use rustc_hir:: { AnonConst , Expr , ExprKind , Item , ItemKind } ;
8
11
use rustc_lint:: { LateContext , LateLintPass } ;
9
12
use rustc_middle:: hir:: nested_filter;
10
13
use rustc_middle:: ty;
@@ -13,142 +16,216 @@ use rustc_span::{Span, sym};
13
16
14
17
declare_clippy_lint ! {
15
18
/// ### What it does
16
- /// Triggers when a testing function (marked with the `#[ test]` attribute) does not have a way to fail.
19
+ /// Checks for test functions that cannot fail.
17
20
///
18
- /// ### Why restrict this?
19
- /// If a test does not have a way to fail, the developer might be getting false positives from their test suites .
20
- /// The idiomatic way of using test functions should be such that they actually can fail in an erroneous state .
21
+ /// ### Why is this bad ?
22
+ /// A test function that cannot fail might indicate that it does not actually test anything .
23
+ /// It could lead to false positives in test suites, giving a false sense of security .
21
24
///
22
25
/// ### Example
23
- /// ```no_run
26
+ /// ```rust
24
27
/// #[test]
25
- /// fn my_cool_test() {
26
- /// // [...]
27
- /// }
28
- ///
29
- /// #[cfg(test)]
30
- /// mod tests {
31
- /// // [...]
28
+ /// fn my_test() {
29
+ /// let x = 2;
30
+ /// let y = 2;
31
+ /// if x + y != 4 {
32
+ /// eprintln!("this is not a correct test")
33
+ /// }
32
34
/// }
33
- ///
34
35
/// ```
36
+ ///
35
37
/// Use instead:
36
- /// ```no_run
37
- /// #[cfg(test)]
38
- /// mod tests {
39
- /// #[test]
40
- /// fn my_cool_test() {
41
- /// // [...]
42
- /// }
38
+ /// ```rust
39
+ /// #[test]
40
+ /// fn my_test() {
41
+ /// let x = 2;
42
+ /// let y = 2;
43
+ /// assert_eq!(x + y, 4);
43
44
/// }
44
45
/// ```
45
- #[ clippy:: version = "1.70 .0" ]
46
+ #[ clippy:: version = "1.82 .0" ]
46
47
pub TEST_WITHOUT_FAIL_CASE ,
47
48
restriction,
48
- "A test function is outside the testing module. "
49
+ "test function cannot fail because it does not panic or assert "
49
50
}
50
51
51
52
declare_lint_pass ! ( TestWithoutFailCase => [ TEST_WITHOUT_FAIL_CASE ] ) ;
52
53
53
- /*
54
- impl<'tcx> LateLintPass<'tcx> for TestWithoutFailCase {
55
- fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx rustc_hir::Item<'_>) {
56
- if let rustc_hir::ItemKind::Fn(sig, _, body_id) = item.kind {
57
-
58
- }
59
- if is_in_test_function(cx.tcx, item.hir_id()) {
60
- let typck = cx.tcx.typeck(item.id.owner_id.def_id);
61
- let find_panic_visitor = FindPanicUnwrap::find_span(cx, typck, item.body)
62
- span_lint(cx, TEST_WITHOUT_FAIL_CASE, item.span, "test function cannot panic");
63
- }
64
- }
65
- }
66
- */
67
-
68
54
impl < ' tcx > LateLintPass < ' tcx > for TestWithoutFailCase {
69
55
fn check_item ( & mut self , cx : & LateContext < ' tcx > , item : & ' tcx Item < ' tcx > ) {
56
+ // Only interested in functions that are test functions
70
57
if let ItemKind :: Fn ( _, _, body_id) = item. kind
71
58
&& is_in_test_function ( cx. tcx , item. hir_id ( ) )
72
59
{
73
60
let body = cx. tcx . hir ( ) . body ( body_id) ;
74
- let typ = cx. tcx . typeck ( item. owner_id ) ;
75
- let panic_span = FindPanicUnwrap :: find_span ( cx, typ , body) ;
61
+ let typeck_results = cx. tcx . typeck ( item. owner_id ) ;
62
+ let panic_span = SearchPanicIntraFunction :: find_span ( cx, typeck_results , body) ;
76
63
if panic_span. is_none ( ) {
77
- // No way to panic for this test.
78
- #[ expect( clippy:: collapsible_span_lint_calls, reason = "rust-clippy#7797" ) ]
64
+ // No way to panic for this test function
79
65
span_lint_and_then (
80
66
cx,
81
67
TEST_WITHOUT_FAIL_CASE ,
82
68
item. span ,
83
- "this function marked with #[test] has no way to fail " ,
69
+ "this function marked with ` #[test]` cannot fail and will always succeed " ,
84
70
|diag| {
85
- diag. note ( "make sure that something is checked in this test " ) ;
71
+ diag. note ( "consider adding assertions or panics to test failure cases " ) ;
86
72
} ,
87
73
) ;
88
74
}
89
75
}
90
76
}
91
77
}
92
78
93
- struct FindPanicUnwrap < ' a , ' tcx > {
79
+ /// Visitor that searches for expressions that could cause a panic, such as `panic!`,
80
+ /// `assert!`, `unwrap()`, or calls to functions that can panic.
81
+ struct SearchPanicIntraFunction < ' a , ' tcx > {
82
+ /// The lint context
94
83
cx : & ' a LateContext < ' tcx > ,
84
+ /// Whether we are inside a constant context
95
85
is_const : bool ,
86
+ /// The span where a panic was found
96
87
panic_span : Option < Span > ,
88
+ /// Type checking results for the current body
97
89
typeck_results : & ' tcx ty:: TypeckResults < ' tcx > ,
90
+ /// Set of function `DefId`s that have been visited to avoid infinite recursion
91
+ visited_functions : FxHashSet < DefId > ,
98
92
}
99
93
100
- impl < ' a , ' tcx > FindPanicUnwrap < ' a , ' tcx > {
94
+ impl < ' a , ' tcx > SearchPanicIntraFunction < ' a , ' tcx > {
95
+ /// Creates a new `FindPanicUnwrap` visitor
96
+ pub fn new ( cx : & ' a LateContext < ' tcx > , typeck_results : & ' tcx ty:: TypeckResults < ' tcx > ) -> Self {
97
+ Self {
98
+ cx,
99
+ is_const : false ,
100
+ panic_span : None ,
101
+ typeck_results,
102
+ visited_functions : FxHashSet :: default ( ) ,
103
+ }
104
+ }
105
+
106
+ /// Searches for a way to panic in the given body and returns the span if found
101
107
pub fn find_span (
102
108
cx : & ' a LateContext < ' tcx > ,
103
109
typeck_results : & ' tcx ty:: TypeckResults < ' tcx > ,
104
110
body : impl Visitable < ' tcx > ,
105
111
) -> Option < ( Span , bool ) > {
106
- let mut vis = Self {
107
- cx,
108
- is_const : false ,
109
- panic_span : None ,
110
- typeck_results,
111
- } ;
112
- body. visit ( & mut vis) ;
113
- vis. panic_span . map ( |el| ( el, vis. is_const ) )
112
+ let mut visitor = Self :: new ( cx, typeck_results) ;
113
+ body. visit ( & mut visitor) ;
114
+ visitor. panic_span . map ( |span| ( span, visitor. is_const ) )
115
+ }
116
+
117
+ /// Checks the called function to see if it contains a panic
118
+ fn check_called_function ( & mut self , def_id : DefId , span : Span ) {
119
+ // Avoid infinite recursion by checking if we've already visited this function
120
+ if !self . visited_functions . insert ( def_id) {
121
+ return ;
122
+ }
123
+
124
+ if def_id. is_local ( ) {
125
+ let hir = self . cx . tcx . hir ( ) ;
126
+ if let Some ( local_def_id) = def_id. as_local ( ) {
127
+ if let Some ( body) = hir. maybe_body_owned_by ( local_def_id) {
128
+ let typeck_results = self . cx . tcx . typeck ( local_def_id) ;
129
+ let mut new_visitor = SearchPanicIntraFunction {
130
+ cx : self . cx ,
131
+ is_const : false ,
132
+ panic_span : None ,
133
+ typeck_results,
134
+ visited_functions : self . visited_functions . clone ( ) ,
135
+ } ;
136
+ body. visit ( & mut new_visitor) ;
137
+ if let Some ( panic_span) = new_visitor. panic_span {
138
+ self . panic_span = Some ( panic_span) ;
139
+ }
140
+ }
141
+ }
142
+ } else {
143
+ // For external functions, assume they can panic
144
+ self . panic_span = Some ( span) ;
145
+ }
114
146
}
115
147
}
116
148
117
- impl < ' a , ' tcx > Visitor < ' tcx > for FindPanicUnwrap < ' a , ' tcx > {
149
+ impl < ' a , ' tcx > Visitor < ' tcx > for SearchPanicIntraFunction < ' a , ' tcx > {
118
150
type NestedFilter = nested_filter:: OnlyBodies ;
119
151
120
152
fn visit_expr ( & mut self , expr : & ' tcx Expr < ' _ > ) {
121
153
if self . panic_span . is_some ( ) {
154
+ // If we've already found a panic, no need to continue
122
155
return ;
123
156
}
124
157
125
- if let Some ( macro_call) = root_macro_call_first_node ( self . cx , expr) {
126
- if is_panic ( self . cx , macro_call. def_id )
127
- || matches ! (
128
- self . cx. tcx. item_name( macro_call. def_id) . as_str( ) ,
129
- "assert" | "assert_eq" | "assert_ne"
130
- )
131
- {
132
- self . is_const = self . cx . tcx . hir ( ) . is_inside_const_context ( expr. hir_id ) ;
133
- self . panic_span = Some ( macro_call. span ) ;
134
- }
135
- }
158
+ match expr. kind {
159
+ ExprKind :: Call ( callee, args) => {
160
+ // Function call
161
+ if let ExprKind :: Path ( ref qpath) = callee. kind {
162
+ if let Res :: Def ( _, def_id) = self . cx . qpath_res ( qpath, callee. hir_id ) {
163
+ self . check_called_function ( def_id, expr. span ) ;
164
+ if self . panic_span . is_some ( ) {
165
+ return ;
166
+ }
167
+ }
168
+ }
169
+ // Visit callee and arguments
170
+ self . visit_expr ( callee) ;
171
+ for arg in args {
172
+ self . visit_expr ( arg) ;
173
+ }
174
+ } ,
175
+ ExprKind :: MethodCall ( _, receiver, args, _) => {
176
+ // Method call
177
+ if let Some ( def_id) = self . typeck_results . type_dependent_def_id ( expr. hir_id ) {
178
+ self . check_called_function ( def_id, expr. span ) ;
179
+ if self . panic_span . is_some ( ) {
180
+ return ;
181
+ }
182
+ }
183
+ // Visit receiver and arguments
184
+ self . visit_expr ( receiver) ;
185
+ for arg in args {
186
+ self . visit_expr ( arg) ;
187
+ }
188
+ } ,
189
+ _ => {
190
+ if let Some ( macro_call) = root_macro_call_first_node ( self . cx , expr) {
191
+ let macro_name = self . cx . tcx . item_name ( macro_call. def_id ) ;
192
+ // Skip macros like `println!`, `print!`, `eprintln!`, `eprint!`.
193
+ // This is a special case, these macros can panic, but it is very unlikely
194
+ // that this is intended. In the name of reducing false positiveness we are
195
+ // giving out soundness.
196
+ //
197
+ // This decision can be justified as it is highly unlikely that the tool is sound
198
+ // without this additional check, and with this we are reducing the number of false
199
+ // positives.
200
+ if matches ! ( macro_name. as_str( ) , "println" | "print" | "eprintln" | "eprint" | "dbg" ) {
201
+ return ;
202
+ }
203
+ if is_panic ( self . cx , macro_call. def_id )
204
+ || matches ! ( macro_name. as_str( ) , "assert" | "assert_eq" | "assert_ne" )
205
+ {
206
+ self . is_const = self . cx . tcx . hir ( ) . is_inside_const_context ( expr. hir_id ) ;
207
+ self . panic_span = Some ( macro_call. span ) ;
208
+ return ;
209
+ }
210
+ }
136
211
137
- // check for `unwrap` and `expect` for both `Option` and `Result`
138
- if let Some ( arglists) = method_chain_args ( expr, & [ "unwrap" ] ) . or ( method_chain_args ( expr, & [ "expect" ] ) ) {
139
- let receiver_ty = self . typeck_results . expr_ty ( arglists[ 0 ] . 0 ) . peel_refs ( ) ;
140
- if is_type_diagnostic_item ( self . cx , receiver_ty, sym:: Option )
141
- || is_type_diagnostic_item ( self . cx , receiver_ty, sym:: Result )
142
- {
143
- self . panic_span = Some ( expr. span ) ;
144
- }
145
- }
212
+ // Check for `unwrap` and `expect` method calls
213
+ if let Some ( arglists) = method_chain_args ( expr, & [ "unwrap" ] ) . or ( method_chain_args ( expr, & [ "expect" ] ) ) {
214
+ let receiver_ty = self . typeck_results . expr_ty ( arglists[ 0 ] . 0 ) . peel_refs ( ) ;
215
+ if is_type_diagnostic_item ( self . cx , receiver_ty, sym:: Option )
216
+ || is_type_diagnostic_item ( self . cx , receiver_ty, sym:: Result )
217
+ {
218
+ self . panic_span = Some ( expr. span ) ;
219
+ return ;
220
+ }
221
+ }
146
222
147
- // and check sub-expressions
148
- intravisit:: walk_expr ( self , expr) ;
223
+ intravisit:: walk_expr ( self , expr) ;
224
+ } ,
225
+ }
149
226
}
150
227
151
- // Panics in const blocks will cause compilation to fail.
228
+ // Do not visit anonymous constants, as panics in const contexts are compile-time errors
152
229
fn visit_anon_const ( & mut self , _: & ' tcx AnonConst ) { }
153
230
154
231
fn nested_visit_map ( & mut self ) -> Self :: Map {
0 commit comments