Skip to content

Commit 500f1e4

Browse files
committed
Rewrite unwrap_in_result lint
- Lint non-`impl` functions as well. - Lint function calls in addition to method calls (such as `Option::unwrap(…)`). - Put the lint on the `unwrap` and `expect` node instead of the function signature. This lets the user `#[allow]` specific `unwrap()` or `expect()` calls. - Do not lint inside closures, `const` and `static`. - Do not mix warnings about `Option` and `Result`.
1 parent 388d3f3 commit 500f1e4

File tree

4 files changed

+309
-102
lines changed

4 files changed

+309
-102
lines changed

clippy_lints/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
654654
store.register_early_pass(move || Box::new(nonstandard_macro_braces::MacroBraces::new(conf)));
655655
store.register_late_pass(|_| Box::<macro_use::MacroUseImports>::default());
656656
store.register_late_pass(|_| Box::new(pattern_type_mismatch::PatternTypeMismatch));
657-
store.register_late_pass(|_| Box::new(unwrap_in_result::UnwrapInResult));
657+
store.register_late_pass(|_| Box::<unwrap_in_result::UnwrapInResult>::default());
658658
store.register_late_pass(|_| Box::new(semicolon_if_nothing_returned::SemicolonIfNothingReturned));
659659
store.register_late_pass(|_| Box::new(async_yields_async::AsyncYieldsAsync));
660660
let attrs = attr_storage.clone();

clippy_lints/src/unwrap_in_result.rs

Lines changed: 155 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
use clippy_utils::diagnostics::span_lint_and_then;
2-
use clippy_utils::ty::is_type_diagnostic_item;
3-
use clippy_utils::visitors::for_each_expr;
4-
use clippy_utils::{method_chain_args, return_ty};
5-
use core::ops::ControlFlow;
6-
use rustc_hir as hir;
7-
use rustc_hir::ImplItemKind;
2+
use clippy_utils::{return_ty, sym};
3+
use rustc_hir::{
4+
Body, BodyOwnerKind, Expr, ExprKind, FnSig, ImplItem, ImplItemKind, Item, ItemKind, OwnerId, PathSegment, QPath,
5+
};
86
use rustc_lint::{LateContext, LateLintPass};
9-
use rustc_session::declare_lint_pass;
10-
use rustc_span::{Span, sym};
7+
use rustc_middle::ty::Ty;
8+
use rustc_session::impl_lint_pass;
9+
use rustc_span::{Ident, Span, Symbol};
1110

