Skip to content

Commit 8ca1274

Browse files
committed
Add match-based manual try to clippy::question_mark
1 parent 12ca363 commit 8ca1274

File tree

4 files changed

+219
-15
lines changed

4 files changed

+219
-15
lines changed

clippy_lints/src/question_mark.rs

Lines changed: 146 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@ use clippy_utils::source::snippet_with_applicability;
88
use clippy_utils::ty::{implements_trait, is_type_diagnostic_item};
99
use clippy_utils::{
1010
eq_expr_value, higher, is_else_clause, is_in_const_context, is_lint_allowed, is_path_lang_item, is_res_lang_ctor,
11-
pat_and_expr_can_be_question_mark, path_to_local, path_to_local_id, peel_blocks, peel_blocks_with_stmt,
11+
pat_and_expr_can_be_question_mark, path_res, path_to_local, path_to_local_id, peel_blocks, peel_blocks_with_stmt,
1212
span_contains_comment,
1313
};
1414
use rustc_errors::Applicability;
1515
use rustc_hir::LangItem::{self, OptionNone, OptionSome, ResultErr, ResultOk};
1616
use rustc_hir::def::Res;
1717
use rustc_hir::{
18-
BindingMode, Block, Body, ByRef, Expr, ExprKind, LetStmt, Mutability, Node, PatKind, PathSegment, QPath, Stmt,
19-
StmtKind,
18+
Arm, BindingMode, Block, Body, ByRef, Expr, ExprKind, HirId, LetStmt, MatchSource, Mutability, Node, Pat, PatKind,
19+
PathSegment, QPath, Stmt, StmtKind,
2020
};
2121
use rustc_lint::{LateContext, LateLintPass};
22-
use rustc_middle::ty::Ty;
22+
use rustc_middle::ty::{self, Ty};
2323
use rustc_session::impl_lint_pass;
2424
use rustc_span::sym;
2525
use rustc_span::symbol::Symbol;
@@ -271,6 +271,140 @@ fn check_is_none_or_err_and_early_return<'tcx>(cx: &LateContext<'tcx>, expr: &Ex
271271
}
272272
}
273273

