Skip to content

Commit 74527c8

Browse files
committed
extends functionality of question mark lint
1 parent fb94992 commit 74527c8

File tree

5 files changed

+257
-21
lines changed

5 files changed

+257
-21
lines changed

clippy_lints/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#![feature(control_flow_enum)]
66
#![feature(drain_filter)]
77
#![feature(iter_intersperse)]
8+
#![feature(let_chains)]
89
#![feature(let_else)]
910
#![feature(once_cell)]
1011
#![feature(rustc_private)]

clippy_lints/src/question_mark.rs

Lines changed: 119 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ use clippy_utils::ty::is_type_diagnostic_item;
66
use clippy_utils::{eq_expr_value, is_lang_ctor, path_to_local, path_to_local_id, peel_blocks, peel_blocks_with_stmt};
77
use if_chain::if_chain;
88
use rustc_errors::Applicability;
9-
use rustc_hir::LangItem::{OptionNone, OptionSome, ResultOk};
10-
use rustc_hir::{BindingAnnotation, Expr, ExprKind, PatKind};
9+
use rustc_hir::LangItem::{OptionNone, OptionSome, ResultErr, ResultOk};
10+
use rustc_hir::{BindingAnnotation, Expr, ExprKind, PatKind, PathSegment, QPath};
1111
use rustc_lint::{LateContext, LateLintPass};
1212
use rustc_session::{declare_lint_pass, declare_tool_lint};
13-
use rustc_span::sym;
13+
use rustc_span::{sym, symbol::Symbol};
1414

1515
declare_clippy_lint! {
1616
/// ### What it does
@@ -61,21 +61,21 @@ impl QuestionMark {
6161
if let ExprKind::MethodCall(segment, args, _) = &cond.kind;
6262
if let Some(subject) = args.get(0);
6363
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));
64+
(Self::result_check_and_early_return(cx, subject, then, None) && segment.ident.name == sym!(is_err));
6565
then {
6666
let mut applicability = Applicability::MachineApplicable;
67-
let receiver_str = &Sugg::hir_with_applicability(cx, subject, "..", &mut applicability);
67+
let suggestion = &Sugg::hir_with_applicability(cx, subject, "..", &mut applicability);
6868
let mut replacement: Option<String> = None;
6969
if let Some(else_inner) = r#else {
7070
if eq_expr_value(cx, subject, peel_blocks(else_inner)) {
71-
replacement = Some(format!("Some({}?)", receiver_str));
71+
replacement = Some(format!("Some({}?)", suggestion));
7272
}
7373
} else if Self::moves_by_default(cx, subject)
7474
&& !matches!(subject.kind, ExprKind::Call(..) | ExprKind::MethodCall(..))
7575
{
76-
replacement = Some(format!("{}.as_ref()?;", receiver_str));
76+
replacement = Some(format!("{}.as_ref()?;", suggestion));
7777
} else {
78-
replacement = Some(format!("{}?;", receiver_str));
78+
replacement = Some(format!("{}?;", suggestion));
7979
}
8080

8181
if let Some(replacement_str) = replacement {
@@ -93,21 +93,52 @@ impl QuestionMark {
9393
}
9494
}
9595

