11use clippy_utils:: diagnostics:: span_lint_and_sugg;
2- use clippy_utils:: higher;
32use clippy_utils:: source:: snippet_with_applicability;
43use clippy_utils:: ty:: is_type_diagnostic_item;
54use clippy_utils:: {
65 eq_expr_value, get_parent_node, in_constant, is_else_clause, is_res_lang_ctor, path_to_local, path_to_local_id,
76 peel_blocks, peel_blocks_with_stmt,
87} ;
8+ use clippy_utils:: { higher, is_path_lang_item} ;
99use if_chain:: if_chain;
1010use rustc_errors:: Applicability ;
1111use rustc_hir:: def:: Res ;
12- use rustc_hir:: LangItem :: { OptionNone , OptionSome , ResultErr , ResultOk } ;
12+ use rustc_hir:: LangItem :: { self , OptionNone , OptionSome , ResultErr , ResultOk } ;
1313use rustc_hir:: { BindingAnnotation , ByRef , Expr , ExprKind , Node , PatKind , PathSegment , QPath } ;
1414use rustc_lint:: { LateContext , LateLintPass } ;
1515use rustc_middle:: ty:: Ty ;
16- use rustc_session:: { declare_lint_pass, declare_tool_lint} ;
16+ use rustc_session:: declare_tool_lint;
17+ use rustc_session:: impl_lint_pass;
1718use rustc_span:: { sym, symbol:: Symbol } ;
1819
1920declare_clippy_lint ! {
@@ -41,7 +42,16 @@ declare_clippy_lint! {
4142 "checks for expressions that could be replaced by the question mark operator"
4243}
4344
44- declare_lint_pass ! ( QuestionMark => [ QUESTION_MARK ] ) ;
45+ #[ derive( Default ) ]
46+ pub struct QuestionMark {
47+ /// Keeps track of how many try blocks we are in at any point during linting.
48+ /// This allows us to answer the question "are we inside of a try block"
49+ /// very quickly, without having to walk up the parent chain, by simply checking
50+ /// if it is greater than zero.
51+ /// As for why we need this in the first place: <https://github.com/rust-lang/rust-clippy/issues/8628>
52+ try_block_depth_stack : Vec < u32 > ,
53+ }
54+ impl_lint_pass ! ( QuestionMark => [ QUESTION_MARK ] ) ;
4555
4656enum IfBlockType < ' hir > {
4757 /// An `if x.is_xxx() { a } else { b } ` expression.
@@ -68,98 +78,6 @@ enum IfBlockType<'hir> {
6878 ) ,
6979}
7080
71- /// Checks if the given expression on the given context matches the following structure:
72- ///
73- /// ```ignore
74- /// if option.is_none() {
75- /// return None;
76- /// }
77- /// ```
78- ///
79- /// ```ignore
80- /// if result.is_err() {
81- /// return result;
82- /// }
83- /// ```
84- ///
85- /// If it matches, it will suggest to use the question mark operator instead
86- fn check_is_none_or_err_and_early_return < ' tcx > ( cx : & LateContext < ' tcx > , expr : & Expr < ' tcx > ) {
87- if_chain ! {
88- if let Some ( higher:: If { cond, then, r#else } ) = higher:: If :: hir( expr) ;
89- if !is_else_clause( cx. tcx, expr) ;
90- if let ExprKind :: MethodCall ( segment, caller, ..) = & cond. kind;
91- let caller_ty = cx. typeck_results( ) . expr_ty( caller) ;
92- let if_block = IfBlockType :: IfIs ( caller, caller_ty, segment. ident. name, then, r#else) ;
93- if is_early_return( sym:: Option , cx, & if_block) || is_early_return( sym:: Result , cx, & if_block) ;
94- then {
95- let mut applicability = Applicability :: MachineApplicable ;
96- let receiver_str = snippet_with_applicability( cx, caller. span, ".." , & mut applicability) ;
97- let by_ref = !caller_ty. is_copy_modulo_regions( cx. tcx, cx. param_env) &&
98- !matches!( caller. kind, ExprKind :: Call ( ..) | ExprKind :: MethodCall ( ..) ) ;
99- let sugg = if let Some ( else_inner) = r#else {
100- if eq_expr_value( cx, caller, peel_blocks( else_inner) ) {
101- format!( "Some({receiver_str}?)" )
102- } else {
103- return ;
104- }
105- } else {
106- format!( "{receiver_str}{}?;" , if by_ref { ".as_ref()" } else { "" } )
107- } ;
108-
109- span_lint_and_sugg(
110- cx,
111- QUESTION_MARK ,
112- expr. span,
113- "this block may be rewritten with the `?` operator" ,
114- "replace it with" ,
115- sugg,
116- applicability,
117- ) ;
118- }
119- }
120- }
121-
122- fn check_if_let_some_or_err_and_early_return < ' tcx > ( cx : & LateContext < ' tcx > , expr : & Expr < ' tcx > ) {
123- if_chain ! {
124- if let Some ( higher:: IfLet { let_pat, let_expr, if_then, if_else } ) = higher:: IfLet :: hir( cx, expr) ;
125- if !is_else_clause( cx. tcx, expr) ;
126- if let PatKind :: TupleStruct ( ref path1, [ field] , ddpos) = let_pat. kind;
127- if ddpos. as_opt_usize( ) . is_none( ) ;
128- if let PatKind :: Binding ( BindingAnnotation ( by_ref, _) , bind_id, ident, None ) = field. kind;
129- let caller_ty = cx. typeck_results( ) . expr_ty( let_expr) ;
130- let if_block = IfBlockType :: IfLet (
131- cx. qpath_res( path1, let_pat. hir_id) ,
132- caller_ty,
133- ident. name,
134- let_expr,
135- if_then,
136- if_else
137- ) ;
138- if ( is_early_return( sym:: Option , cx, & if_block) && path_to_local_id( peel_blocks( if_then) , bind_id) )
139- || is_early_return( sym:: Result , cx, & if_block) ;
140- if if_else. map( |e| eq_expr_value( cx, let_expr, peel_blocks( e) ) ) . filter( |e| * e) . is_none( ) ;
141- then {
142- let mut applicability = Applicability :: MachineApplicable ;
143- let receiver_str = snippet_with_applicability( cx, let_expr. span, ".." , & mut applicability) ;
144- let requires_semi = matches!( get_parent_node( cx. tcx, expr. hir_id) , Some ( Node :: Stmt ( _) ) ) ;
145- let sugg = format!(
146- "{receiver_str}{}?{}" ,
147- if by_ref == ByRef :: Yes { ".as_ref()" } else { "" } ,
148- if requires_semi { ";" } else { "" }
149- ) ;
150- span_lint_and_sugg(
151- cx,
152- QUESTION_MARK ,
153- expr. span,
154- "this block may be rewritten with the `?` operator" ,
155- "replace it with" ,
156- sugg,
157- applicability,
158- ) ;
159- }
160- }
161- }
162-
16381fn is_early_return ( smbl : Symbol , cx : & LateContext < ' _ > , if_block : & IfBlockType < ' _ > ) -> bool {
16482 match * if_block {
16583 IfBlockType :: IfIs ( caller, caller_ty, call_sym, if_then, _) => {
@@ -230,11 +148,147 @@ fn expr_return_none_or_err(
230148 }
231149}
232150
151+ impl QuestionMark {
152+ fn inside_try_block ( & self ) -> bool {
153+ self . try_block_depth_stack . last ( ) > Some ( & 0 )
154+ }
155+
156+ /// Checks if the given expression on the given context matches the following structure:
157+ ///
158+ /// ```ignore
159+ /// if option.is_none() {
160+ /// return None;
161+ /// }
162+ /// ```
163+ ///
164+ /// ```ignore
165+ /// if result.is_err() {
166+ /// return result;
167+ /// }
168+ /// ```
169+ ///
170+ /// If it matches, it will suggest to use the question mark operator instead
171+ fn check_is_none_or_err_and_early_return < ' tcx > ( & self , cx : & LateContext < ' tcx > , expr : & Expr < ' tcx > ) {
172+ if_chain ! {
173+ if !self . inside_try_block( ) ;
174+ if let Some ( higher:: If { cond, then, r#else } ) = higher:: If :: hir( expr) ;
175+ if !is_else_clause( cx. tcx, expr) ;
176+ if let ExprKind :: MethodCall ( segment, caller, ..) = & cond. kind;
177+ let caller_ty = cx. typeck_results( ) . expr_ty( caller) ;
178+ let if_block = IfBlockType :: IfIs ( caller, caller_ty, segment. ident. name, then, r#else) ;
179+ if is_early_return( sym:: Option , cx, & if_block) || is_early_return( sym:: Result , cx, & if_block) ;
180+ then {
181+ let mut applicability = Applicability :: MachineApplicable ;
182+ let receiver_str = snippet_with_applicability( cx, caller. span, ".." , & mut applicability) ;
183+ let by_ref = !caller_ty. is_copy_modulo_regions( cx. tcx, cx. param_env) &&
184+ !matches!( caller. kind, ExprKind :: Call ( ..) | ExprKind :: MethodCall ( ..) ) ;
185+ let sugg = if let Some ( else_inner) = r#else {
186+ if eq_expr_value( cx, caller, peel_blocks( else_inner) ) {
187+ format!( "Some({receiver_str}?)" )
188+ } else {
189+ return ;
190+ }
191+ } else {
192+ format!( "{receiver_str}{}?;" , if by_ref { ".as_ref()" } else { "" } )
193+ } ;
194+
195+ span_lint_and_sugg(
196+ cx,
197+ QUESTION_MARK ,
198+ expr. span,
199+ "this block may be rewritten with the `?` operator" ,
200+ "replace it with" ,
201+ sugg,
202+ applicability,
203+ ) ;
204+ }
205+ }
206+ }
207+
208+ fn check_if_let_some_or_err_and_early_return < ' tcx > ( & self , cx : & LateContext < ' tcx > , expr : & Expr < ' tcx > ) {
209+ if_chain ! {
210+ if !self . inside_try_block( ) ;
211+ if let Some ( higher:: IfLet { let_pat, let_expr, if_then, if_else } ) = higher:: IfLet :: hir( cx, expr) ;
212+ if !is_else_clause( cx. tcx, expr) ;
213+ if let PatKind :: TupleStruct ( ref path1, [ field] , ddpos) = let_pat. kind;
214+ if ddpos. as_opt_usize( ) . is_none( ) ;
215+ if let PatKind :: Binding ( BindingAnnotation ( by_ref, _) , bind_id, ident, None ) = field. kind;
216+ let caller_ty = cx. typeck_results( ) . expr_ty( let_expr) ;
217+ let if_block = IfBlockType :: IfLet (
218+ cx. qpath_res( path1, let_pat. hir_id) ,
219+ caller_ty,
220+ ident. name,
221+ let_expr,
222+ if_then,
223+ if_else
224+ ) ;
225+ if ( is_early_return( sym:: Option , cx, & if_block) && path_to_local_id( peel_blocks( if_then) , bind_id) )
226+ || is_early_return( sym:: Result , cx, & if_block) ;
227+ if if_else. map( |e| eq_expr_value( cx, let_expr, peel_blocks( e) ) ) . filter( |e| * e) . is_none( ) ;
228+ then {
229+ let mut applicability = Applicability :: MachineApplicable ;
230+ let receiver_str = snippet_with_applicability( cx, let_expr. span, ".." , & mut applicability) ;
231+ let requires_semi = matches!( get_parent_node( cx. tcx, expr. hir_id) , Some ( Node :: Stmt ( _) ) ) ;
232+ let sugg = format!(
233+ "{receiver_str}{}?{}" ,
234+ if by_ref == ByRef :: Yes { ".as_ref()" } else { "" } ,
235+ if requires_semi { ";" } else { "" }
236+ ) ;
237+ span_lint_and_sugg(
238+ cx,
239+ QUESTION_MARK ,
240+ expr. span,
241+ "this block may be rewritten with the `?` operator" ,
242+ "replace it with" ,
243+ sugg,
244+ applicability,
245+ ) ;
246+ }
247+ }
248+ }
249+ }
250+
251+ fn is_try_block ( cx : & LateContext < ' _ > , bl : & rustc_hir:: Block < ' _ > ) -> bool {
252+ if let Some ( expr) = bl. expr
253+ && let rustc_hir:: ExprKind :: Call ( callee, _) = expr. kind
254+ {
255+ is_path_lang_item ( cx, callee, LangItem :: TryTraitFromOutput )
256+ } else {
257+ false
258+ }
259+ }
260+
233261impl < ' tcx > LateLintPass < ' tcx > for QuestionMark {
234262 fn check_expr ( & mut self , cx : & LateContext < ' tcx > , expr : & ' tcx Expr < ' _ > ) {
235263 if !in_constant ( cx, expr. hir_id ) {
236- check_is_none_or_err_and_early_return ( cx, expr) ;
237- check_if_let_some_or_err_and_early_return ( cx, expr) ;
264+ self . check_is_none_or_err_and_early_return ( cx, expr) ;
265+ self . check_if_let_some_or_err_and_early_return ( cx, expr) ;
266+ }
267+ }
268+
269+ fn check_block ( & mut self , cx : & LateContext < ' tcx > , block : & ' tcx rustc_hir:: Block < ' tcx > ) {
270+ if is_try_block ( cx, block) {
271+ * self
272+ . try_block_depth_stack
273+ . last_mut ( )
274+ . expect ( "blocks are always part of bodies and must have a depth" ) += 1 ;
275+ }
276+ }
277+
278+ fn check_body ( & mut self , _: & LateContext < ' tcx > , _: & ' tcx rustc_hir:: Body < ' tcx > ) {
279+ self . try_block_depth_stack . push ( 0 ) ;
280+ }
281+
282+ fn check_body_post ( & mut self , _: & LateContext < ' tcx > , _: & ' tcx rustc_hir:: Body < ' tcx > ) {
283+ self . try_block_depth_stack . pop ( ) ;
284+ }
285+
286+ fn check_block_post ( & mut self , cx : & LateContext < ' tcx > , block : & ' tcx rustc_hir:: Block < ' tcx > ) {
287+ if is_try_block ( cx, block) {
288+ * self
289+ . try_block_depth_stack
290+ . last_mut ( )
291+ . expect ( "blocks are always part of bodies and must have a depth" ) -= 1 ;
238292 }
239293 }
240294}
0 commit comments