Skip to content

Commit 1e64193

Browse files
committed
expend [question_mark] lint to suggest for early early returning if-let expressions
1 parent 4198013 commit 1e64193

File tree

4 files changed

+300
-165
lines changed

4 files changed

+300
-165
lines changed

clippy_lints/src/question_mark.rs

Lines changed: 166 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
use clippy_utils::diagnostics::span_lint_and_sugg;
22
use clippy_utils::higher;
33
use clippy_utils::source::snippet_with_applicability;
4-
use clippy_utils::sugg::Sugg;
54
use clippy_utils::ty::is_type_diagnostic_item;
6-
use clippy_utils::{eq_expr_value, is_lang_ctor, path_to_local, path_to_local_id, peel_blocks, peel_blocks_with_stmt};
5+
use clippy_utils::{
6+
eq_expr_value, get_parent_node, is_else_clause, is_lang_ctor, path_to_local, path_to_local_id, peel_blocks,
7+
peel_blocks_with_stmt,
8+
};
79
use if_chain::if_chain;
810
use rustc_errors::Applicability;
9-
use rustc_hir::LangItem::{OptionNone, OptionSome, ResultOk};
10-
use rustc_hir::{BindingAnnotation, Expr, ExprKind, PatKind};
11+
use rustc_hir::LangItem::{OptionNone, OptionSome, ResultErr, ResultOk};
12+
use rustc_hir::{BindingAnnotation, Expr, ExprKind, Node, PatKind, PathSegment, QPath};
1113
use rustc_lint::{LateContext, LateLintPass};
14+
use rustc_middle::ty::Ty;
1215
use rustc_session::{declare_lint_pass, declare_tool_lint};
13-
use rustc_span::sym;
16+
use rustc_span::{sym, symbol::Symbol};
1417

