Skip to content

Commit 9373413

Browse files
authored
Rewrite unwrap_in_result lint (#15445)
- 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`. changelog: [`unwrap_in_result`]: issue lint on `.unwrap()` and `.expect()` nodes instead of the function one changelog: [`unwrap_in_result`]: also lint in functions outside implementation blocks changelog: [`unwrap_in_result`]: do not mix `Result` and `Option` Fixes rust-lang/rust-clippy#15439
2 parents ad419b8 + 9d05ac4 commit 9373413

File tree

4 files changed

+308
-102
lines changed

4 files changed

+308
-102
lines changed

clippy_lints/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
656656
store.register_early_pass(move || Box::new(nonstandard_macro_braces::MacroBraces::new(conf)));
657657
store.register_late_pass(|_| Box::<macro_use::MacroUseImports>::default());
658658
store.register_late_pass(|_| Box::new(pattern_type_mismatch::PatternTypeMismatch));
659-
store.register_late_pass(|_| Box::new(unwrap_in_result::UnwrapInResult));
659+
store.register_late_pass(|_| Box::<unwrap_in_result::UnwrapInResult>::default());
660660
store.register_late_pass(|_| Box::new(semicolon_if_nothing_returned::SemicolonIfNothingReturned));
661661
store.register_late_pass(|_| Box::new(async_yields_async::AsyncYieldsAsync));
662662
let attrs = attr_storage.clone();
Lines changed: 154 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,165 @@ 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+
match self {
70+
Self::Option => "an `Option`",
71+
Self::Result => "a `Result`",
7172
}
7273
}
7374
}
7475

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(())
76+
struct OptionOrResultFn {
77+
kind: OptionOrResult,
78+
return_ty_span: Option<Span>,
79+
}
80+
81+
#[derive(Default)]
82+
pub struct UnwrapInResult {
83+
fn_stack: Vec<Option<OptionOrResultFn>>,
84+
current_fn: Option<OptionOrResultFn>,
85+
}
86+
87+
impl UnwrapInResult {
88+
fn enter_item(&mut self, cx: &LateContext<'_>, fn_def_id: OwnerId, sig: &FnSig<'_>) {
89+
self.fn_stack.push(self.current_fn.take());
90+
self.current_fn = is_option_or_result(cx, return_ty(cx, fn_def_id)).map(|kind| OptionOrResultFn {
91+
kind,
92+
return_ty_span: Some(sig.decl.output.span()),
10293
});
94+
}
10395

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

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)