@@ -3,8 +3,11 @@ use clippy_utils::macros::{is_panic, root_macro_call_first_node};
33use clippy_utils:: ty:: is_type_diagnostic_item;
44use clippy_utils:: visitors:: Visitable ;
55use 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 } ;
811use rustc_lint:: { LateContext , LateLintPass } ;
912use rustc_middle:: hir:: nested_filter;
1013use rustc_middle:: ty;
@@ -13,142 +16,216 @@ use rustc_span::{Span, sym};
1316
1417declare_clippy_lint ! {
1518 /// ### 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.
1720 ///
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 .
2124 ///
2225 /// ### Example
23- /// ```no_run
26+ /// ```rust
2427 /// #[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+ /// }
3234 /// }
33- ///
3435 /// ```
36+ ///
3537 /// 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);
4344 /// }
4445 /// ```
45- #[ clippy:: version = "1.70 .0" ]
46+ #[ clippy:: version = "1.82 .0" ]
4647 pub TEST_WITHOUT_FAIL_CASE ,
4748 restriction,
48- "A test function is outside the testing module. "
49+ "test function cannot fail because it does not panic or assert "
4950}
5051
5152declare_lint_pass ! ( TestWithoutFailCase => [ TEST_WITHOUT_FAIL_CASE ] ) ;
5253
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-
6854impl < ' tcx > LateLintPass < ' tcx > for TestWithoutFailCase {
6955 fn check_item ( & mut self , cx : & LateContext < ' tcx > , item : & ' tcx Item < ' tcx > ) {
56+ // Only interested in functions that are test functions
7057 if let ItemKind :: Fn ( _, _, body_id) = item. kind
7158 && is_in_test_function ( cx. tcx , item. hir_id ( ) )
7259 {
7360 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) ;
7663 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
7965 span_lint_and_then (
8066 cx,
8167 TEST_WITHOUT_FAIL_CASE ,
8268 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 " ,
8470 |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 " ) ;
8672 } ,
8773 ) ;
8874 }
8975 }
9076 }
9177}
9278
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
9483 cx : & ' a LateContext < ' tcx > ,
84+ /// Whether we are inside a constant context
9585 is_const : bool ,
86+ /// The span where a panic was found
9687 panic_span : Option < Span > ,
88+ /// Type checking results for the current body
9789 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 > ,
9892}
9993
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
101107 pub fn find_span (
102108 cx : & ' a LateContext < ' tcx > ,
103109 typeck_results : & ' tcx ty:: TypeckResults < ' tcx > ,
104110 body : impl Visitable < ' tcx > ,
105111 ) -> 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+ }
114146 }
115147}
116148
117- impl < ' a , ' tcx > Visitor < ' tcx > for FindPanicUnwrap < ' a , ' tcx > {
149+ impl < ' a , ' tcx > Visitor < ' tcx > for SearchPanicIntraFunction < ' a , ' tcx > {
118150 type NestedFilter = nested_filter:: OnlyBodies ;
119151
120152 fn visit_expr ( & mut self , expr : & ' tcx Expr < ' _ > ) {
121153 if self . panic_span . is_some ( ) {
154+ // If we've already found a panic, no need to continue
122155 return ;
123156 }
124157
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+ }
136211
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+ }
146222
147- // and check sub-expressions
148- intravisit:: walk_expr ( self , expr) ;
223+ intravisit:: walk_expr ( self , expr) ;
224+ } ,
225+ }
149226 }
150227
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
152229 fn visit_anon_const ( & mut self , _: & ' tcx AnonConst ) { }
153230
154231 fn nested_visit_map ( & mut self ) -> Self :: Map {
0 commit comments