1518
declare_clippy_lint! {
1619
/// ### What it does
@@ -39,135 +42,185 @@ declare_clippy_lint! {
3942

4043
declare_lint_pass!(QuestionMark => [QUESTION_MARK]);
4144

42-
impl QuestionMark {
43-
/// Checks if the given expression on the given context matches the following structure:
45+
enum IfBlockType<'hir> {
46+
/// An `if x.is_xxx() { a } else { b } ` expression.
4447
///
45-
/// ```ignore
46-
/// if option.is_none() {
47-
/// return None;
48-
/// }
49-
/// ```
48+
/// Contains: caller (x), caller_type, call_sym (is_xxx), if_then (a), if_else (b)
49+
IfIs(
50+
&'hir Expr<'hir>,
51+
Ty<'hir>,
52+
Symbol,
53+
&'hir Expr<'hir>,
54+
Option<&'hir Expr<'hir>>,
55+
),
56+
/// An `if let Xxx(a) = b { c } else { d }` expression.
5057
///
51-
/// ```ignore
52-
/// if result.is_err() {
53-
/// return result;
54-
/// }
55-
/// ```
56-
///
57-
/// If it matches, it will suggest to use the question mark operator instead
58-
fn check_is_none_or_err_and_early_return(cx: &LateContext<'_>, expr: &Expr<'_>) {
59-
if_chain! {
60-
if let Some(higher::If { cond, then, r#else }) = higher::If::hir(expr);
61-
if let ExprKind::MethodCall(segment, args, _) = &cond.kind;
62-
if let Some(subject) = args.get(0);
63-
if (Self::option_check_and_early_return(cx, subject, then) && segment.ident.name == sym!(is_none)) ||
64-
(Self::result_check_and_early_return(cx, subject, then) && segment.ident.name == sym!(is_err));
65-
then {
66-
let mut applicability = Applicability::MachineApplicable;
67-
let receiver_str = &Sugg::hir_with_applicability(cx, subject, "..", &mut applicability);
68-
let mut replacement: Option<String> = None;
69-
if let Some(else_inner) = r#else {
70-
if eq_expr_value(cx, subject, peel_blocks(else_inner)) {
71-
replacement = Some(format!("Some({}?)", receiver_str));
72-
}
73-
} else if Self::moves_by_default(cx, subject)
74-
&& !matches!(subject.kind, ExprKind::Call(..) | ExprKind::MethodCall(..))
75-
{
76-
replacement = Some(format!("{}.as_ref()?;", receiver_str));
77-
} else {
78-
replacement = Some(format!("{}?;", receiver_str));
79-
}
58+
/// Contains: let_pat_qpath (Xxx), let_pat_type, let_pat_sym (a), let_expr (b), if_then (c),
59+
/// if_else (d)
60+
IfLet(
61+
&'hir QPath<'hir>,
62+
Ty<'hir>,
63+
Symbol,
64+
&'hir Expr<'hir>,
65+
&'hir Expr<'hir>,
66+
Option<&'hir Expr<'hir>>,
67+
),
68+
}
8069

81-
if let Some(replacement_str) = replacement {
82-
span_lint_and_sugg(
83-
cx,
84-
QUESTION_MARK,
85-
expr.span,
86-
"this block may be rewritten with the `?` operator",
87-
"replace it with",
88-
replacement_str,
89-
applicability,
90-
);
70+
/// Checks if the given expression on the given context matches the following structure:
71+
///
72+
/// ```ignore
73+
/// if option.is_none() {
74+
/// return None;
75+
/// }
76+
/// ```
77+
///
78+
/// ```ignore
79+
/// if result.is_err() {
80+
/// return result;
81+
/// }
82+
/// ```
83+
///
84+
/// If it matches, it will suggest to use the question mark operator instead
85+
fn check_is_none_or_err_and_early_return(cx: &LateContext<'_>, expr: &Expr<'_>) {
86+
if_chain! {
87+
if let Some(higher::If { cond, then, r#else }) = higher::If::hir(expr);
88+
if !is_else_clause(cx.tcx, expr);
89+
if let ExprKind::MethodCall(segment, args, _) = &cond.kind;
90+
if let Some(caller) = args.get(0);
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.at(caller.span), cx.param_env) &&
98+
!matches!(caller.kind, ExprKind::Call(..) | ExprKind::MethodCall(..));
99+
let mut suggestion = String::new();
100+
if let Some(else_inner) = r#else {
101+
if eq_expr_value(cx, caller, peel_blocks(else_inner)) {
102+
suggestion = format!("Some({}?)", receiver_str);
91103
}
104+
} else {
105+
suggestion = format!("{}{}?;", receiver_str, if by_ref { ".as_ref()" } else { "" });
92106
}
107+
offer_suggestion(cx, expr, suggestion, applicability);
93108
}
94109
}
110+
}
95111

96-
fn check_if_let_some_or_err_and_early_return(cx: &LateContext<'_>, expr: &Expr<'_>) {
97-
if_chain! {
98-
if let Some(higher::IfLet { let_pat, let_expr, if_then, if_else: Some(if_else) })
99-
= higher::IfLet::hir(cx, expr);
100-
if let PatKind::TupleStruct(ref path1, fields, None) = let_pat.kind;
101-
if (Self::option_check_and_early_return(cx, let_expr, if_else) && is_lang_ctor(cx, path1, OptionSome)) ||
102-
(Self::result_check_and_early_return(cx, let_expr, if_else) && is_lang_ctor(cx, path1, ResultOk));
103-
104-
if let PatKind::Binding(annot, bind_id, _, _) = fields[0].kind;
112+
fn check_if_let_some_or_err_and_early_return(cx: &LateContext<'_>, expr: &Expr<'_>) {
113+
if_chain! {
114+
if let Some(higher::IfLet { let_pat, let_expr, if_then, if_else }) = higher::IfLet::hir(cx, expr);
115+
if !is_else_clause(cx.tcx, expr);
116+
if let PatKind::TupleStruct(ref path1, fields, None) = let_pat.kind;
117+
if let PatKind::Binding(annot, bind_id, ident, _) = fields[0].kind;
118+
let caller_ty = cx.typeck_results().expr_ty(let_expr);
119+
let if_block = IfBlockType::IfLet(path1, caller_ty, ident.name, let_expr, if_then, if_else);
120+
if (is_early_return(sym::Option, cx, &if_block) && path_to_local_id(peel_blocks(if_then), bind_id))
121+
|| is_early_return(sym::Result, cx, &if_block);
122+
then {
123+
let mut applicability = Applicability::MachineApplicable;
124+
let receiver_str = snippet_with_applicability(cx, let_expr.span, "..", &mut applicability);
105125
let by_ref = matches!(annot, BindingAnnotation::Ref | BindingAnnotation::RefMut);
106-
if path_to_local_id(peel_blocks(if_then), bind_id);
107-
then {
108-
let mut applicability = Applicability::MachineApplicable;
109-
let receiver_str = snippet_with_applicability(cx, let_expr.span, "..", &mut applicability);
110-
let replacement = format!("{}{}?", receiver_str, if by_ref { ".as_ref()" } else { "" },);
111-
112-
span_lint_and_sugg(
113-
cx,
114-
QUESTION_MARK,
115-
expr.span,
116-
"this if-let-else may be rewritten with the `?` operator",
117-
"replace it with",
118-
replacement,
119-
applicability,
120-
);
121-
}
126+
let requires_semi = matches!(get_parent_node(cx.tcx, expr.hir_id), Some(Node::Stmt(_)));
127+
let replacement = format!(
128+
"{}{}?{}",
129+
receiver_str,
130+
if by_ref { ".as_ref()" } else { "" },
131+
if requires_semi { ";" } else { "" }
132+
);
133+
offer_suggestion(cx, expr, replacement, applicability);
122134
}
123135
}
136+
}
124137

125-
fn result_check_and_early_return(cx: &LateContext<'_>, expr: &Expr<'_>, nested_expr: &Expr<'_>) -> bool {
126-
Self::is_result(cx, expr) && Self::expression_returns_unmodified_err(nested_expr, expr)
127-
}
128-
129-
fn option_check_and_early_return(cx: &LateContext<'_>, expr: &Expr<'_>, nested_expr: &Expr<'_>) -> bool {
130-
Self::is_option(cx, expr) && Self::expression_returns_none(cx, nested_expr)
131-
}
132-
133-
fn moves_by_default(cx: &LateContext<'_>, expression: &Expr<'_>) -> bool {
134-
let expr_ty = cx.typeck_results().expr_ty(expression);
135-
136-
!expr_ty.is_copy_modulo_regions(cx.tcx.at(expression.span), cx.param_env)
137-
}
138-
139-
fn is_option(cx: &LateContext<'_>, expression: &Expr<'_>) -> bool {
140-
let expr_ty = cx.typeck_results().expr_ty(expression);
141-
142-
is_type_diagnostic_item(cx, expr_ty, sym::Option)
143-
}
144-
145-
fn is_result(cx: &LateContext<'_>, expression: &Expr<'_>) -> bool {
146-
let expr_ty = cx.typeck_results().expr_ty(expression);
147-
148-
is_type_diagnostic_item(cx, expr_ty, sym::Result)
138+
fn is_early_return(smbl: Symbol, cx: &LateContext<'_>, if_block: &IfBlockType<'_>) -> bool {
139+
match *if_block {
140+
IfBlockType::IfIs(caller, caller_ty, call_sym, if_then, _) => {
141+
// If the block could be identified as `if x.is_none()/is_err()`,
142+
// we then only need to check the if_then return to see if it is none/err.
143+
is_type_diagnostic_item(cx, caller_ty, smbl)
144+
&& expr_return_none_or_err(smbl, cx, if_then, caller, None)
145+
&& match smbl {
146+
sym::Option => call_sym == sym!(is_none),
147+
sym::Result => call_sym == sym!(is_err),
148+
_ => false,
149+
}
150+
},
151+
IfBlockType::IfLet(qpath, let_expr_ty, let_pat_sym, let_expr, if_then, if_else) => {
152+
is_type_diagnostic_item(cx, let_expr_ty, smbl)
153+
&& match smbl {
154+
sym::Option => {
155+
// We only need to check `if let Some(x) = option` not `if let None = option`,
156+
// because the later one will be suggested as `if option.is_none()` thus causing conflict.
157+
is_lang_ctor(cx, qpath, OptionSome)
158+
&& if_else.is_some()
159+
&& expr_return_none_or_err(smbl, cx, if_else.unwrap(), let_expr, None)
160+
},
161+
sym::Result => {
162+
(is_lang_ctor(cx, qpath, ResultOk)
163+
&& if_else.is_some()
164+
&& expr_return_none_or_err(smbl, cx, if_else.unwrap(), let_expr, Some(let_pat_sym)))
165+
|| is_lang_ctor(cx, qpath, ResultErr)
166+
&& expr_return_none_or_err(smbl, cx, if_then, let_expr, Some(let_pat_sym))
167+
},
168+
_ => false,
169+
}
170+
},
149171
}
172+
}
150173

151-
fn expression_returns_none(cx: &LateContext<'_>, expression: &Expr<'_>) -> bool {
152-
match peel_blocks_with_stmt(expression).kind {
153-
ExprKind::Ret(Some(expr)) => Self::expression_returns_none(cx, expr),
154-
ExprKind::Path(ref qpath) => is_lang_ctor(cx, qpath, OptionNone),
174+
fn expr_return_none_or_err(
175+
smbl: Symbol,
176+
cx: &LateContext<'_>,
177+
expr: &Expr<'_>,
178+
cond_expr: &Expr<'_>,
179+
err_sym: Option<Symbol>,
180+
) -> bool {
181+
match peel_blocks_with_stmt(expr).kind {
182+
ExprKind::Ret(Some(ret_expr)) => expr_return_none_or_err(smbl, cx, ret_expr, cond_expr, err_sym),
183+
ExprKind::Path(ref qpath) => match smbl {
184+
sym::Option => is_lang_ctor(cx, qpath, OptionNone),
185+
sym::Result => path_to_local(expr).is_some() && path_to_local(expr) == path_to_local(cond_expr),
155186
_ => false,
156-
}
187+
},
188+
ExprKind::Call(call_expr, args_expr) => {
189+
if_chain! {
190+
if smbl == sym::Result;
191+
if let ExprKind::Path(QPath::Resolved(_, path)) = &call_expr.kind;
192+
if let Some(segment) = path.segments.first();
193+
if let Some(err_sym) = err_sym;
194+
if let Some(arg) = args_expr.first();
195+
if let ExprKind::Path(QPath::Resolved(_, arg_path)) = &arg.kind;
196+
if let Some(PathSegment { ident, .. }) = arg_path.segments.first();
197+
then {
198+
return segment.ident.name == sym::Err && err_sym == ident.name;
199+
}
200+
}
201+
false
202+
},
203+
_ => false,
157204
}
205+
}
158206

159-
fn expression_returns_unmodified_err(expr: &Expr<'_>, cond_expr: &Expr<'_>) -> bool {
160-
match peel_blocks_with_stmt(expr).kind {
161-
ExprKind::Ret(Some(ret_expr)) => Self::expression_returns_unmodified_err(ret_expr, cond_expr),
162-
ExprKind::Path(_) => path_to_local(expr).is_some() && path_to_local(expr) == path_to_local(cond_expr),
163-
_ => false,
164-
}
207+
fn offer_suggestion(cx: &LateContext<'_>, expr: &Expr<'_>, suggestion: String, applicability: Applicability) {
208+
if !suggestion.is_empty() {
209+
span_lint_and_sugg(
210+
cx,
211+
QUESTION_MARK,
212+
expr.span,
213+
"this block may be rewritten with the `?` operator",
214+
"replace it with",
215+
suggestion,
216+
applicability,
217+
);
165218
}
166219
}
167220

168221
impl<'tcx> LateLintPass<'tcx> for QuestionMark {
169222
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
170-
Self::check_is_none_or_err_and_early_return(cx, expr);
171-
Self::check_if_let_some_or_err_and_early_return(cx, expr);
223+
check_is_none_or_err_and_early_return(cx, expr);
224+
check_if_let_some_or_err_and_early_return(cx, expr);
172225
}
173226
}

tests/ui/question_mark.fixed

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// run-rustfix
22
#![allow(unreachable_code)]
3+
#![allow(dead_code)]
34
#![allow(clippy::unnecessary_wraps)]
45

56
fn some_func(a: Option<u32>) -> Option<u32> {
@@ -154,26 +155,56 @@ fn f() -> NotOption {
154155
NotOption::First
155156
}
156157

157-
fn main() {
158-
some_func(Some(42));
159-
some_func(None);
160-
some_other_func(Some(42));
158+
fn do_something() {}
161159

162-
let copy_struct = CopyStruct { opt: Some(54) };
163-
copy_struct.func();
160+
fn err_immediate_return() -> Result<i32, i32> {
161+
func_returning_result()?;
162+
Ok(1)
163+
}
164164

165-
let move_struct = MoveStruct {
166-
opt: Some(vec![42, 1337]),
167-
};
168-
move_struct.ref_func();
169-
move_struct.clone().mov_func_reuse();
170-
move_struct.mov_func_no_use();
165+
fn err_immediate_return_and_do_something() -> Result<i32, i32> {
166+
func_returning_result()?;
167+
do_something();
168+
Ok(1)
169+
}
171170

172-
let so = SeemsOption::Some(45);
173-
returns_something_similar_to_option(so);
171+
// No warning
172+
fn no_immediate_return() -> Result<i32, i32> {
173+
if let Err(err) = func_returning_result() {
174+
do_something();
175+
return Err(err);
176+
}
177+
Ok(1)
178+
}
174179

175-
func();
180+
// No warning
181+
fn mixed_result_and_option() -> Option<i32> {
182+
if let Err(err) = func_returning_result() {
183+
return Some(err);
184+
}
185+
None
186+
}
187+
188+
// No warning
189+
fn else_if_check() -> Result<i32, i32> {
190+
if true {
191+
Ok(1)
192+
} else if let Err(e) = func_returning_result() {
193+
Err(e)
194+
} else {
195+
Err(-1)
196+
}
197+
}
176198

177-
let _ = result_func(Ok(42));
178-
let _ = f();
199+
// No warning
200+
#[allow(clippy::manual_map)]
201+
#[rustfmt::skip]
202+
fn option_map() -> Option<bool> {
203+
if let Some(a) = Some(false) {
204+
Some(!a)
205+
} else {
206+
None
207+
}
179208
}
209+
210+
fn main() {}

0 commit comments

Comments
 (0)