274+
#[derive(Clone, Copy, Debug)]
275+
enum TryMode {
276+
Result,
277+
Option,
278+
}
279+
280+
fn find_try_mode<'tcx>(cx: &LateContext<'tcx>, scrutinee: &Expr<'tcx>) -> Option<TryMode> {
281+
let scrutinee_ty = cx.typeck_results().expr_ty_adjusted(scrutinee);
282+
let ty::Adt(scrutinee_adt_def, _) = scrutinee_ty.kind() else {
283+
return None;
284+
};
285+
286+
match cx.tcx.get_diagnostic_name(scrutinee_adt_def.did())? {
287+
sym::Result => Some(TryMode::Result),
288+
sym::Option => Some(TryMode::Option),
289+
_ => None,
290+
}
291+
}
292+
293+
// Check that `pat` is `{ctor_lang_item}(val)`, returning `val`.
294+
fn extract_ctor_call<'a, 'tcx>(
295+
cx: &LateContext<'tcx>,
296+
expected_ctor: LangItem,
297+
pat: &'a Pat<'tcx>,
298+
) -> Option<&'a Pat<'tcx>> {
299+
if let PatKind::TupleStruct(variant_path, [val_binding], _) = &pat.kind
300+
&& is_res_lang_ctor(cx, cx.qpath_res(variant_path, pat.hir_id), expected_ctor)
301+
{
302+
Some(val_binding)
303+
} else {
304+
None
305+
}
306+
}
307+
308+
// Extracts the local ID of a plain `val` pattern.
309+
fn extract_binding_pat(pat: &Pat<'_>) -> Option<HirId> {
310+
if let PatKind::Binding(BindingMode::NONE, binding, _, None) = pat.kind {
311+
Some(binding)
312+
} else {
313+
None
314+
}
315+
}
316+
317+
// Check `arm` is `Ok(val) => val` or `Some(val) => val`.
318+
fn check_arm_is_happy<'tcx>(cx: &LateContext<'tcx>, mode: TryMode, arm: &Arm<'tcx>) -> bool {
319+
let arm_body = arm.body.peel_blocks().peel_drop_temps();
320+
let happy_ctor = match mode {
321+
TryMode::Result => ResultOk,
322+
TryMode::Option => OptionSome,
323+
};
324+
325+
// Check for `Ok(val)` or `Some(val)`
326+
if arm.guard.is_none()
327+
&& let Some(val_binding) = extract_ctor_call(cx, happy_ctor, arm.pat)
328+
// Extract out `val`
329+
&& let Some(binding) = extract_binding_pat(val_binding)
330+
// Check body is just `=> val`
331+
&& let ExprKind::Path(body_path) = &arm_body.kind
332+
&& let Res::Local(body_local_ref) = cx.qpath_res(body_path, arm_body.hir_id)
333+
&& binding == body_local_ref
334+
{
335+
true
336+
} else {
337+
false
338+
}
339+
}
340+
341+
// Check `arm` is `Err(err) => return Err(err)` or `None => return None`.
342+
fn check_arm_is_sad<'tcx>(cx: &LateContext<'tcx>, mode: TryMode, arm: &Arm<'tcx>) -> bool {
343+
if arm.guard.is_some() {
344+
return false;
345+
}
346+
347+
let arm_body = arm.body.peel_blocks().peel_drop_temps();
348+
match mode {
349+
TryMode::Result => {
350+
// Check that pat is Err(val)
351+
if let Some(ok_pat) = extract_ctor_call(cx, ResultErr, arm.pat)
352+
&& let Some(ok_val) = extract_binding_pat(ok_pat)
353+
// check `=> return Err(...)`
354+
&& let ExprKind::Ret(Some(wrapped_ret_expr)) = arm_body.kind
355+
&& let ExprKind::Call(ok_ctor, [ret_expr]) = wrapped_ret_expr.kind
356+
&& is_res_lang_ctor(cx, path_res(cx, ok_ctor), ResultErr)
357+
// check `...` is `val` from binding
358+
&& let Res::Local(ret_local_ref) = path_res(cx, ret_expr)
359+
&& ok_val == ret_local_ref
360+
{
361+
true
362+
} else {
363+
false
364+
}
365+
},
366+
TryMode::Option => {
367+
// Check the pat is `None`
368+
if is_res_lang_ctor(cx, path_res(cx, arm.pat), OptionNone)
369+
// Check `=> return None`
370+
&& let ExprKind::Ret(Some(ret_expr)) = arm_body.kind
371+
&& is_res_lang_ctor(cx, path_res(cx, ret_expr), OptionNone)
372+
{
373+
true
374+
} else {
375+
false
376+
}
377+
},
378+
}
379+
}
380+
381+
fn check_arms_are_try<'tcx>(cx: &LateContext<'tcx>, mode: TryMode, arm1: &Arm<'tcx>, arm2: &Arm<'tcx>) -> bool {
382+
(check_arm_is_happy(cx, mode, arm1) && check_arm_is_sad(cx, mode, arm2))
383+
|| (check_arm_is_happy(cx, mode, arm2) && check_arm_is_sad(cx, mode, arm1))
384+
}
385+
386+
fn check_if_try_match<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
387+
if let ExprKind::Match(scrutinee, [arm1, arm2], MatchSource::Normal | MatchSource::Postfix) = expr.kind
388+
&& !expr.span.from_expansion()
389+
&& let Some(mode) = find_try_mode(cx, scrutinee)
390+
&& check_arms_are_try(cx, mode, arm1, arm2)
391+
{
392+
let mut applicability = Applicability::MachineApplicable;
393+
let mut snippet = snippet_with_applicability(cx, scrutinee.span, "...", &mut applicability).into_owned();
394+
snippet.push('?');
395+
396+
span_lint_and_sugg(
397+
cx,
398+
QUESTION_MARK,
399+
expr.span,
400+
"this `match` expression can be replaced with `?`",
401+
"try instead",
402+
snippet,
403+
applicability,
404+
);
405+
}
406+
}
407+
274408
fn check_if_let_some_or_err_and_early_return<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
275409
if let Some(higher::IfLet {
276410
let_pat,
@@ -350,11 +484,15 @@ impl<'tcx> LateLintPass<'tcx> for QuestionMark {
350484
}
351485
self.check_manual_let_else(cx, stmt);
352486
}
487+
353488
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
354-
if !self.inside_try_block() && !is_in_const_context(cx) && is_lint_allowed(cx, QUESTION_MARK_USED, expr.hir_id)
355-
{
356-
check_is_none_or_err_and_early_return(cx, expr);
357-
check_if_let_some_or_err_and_early_return(cx, expr);
489+
if !is_in_const_context(cx) && is_lint_allowed(cx, QUESTION_MARK_USED, expr.hir_id) {
490+
if !self.inside_try_block() {
491+
check_is_none_or_err_and_early_return(cx, expr);
492+
check_if_let_some_or_err_and_early_return(cx, expr);
493+
}
494+
495+
check_if_try_match(cx, expr);
358496
}
359497
}
360498

tests/ui/question_mark.fixed

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ fn func() -> Option<i32> {
102102

103103
f()?;
104104

105+
let _val = f()?;
106+
107+
f()?;
108+
105109
Some(0)
106110
}
107111

@@ -114,6 +118,10 @@ fn result_func(x: Result<i32, i32>) -> Result<i32, i32> {
114118

115119
x?;
116120

121+
let _val = func_returning_result()?;
122+
123+
func_returning_result()?;
124+
117125
// No warning
118126
let y = if let Ok(x) = x {
119127
x

tests/ui/question_mark.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,16 @@ fn func() -> Option<i32> {
132132
return None;
133133
}
134134

135+
let _val = match f() {
136+
Some(val) => val,
137+
None => return None,
138+
};
139+
140+
match f() {
141+
Some(val) => val,
142+
None => return None,
143+
};
144+
135145
Some(0)
136146
}
137147

@@ -146,6 +156,16 @@ fn result_func(x: Result<i32, i32>) -> Result<i32, i32> {
146156
return x;
147157
}
148158

159+
let _val = match func_returning_result() {
160+
Ok(val) => val,
161+
Err(err) => return Err(err),
162+
};
163+
164+
match func_returning_result() {
165+
Ok(val) => val,
166+
Err(err) => return Err(err),
167+
};
168+
149169
// No warning
150170
let y = if let Ok(x) = x {
151171
x

tests/ui/question_mark.stderr

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,38 +101,76 @@ LL | | return None;
101101
LL | | }
102102
| |_____^ help: replace it with: `f()?;`
103103

104+
error: this `match` expression can be replaced with `?`
105+
--> tests/ui/question_mark.rs:135:16
106+
|
107+
LL | let _val = match f() {
108+
| ________________^
109+
LL | | Some(val) => val,
110+
LL | | None => return None,
111+
LL | | };
112+
| |_____^ help: try instead: `f()?`
113+
114+
error: this `match` expression can be replaced with `?`
115+
--> tests/ui/question_mark.rs:140:5
116+
|
117+
LL | / match f() {
118+
LL | | Some(val) => val,
119+
LL | | None => return None,
120+
LL | | };
121+
| |_____^ help: try instead: `f()?`
122+
104123
error: this block may be rewritten with the `?` operator
105-
--> tests/ui/question_mark.rs:143:13
124+
--> tests/ui/question_mark.rs:153:13
106125
|
107126
LL | let _ = if let Ok(x) = x { x } else { return x };
108127
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace it with: `x?`
109128

110129
error: this block may be rewritten with the `?` operator
111-
--> tests/ui/question_mark.rs:145:5
130+
--> tests/ui/question_mark.rs:155:5
112131
|
113132
LL | / if x.is_err() {
114133
LL | | return x;
115134
LL | | }
116135
| |_____^ help: replace it with: `x?;`
117136

137+
error: this `match` expression can be replaced with `?`
138+
--> tests/ui/question_mark.rs:159:16
139+
|
140+
LL | let _val = match func_returning_result() {
141+
| ________________^
142+
LL | | Ok(val) => val,
143+
LL | | Err(err) => return Err(err),
144+
LL | | };
145+
| |_____^ help: try instead: `func_returning_result()?`
146+
147+
error: this `match` expression can be replaced with `?`
148+
--> tests/ui/question_mark.rs:164:5
149+
|
150+
LL | / match func_returning_result() {
151+
LL | | Ok(val) => val,
152+
LL | | Err(err) => return Err(err),
153+
LL | | };
154+
| |_____^ help: try instead: `func_returning_result()?`
155+
118156
error: this block may be rewritten with the `?` operator
119-
--> tests/ui/question_mark.rs:213:5
157+
--> tests/ui/question_mark.rs:233:5
120158
|
121159
LL | / if let Err(err) = func_returning_result() {
122160
LL | | return Err(err);
123161
LL | | }
124162
| |_____^ help: replace it with: `func_returning_result()?;`
125163

126164
error: this block may be rewritten with the `?` operator
127-
--> tests/ui/question_mark.rs:220:5
165+
--> tests/ui/question_mark.rs:240:5
128166
|
129167
LL | / if let Err(err) = func_returning_result() {
130168
LL | | return Err(err);
131169
LL | | }
132170
| |_____^ help: replace it with: `func_returning_result()?;`
133171

134172
error: this block may be rewritten with the `?` operator
135-
--> tests/ui/question_mark.rs:297:13
173+
--> tests/ui/question_mark.rs:317:13
136174
|
137175
LL | / if a.is_none() {
138176
LL | | return None;
@@ -142,12 +180,12 @@ LL | | }
142180
| |_____________^ help: replace it with: `a?;`
143181

144182
error: this `let...else` may be rewritten with the `?` operator
145-
--> tests/ui/question_mark.rs:357:5
183+
--> tests/ui/question_mark.rs:377:5
146184
|
147185
LL | / let Some(v) = bar.foo.owned.clone() else {
148186
LL | | return None;
149187
LL | | };
150188
| |______^ help: replace it with: `let v = bar.foo.owned.clone()?;`
151189

152-
error: aborting due to 17 previous errors
190+
error: aborting due to 21 previous errors
153191

0 commit comments

Comments
 (0)