96+
/// Checks if the given expression on the given context matches the following structure:
97+
///
98+
/// ```ignore
99+
/// if let Some(x) = option {
100+
/// return x;
101+
/// }
102+
/// ```
103+
///
104+
/// ```ignore
105+
/// if let Err(err) = result {
106+
/// return result;
107+
/// }
108+
/// ```
109+
///
110+
/// ```ignore
111+
/// if let Ok(x) = result {
112+
/// return x;
113+
/// }
114+
/// ```
115+
///
116+
/// If it matches, it will suggest to use the question mark operator instead
96117
fn check_if_let_some_or_err_and_early_return(cx: &LateContext<'_>, expr: &Expr<'_>) {
97118
if_chain! {
98-
if let Some(higher::IfLet { let_pat, let_expr, if_then, if_else: Some(if_else) })
119+
if let Some(higher::IfLet { let_pat, let_expr, if_then, if_else })
99120
= higher::IfLet::hir(cx, expr);
121+
// Make sure this expression is not part of `.. else if ..`,
122+
// otherwise it will cause trouble when replacing blocks with question mark.
123+
if !Self::expr_is_else_if(cx, expr);
100124
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;
125+
let nested_expr = if_else.unwrap_or(if_then);
126+
if let PatKind::Binding(annot, bind_id, ident, _) = fields[0].kind;
127+
if (Self::result_check_and_early_return(cx, let_expr, nested_expr, Some(ident.name)) &&
128+
(is_lang_ctor(cx, path1, ResultOk) || is_lang_ctor(cx, path1, ResultErr))) ||
129+
(Self::option_check_and_early_return(cx, let_expr, nested_expr) &&
130+
is_lang_ctor(cx, path1, OptionSome)) && path_to_local_id(peel_blocks(if_then), bind_id);
105131
let by_ref = matches!(annot, BindingAnnotation::Ref | BindingAnnotation::RefMut);
106-
if path_to_local_id(peel_blocks(if_then), bind_id);
107132
then {
108133
let mut applicability = Applicability::MachineApplicable;
109134
let receiver_str = snippet_with_applicability(cx, let_expr.span, "..", &mut applicability);
110-
let replacement = format!("{}{}?", receiver_str, if by_ref { ".as_ref()" } else { "" },);
135+
let append_semi = Self::is_semicolon_needed(if_then, if_else);
136+
let replacement = format!(
137+
"{}{}?{}",
138+
receiver_str,
139+
if by_ref { ".as_ref()" } else { "" },
140+
if append_semi { ";" } else { "" }
141+
);
111142

112143
span_lint_and_sugg(
113144
cx,
@@ -122,8 +153,54 @@ impl QuestionMark {
122153
}
123154
}
124155

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(cx, nested_expr, expr)
156+
/// Check whether the encountered expression contains `else if`
157+
fn expr_is_else_if(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
158+
if let ExprKind::If(..) = expr.kind
159+
&& let Some(parent_expr) = clippy_utils::get_parent_expr(cx, expr)
160+
&& let ExprKind::If(..) = parent_expr.kind
161+
{
162+
return true;
163+
}
164+
false
165+
}
166+
167+
/// Check whether the fixed code needs semicolon after `?`
168+
///
169+
/// The fixed code of this expression doesn't need semi
170+
/// ```ignore
171+
/// let _ = if let Some(x) = Some(0) {
172+
/// x
173+
/// } else {
174+
/// return None;
175+
/// };
176+
/// ```
177+
///
178+
/// The fixed code of this expression needs semi
179+
/// ```ignore
180+
/// if let Err(err) = foo() {
181+
/// return Err(err);
182+
/// }
183+
/// ```
184+
fn is_semicolon_needed(if_then: &Expr<'_>, if_else: Option<&'_ Expr<'_>>) -> bool {
185+
let if_then_kind = &peel_blocks_with_stmt(if_then).kind;
186+
let if_then_is_ret_stmt = matches!(if_then_kind, ExprKind::Ret(_));
187+
188+
if if_else.is_none() {
189+
return if_then_is_ret_stmt;
190+
}
191+
if let ExprKind::Ret(_) = peel_blocks_with_stmt(if_else.unwrap()).kind {
192+
return if_then_is_ret_stmt;
193+
}
194+
false
195+
}
196+
197+
fn result_check_and_early_return(
198+
cx: &LateContext<'_>,
199+
expr: &Expr<'_>,
200+
nested_expr: &Expr<'_>,
201+
pat_symbol: Option<Symbol>,
202+
) -> bool {
203+
Self::is_result(cx, expr) && Self::expression_returns_unmodified_err(cx, nested_expr, expr, pat_symbol)
127204
}
128205

129206
fn option_check_and_early_return(cx: &LateContext<'_>, expr: &Expr<'_>, nested_expr: &Expr<'_>) -> bool {
@@ -156,10 +233,32 @@ impl QuestionMark {
156233
}
157234
}
158235

159-
fn expression_returns_unmodified_err(cx: &LateContext<'_>, 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(cx, ret_expr, cond_expr),
236+
fn expression_returns_unmodified_err(
237+
cx: &LateContext<'_>,
238+
expr: &Expr<'_>,
239+
cond_expr: &Expr<'_>,
240+
pat_symbol: Option<Symbol>,
241+
) -> bool {
242+
match &peel_blocks_with_stmt(expr).kind {
243+
ExprKind::Ret(Some(ret_expr)) => {
244+
Self::expression_returns_unmodified_err(cx, ret_expr, cond_expr, pat_symbol)
245+
},
162246
ExprKind::Path(_) => path_to_local(expr).is_some() && path_to_local(expr) == path_to_local(cond_expr),
247+
ExprKind::Call(call_expr, args_expr) => {
248+
if let ExprKind::Path(qpath) = &call_expr.kind
249+
&& let QPath::Resolved(_, path) = qpath
250+
&& let Some(segment) = path.segments.first()
251+
&& let Some(pat_sym) = pat_symbol
252+
&& let Some(arg) = args_expr.first()
253+
&& let ExprKind::Path(arg_qpath) = &arg.kind
254+
&& let QPath::Resolved(_, arg_path) = arg_qpath
255+
&& let Some(PathSegment { ident, .. }) = arg_path.segments.first()
256+
{
257+
return segment.ident.name == sym::Err && pat_sym == ident.name
258+
}
259+
260+
false
261+
},
163262
_ => false,
164263
}
165264
}

tests/ui/question_mark.fixed

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,64 @@ fn f() -> NotOption {
154154
NotOption::First
155155
}
156156

157+
#[allow(dead_code)]
158+
mod issue8288 {
159+
fn do_something() {}
160+
161+
fn foo() -> Result<(), i32> {
162+
Ok(())
163+
}
164+
165+
fn bar() -> Result<(), i32> {
166+
foo()?;
167+
168+
Ok(())
169+
}
170+
171+
fn baz() -> Result<(), i32> {
172+
foo()?;
173+
do_something();
174+
175+
Ok(())
176+
}
177+
178+
// No warning
179+
fn qux() -> Result<(), i32> {
180+
if let Err(err) = foo() {
181+
do_something();
182+
return Err(err);
183+
}
184+
185+
Ok(())
186+
}
187+
188+
// No warning
189+
fn quux() -> Option<i32> {
190+
if let Err(err) = foo() {
191+
return Some(err);
192+
}
193+
194+
None
195+
}
196+
197+
// No warning
198+
fn quuz() -> Result<(), i32> {
199+
if true {
200+
Ok(())
201+
} else if let Err(e) = foo() {
202+
Err(e)
203+
} else {
204+
Err(-1)
205+
}
206+
}
207+
208+
// No warning
209+
#[allow(clippy::manual_map)]
210+
fn option_map() -> Option<bool> {
211+
if let Some(a) = Some(false) { Some(!a) } else { None }
212+
}
213+
}
214+
157215
fn main() {
158216
some_func(Some(42));
159217
some_func(None);

tests/ui/question_mark.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,68 @@ fn f() -> NotOption {
186186
NotOption::First
187187
}
188188

189+
#[allow(dead_code)]
190+
mod issue8288 {
191+
fn do_something() {}
192+
193+
fn foo() -> Result<(), i32> {
194+
Ok(())
195+
}
196+
197+
fn bar() -> Result<(), i32> {
198+
if let Err(err) = foo() {
199+
return Err(err);
200+
}
201+
202+
Ok(())
203+
}
204+
205+
fn baz() -> Result<(), i32> {
206+
if let Err(err) = foo() {
207+
return Err(err);
208+
}
209+
do_something();
210+
211+
Ok(())
212+
}
213+
214+
// No warning
215+
fn qux() -> Result<(), i32> {
216+
if let Err(err) = foo() {
217+
do_something();
218+
return Err(err);
219+
}
220+
221+
Ok(())
222+
}
223+
224+
// No warning
225+
fn quux() -> Option<i32> {
226+
if let Err(err) = foo() {
227+
return Some(err);
228+
}
229+
230+
None
231+
}
232+
233+
// No warning
234+
fn quuz() -> Result<(), i32> {
235+
if true {
236+
Ok(())
237+
} else if let Err(e) = foo() {
238+
Err(e)
239+
} else {
240+
Err(-1)
241+
}
242+
}
243+
244+
// No warning
245+
#[allow(clippy::manual_map)]
246+
fn option_map() -> Option<bool> {
247+
if let Some(a) = Some(false) { Some(!a) } else { None }
248+
}
249+
}
250+
189251
fn main() {
190252
some_func(Some(42));
191253
some_func(None);

tests/ui/question_mark.stderr

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,5 +114,21 @@ LL | | return x;
114114
LL | | }
115115
| |_____^ help: replace it with: `x?;`
116116

117-
error: aborting due to 13 previous errors
117+
error: this if-let-else may be rewritten with the `?` operator
118+
--> $DIR/question_mark.rs:198:9
119+
|
120+
LL | / if let Err(err) = foo() {
121+
LL | | return Err(err);
122+
LL | | }
123+
| |_________^ help: replace it with: `foo()?;`
124+
125+
error: this if-let-else may be rewritten with the `?` operator
126+
--> $DIR/question_mark.rs:206:9
127+
|
128+
LL | / if let Err(err) = foo() {
129+
LL | | return Err(err);
130+
LL | | }
131+
| |_________^ help: replace it with: `foo()?;`
132+
133+
error: aborting due to 15 previous errors
118134

0 commit comments

Comments
 (0)