1211
declare_clippy_lint! {
1312
/// ### What it does
@@ -57,62 +56,166 @@ declare_clippy_lint! {
5756
"functions of type `Result<..>` or `Option`<...> that contain `expect()` or `unwrap()`"
5857
}
5958

60-
declare_lint_pass!(UnwrapInResult=> [UNWRAP_IN_RESULT]);
59+
impl_lint_pass!(UnwrapInResult=> [UNWRAP_IN_RESULT]);
6160

62-
impl<'tcx> LateLintPass<'tcx> for UnwrapInResult {
63-
fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &'tcx hir::ImplItem<'_>) {
64-
if let ImplItemKind::Fn(ref _signature, _) = impl_item.kind
65-
// first check if it's a method or function
66-
// checking if its return type is `result` or `option`
67-
&& (is_type_diagnostic_item(cx, return_ty(cx, impl_item.owner_id), sym::Result)
68-
|| is_type_diagnostic_item(cx, return_ty(cx, impl_item.owner_id), sym::Option))
69-
{
70-
lint_impl_body(cx, impl_item.span, impl_item);
61+
#[derive(Clone, Copy, Eq, PartialEq)]
62+
enum OptionOrResult {
63+
Option,
64+
Result,
65+
}
66+
67+
impl OptionOrResult {
68+
fn with_article(self) -> &'static str {
69+
if matches!(self, Self::Option) {
70+
"an `Option`"
71+
} else {
72+
"a `Result`"
7173
}
7274
}
7375
}
7476

75-
fn lint_impl_body<'tcx>(cx: &LateContext<'tcx>, impl_span: Span, impl_item: &'tcx hir::ImplItem<'_>) {
76-
if let ImplItemKind::Fn(_, body_id) = impl_item.kind {
77-
let body = cx.tcx.hir_body(body_id);
78-
let typeck = cx.tcx.typeck(impl_item.owner_id.def_id);
79-
let mut result = Vec::new();
80-
let _: Option<!> = for_each_expr(cx, body.value, |e| {
81-
// check for `expect`
82-
if let Some(arglists) = method_chain_args(e, &[sym::expect]) {
83-
let receiver_ty = typeck.expr_ty(arglists[0].0).peel_refs();
84-
if is_type_diagnostic_item(cx, receiver_ty, sym::Option)
85-
|| is_type_diagnostic_item(cx, receiver_ty, sym::Result)
86-
{
87-
result.push(e.span);
88-
}
89-
}
90-
91-
// check for `unwrap`
92-
if let Some(arglists) = method_chain_args(e, &[sym::unwrap]) {
93-
let receiver_ty = typeck.expr_ty(arglists[0].0).peel_refs();
94-
if is_type_diagnostic_item(cx, receiver_ty, sym::Option)
95-
|| is_type_diagnostic_item(cx, receiver_ty, sym::Result)
96-
{
97-
result.push(e.span);
98-
}
99-
}
100-
101-
ControlFlow::Continue(())
77+
struct OptionOrResultFn {
78+
kind: OptionOrResult,
79+
return_ty_span: Option<Span>,
80+
}
81+
82+
#[derive(Default)]
83+
pub struct UnwrapInResult {
84+
fn_stack: Vec<Option<OptionOrResultFn>>,
85+
current_fn: Option<OptionOrResultFn>,
86+
}
87+
88+
impl UnwrapInResult {
89+
fn enter_item(&mut self, cx: &LateContext<'_>, fn_def_id: OwnerId, sig: &FnSig<'_>) {
90+
self.fn_stack.push(self.current_fn.take());
91+
self.current_fn = is_option_or_result(cx, return_ty(cx, fn_def_id)).map(|kind| OptionOrResultFn {
92+
kind,
93+
return_ty_span: Some(sig.decl.output.span()),
10294
});
95+
}
10396

104-
// if we've found one, lint
105-
if !result.is_empty() {
97+
fn leave_item(&mut self) {
98+
self.current_fn = self.fn_stack.pop().unwrap();
99+
}
100+
}
101+
102+
impl<'tcx> LateLintPass<'tcx> for UnwrapInResult {
103+
fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &'tcx ImplItem<'_>) {
104+
if let ImplItemKind::Fn(sig, _) = &impl_item.kind {
105+
self.enter_item(cx, impl_item.owner_id, sig);
106+
}
107+
}
108+
109+
fn check_impl_item_post(&mut self, _: &LateContext<'tcx>, impl_item: &'tcx ImplItem<'tcx>) {
110+
if let ImplItemKind::Fn(..) = impl_item.kind {
111+
self.leave_item();
112+
}
113+
}
114+
115+
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
116+
if let ItemKind::Fn {
117+
has_body: true, sig, ..
118+
} = &item.kind
119+
{
120+
self.enter_item(cx, item.owner_id, sig);
121+
}
122+
}
123+
124+
fn check_item_post(&mut self, _: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
125+
if let ItemKind::Fn { has_body: true, .. } = item.kind {
126+
self.leave_item();
127+
}
128+
}
129+
130+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'_>) {
131+
if expr.span.from_expansion() {
132+
return;
133+
}
134+
135+
if let Some(OptionOrResultFn {
136+
kind,
137+
ref mut return_ty_span,
138+
}) = self.current_fn
139+
&& let Some((oor, fn_name)) = is_unwrap_or_expect_call(cx, expr)
140+
&& oor == kind
141+
{
106142
span_lint_and_then(
107143
cx,
108144
UNWRAP_IN_RESULT,
109-
impl_span,
110-
"used unwrap or expect in a function that returns result or option",
111-
move |diag| {
112-
diag.help("unwrap and expect should not be used in a function that returns result or option");
113-
diag.span_note(result, "potential non-recoverable error(s)");
145+
expr.span,
146+
format!("`{fn_name}` used in a function that returns {}", kind.with_article()),
147+
|diag| {
148+
// Issue the note and help only once per function
149+
if let Some(span) = return_ty_span.take() {
150+
diag.span_note(span, "in this function signature");
151+
let complement = if kind == OptionOrResult::Result {
152+
" or calling the `.map_err()` method"
153+
} else {
154+
""
155+
};
156+
diag.help(format!("consider using the `?` operator{complement}"));
157+
}
114158
},
115159
);
116160
}
117161
}
162+
163+
fn check_body(&mut self, cx: &LateContext<'tcx>, body: &Body<'tcx>) {
164+
let body_def_id = cx.tcx.hir_body_owner_def_id(body.id());
165+
if !matches!(cx.tcx.hir_body_owner_kind(body_def_id), BodyOwnerKind::Fn) {
166+
// When entering a body which is not a function, mask the potential surrounding
167+
// function to not apply the lint.
168+
self.fn_stack.push(self.current_fn.take());
169+
}
170+
}
171+
172+
fn check_body_post(&mut self, cx: &LateContext<'tcx>, body: &Body<'tcx>) {
173+
let body_def_id = cx.tcx.hir_body_owner_def_id(body.id());
174+
if !matches!(cx.tcx.hir_body_owner_kind(body_def_id), BodyOwnerKind::Fn) {
175+
// Unmask the potential surrounding function.
176+
self.current_fn = self.fn_stack.pop().unwrap();
177+
}
178+
}
179+
}
180+
181+
fn is_option_or_result(cx: &LateContext<'_>, ty: Ty<'_>) -> Option<OptionOrResult> {
182+
match ty.ty_adt_def().and_then(|def| cx.tcx.get_diagnostic_name(def.did())) {
183+
Some(sym::Option) => Some(OptionOrResult::Option),
184+
Some(sym::Result) => Some(OptionOrResult::Result),
185+
_ => None,
186+
}
187+
}
188+
189+
fn is_unwrap_or_expect_call(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<(OptionOrResult, Symbol)> {
190+
if let ExprKind::Call(func, _) = expr.kind
191+
&& let ExprKind::Path(QPath::TypeRelative(
192+
hir_ty,
193+
PathSegment {
194+
ident:
195+
Ident {
196+
name: name @ (sym::unwrap | sym::expect),
197+
..
198+
},
199+
..
200+
},
201+
)) = func.kind
202+
{
203+
is_option_or_result(cx, cx.typeck_results().node_type(hir_ty.hir_id)).map(|oor| (oor, *name))
204+
} else if let ExprKind::MethodCall(
205+
PathSegment {
206+
ident: Ident {
207+
name: name @ (sym::unwrap | sym::expect),
208+
..
209+
},
210+
..
211+
},
212+
recv,
213+
_,
214+
_,
215+
) = expr.kind
216+
{
217+
is_option_or_result(cx, cx.typeck_results().expr_ty_adjusted(recv)).map(|oor| (oor, *name))
218+
} else {
219+
None
220+
}
118221
}

tests/ui/unwrap_in_result.rs

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#![warn(clippy::unwrap_in_result)]
2+
#![allow(clippy::ok_expect)]
23

34
struct A;
45

@@ -20,10 +21,9 @@ impl A {
2021

2122
// should be detected
2223
fn bad_divisible_by_3(i_str: String) -> Result<bool, String> {
23-
//~^ unwrap_in_result
24-
2524
// checks whether a string represents a number divisible by 3
2625
let i = i_str.parse::<i32>().unwrap();
26+
//~^ unwrap_in_result
2727
if i % 3 == 0 {
2828
Ok(true)
2929
} else {
@@ -32,23 +32,75 @@ impl A {
3232
}
3333

3434
fn example_option_expect(i_str: String) -> Option<bool> {
35+
let i = i_str.parse::<i32>().ok().expect("not a number");
3536
//~^ unwrap_in_result
36-
37-
let i = i_str.parse::<i32>().expect("not a number");
3837
if i % 3 == 0 {
3938
return Some(true);
4039
}
4140
None
4241
}
4342

4443
fn in_closure(a: Option<bool>) -> Option<bool> {
45-
//~^ unwrap_in_result
44+
// No lint inside a closure
4645
let c = || a.unwrap();
47-
Some(c())
46+
47+
// But lint outside
48+
let a = c().then_some(true);
49+
let _ = a.unwrap();
50+
//~^ unwrap_in_result
51+
52+
None
4853
}
54+
55+
const fn in_const_inside_fn() -> bool {
56+
const A: bool = {
57+
const fn inner(b: Option<bool>) -> Option<bool> {
58+
Some(b.unwrap())
59+
//~^ unwrap_in_result
60+
}
61+
62+
// No lint inside `const`
63+
inner(Some(true)).unwrap()
64+
};
65+
A
66+
}
67+
68+
fn in_static_inside_fn() -> bool {
69+
static A: bool = {
70+
const fn inner(b: Option<bool>) -> Option<bool> {
71+
Some(b.unwrap())
72+
//~^ unwrap_in_result
73+
}
74+
75+
// No lint inside `static`
76+
inner(Some(true)).unwrap()
77+
};
78+
A
79+
}
80+
}
81+
82+
macro_rules! mac {
83+
() => {
84+
Option::unwrap(Some(3))
85+
};
86+
}
87+
88+
fn type_relative_unwrap() -> Option<()> {
89+
_ = Option::unwrap(Some(3));
90+
//~^ unwrap_in_result
91+
92+
// Do not lint macro output
93+
_ = mac!();
94+
95+
None
4996
}
5097

51-
fn main() {
52-
A::bad_divisible_by_3("3".to_string());
53-
A::good_divisible_by_3("3".to_string());
98+
fn main() -> Result<(), ()> {
99+
A::bad_divisible_by_3("3".to_string()).unwrap();
100+
//~^ unwrap_in_result
101+
A::good_divisible_by_3("3".to_string()).unwrap();
102+
//~^ unwrap_in_result
103+
Result::unwrap(A::good_divisible_by_3("3".to_string()));
104+
//~^ unwrap_in_result
105+
Ok(())
54106
}

0 commit comments

Comments
 (0)