]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/question_mark.rs
bb86fb3b7d42f43858db4b853f7fe3cbcb6ca8f6
[rust.git] / clippy_lints / src / question_mark.rs
1 use clippy_utils::diagnostics::span_lint_and_sugg;
2 use clippy_utils::higher;
3 use clippy_utils::source::snippet_with_applicability;
4 use clippy_utils::ty::is_type_diagnostic_item;
5 use clippy_utils::{
6     eq_expr_value, get_parent_node, in_constant, is_else_clause, is_res_lang_ctor, path_to_local, path_to_local_id,
7     peel_blocks, peel_blocks_with_stmt,
8 };
9 use if_chain::if_chain;
10 use rustc_errors::Applicability;
11 use rustc_hir::def::Res;
12 use rustc_hir::LangItem::{OptionNone, OptionSome, ResultErr, ResultOk};
13 use rustc_hir::{BindingAnnotation, ByRef, Expr, ExprKind, Node, PatKind, PathSegment, QPath};
14 use rustc_lint::{LateContext, LateLintPass};
15 use rustc_middle::ty::Ty;
16 use rustc_session::{declare_lint_pass, declare_tool_lint};
17 use rustc_span::{sym, symbol::Symbol};
18
19 declare_clippy_lint! {
20     /// ### What it does
21     /// Checks for expressions that could be replaced by the question mark operator.
22     ///
23     /// ### Why is this bad?
24     /// Question mark usage is more idiomatic.
25     ///
26     /// ### Example
27     /// ```ignore
28     /// if option.is_none() {
29     ///     return None;
30     /// }
31     /// ```
32     ///
33     /// Could be written:
34     ///
35     /// ```ignore
36     /// option?;
37     /// ```
38     #[clippy::version = "pre 1.29.0"]
39     pub QUESTION_MARK,
40     style,
41     "checks for expressions that could be replaced by the question mark operator"
42 }
43
44 declare_lint_pass!(QuestionMark => [QUESTION_MARK]);
45
46 enum IfBlockType<'hir> {
47     /// An `if x.is_xxx() { a } else { b } ` expression.
48     ///
49     /// Contains: caller (x), caller_type, call_sym (is_xxx), if_then (a), if_else (b)
50     IfIs(
51         &'hir Expr<'hir>,
52         Ty<'hir>,
53         Symbol,
54         &'hir Expr<'hir>,
55         Option<&'hir Expr<'hir>>,
56     ),
57     /// An `if let Xxx(a) = b { c } else { d }` expression.
58     ///
59     /// Contains: let_pat_qpath (Xxx), let_pat_type, let_pat_sym (a), let_expr (b), if_then (c),
60     /// if_else (d)
61     IfLet(
62         Res,
63         Ty<'hir>,
64         Symbol,
65         &'hir Expr<'hir>,
66         &'hir Expr<'hir>,
67         Option<&'hir Expr<'hir>>,
68     ),
69 }
70
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
163 fn is_early_return(smbl: Symbol, cx: &LateContext<'_>, if_block: &IfBlockType<'_>) -> bool {
164     match *if_block {
165         IfBlockType::IfIs(caller, caller_ty, call_sym, if_then, _) => {
166             // If the block could be identified as `if x.is_none()/is_err()`,
167             // we then only need to check the if_then return to see if it is none/err.
168             is_type_diagnostic_item(cx, caller_ty, smbl)
169                 && expr_return_none_or_err(smbl, cx, if_then, caller, None)
170                 && match smbl {
171                     sym::Option => call_sym == sym!(is_none),
172                     sym::Result => call_sym == sym!(is_err),
173                     _ => false,
174                 }
175         },
176         IfBlockType::IfLet(res, let_expr_ty, let_pat_sym, let_expr, if_then, if_else) => {
177             is_type_diagnostic_item(cx, let_expr_ty, smbl)
178                 && match smbl {
179                     sym::Option => {
180                         // We only need to check `if let Some(x) = option` not `if let None = option`,
181                         // because the later one will be suggested as `if option.is_none()` thus causing conflict.
182                         is_res_lang_ctor(cx, res, OptionSome)
183                             && if_else.is_some()
184                             && expr_return_none_or_err(smbl, cx, if_else.unwrap(), let_expr, None)
185                     },
186                     sym::Result => {
187                         (is_res_lang_ctor(cx, res, ResultOk)
188                             && if_else.is_some()
189                             && expr_return_none_or_err(smbl, cx, if_else.unwrap(), let_expr, Some(let_pat_sym)))
190                             || is_res_lang_ctor(cx, res, ResultErr)
191                                 && expr_return_none_or_err(smbl, cx, if_then, let_expr, Some(let_pat_sym))
192                     },
193                     _ => false,
194                 }
195         },
196     }
197 }
198
199 fn expr_return_none_or_err(
200     smbl: Symbol,
201     cx: &LateContext<'_>,
202     expr: &Expr<'_>,
203     cond_expr: &Expr<'_>,
204     err_sym: Option<Symbol>,
205 ) -> bool {
206     match peel_blocks_with_stmt(expr).kind {
207         ExprKind::Ret(Some(ret_expr)) => expr_return_none_or_err(smbl, cx, ret_expr, cond_expr, err_sym),
208         ExprKind::Path(ref qpath) => match smbl {
209             sym::Option => is_res_lang_ctor(cx, cx.qpath_res(qpath, expr.hir_id), OptionNone),
210             sym::Result => path_to_local(expr).is_some() && path_to_local(expr) == path_to_local(cond_expr),
211             _ => false,
212         },
213         ExprKind::Call(call_expr, args_expr) => {
214             if_chain! {
215                 if smbl == sym::Result;
216                 if let ExprKind::Path(QPath::Resolved(_, path)) = &call_expr.kind;
217                 if let Some(segment) = path.segments.first();
218                 if let Some(err_sym) = err_sym;
219                 if let Some(arg) = args_expr.first();
220                 if let ExprKind::Path(QPath::Resolved(_, arg_path)) = &arg.kind;
221                 if let Some(PathSegment { ident, .. }) = arg_path.segments.first();
222                 then {
223                     return segment.ident.name == sym::Err && err_sym == ident.name;
224                 }
225             }
226             false
227         },
228         _ => false,
229     }
230 }
231
232 impl<'tcx> LateLintPass<'tcx> for QuestionMark {
233     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
234         if !in_constant(cx, expr.hir_id) {
235             check_is_none_or_err_and_early_return(cx, expr);
236             check_if_let_some_or_err_and_early_return(cx, expr);
237         }
238     }
239 }