Skip to content

Commit d68b8ce

Browse files
authored
Merge pull request #2811 from fanzier/checked_unwrap
Implement lint that checks for unidiomatic unwrap() (closes #1770)
2 parents d0620ae + 54826cf commit d68b8ce

File tree

7 files changed

+510
-66
lines changed

7 files changed

+510
-66
lines changed

clippy_lints/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ pub mod unicode;
200200
pub mod unsafe_removed_from_name;
201201
pub mod unused_io_amount;
202202
pub mod unused_label;
203+
pub mod unwrap;
203204
pub mod use_self;
204205
pub mod vec;
205206
pub mod write;
@@ -421,6 +422,8 @@ pub fn register_plugins(reg: &mut rustc_plugin::Registry) {
421422
reg.register_late_lint_pass(box infallible_destructuring_match::Pass);
422423
reg.register_late_lint_pass(box inherent_impl::Pass::default());
423424
reg.register_late_lint_pass(box neg_cmp_op_on_partial_ord::NoNegCompOpForPartialOrd);
425+
reg.register_late_lint_pass(box unwrap::Pass);
426+
424427

425428
reg.register_lint_group("clippy_restriction", vec![
426429
arithmetic::FLOAT_ARITHMETIC,
@@ -837,6 +840,7 @@ pub fn register_plugins(reg: &mut rustc_plugin::Registry) {
837840
types::UNIT_ARG,
838841
types::UNNECESSARY_CAST,
839842
unused_label::UNUSED_LABEL,
843+
unwrap::UNNECESSARY_UNWRAP,
840844
zero_div_zero::ZERO_DIVIDED_BY_ZERO,
841845
]);
842846

clippy_lints/src/loops.rs

Lines changed: 17 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use std::iter::{once, Iterator};
1818
use syntax::ast;
1919
use syntax::codemap::Span;
2020
use crate::utils::{sugg, sext};
21+
use crate::utils::usage::mutated_variables;
2122
use crate::consts::{constant, Constant};
2223

2324
use crate::utils::{get_enclosing_block, get_parent_expr, higher, in_external_macro, is_integer_literal, is_refutable,
@@ -504,8 +505,8 @@ impl<'a, 'tcx> LateLintPass<'a, 'tcx> for Pass {
504505
}
505506

506507
// check for while loops which conditions never change
507-
if let ExprWhile(ref cond, ref block, _) = expr.node {
508-
check_infinite_loop(cx, cond, block, expr);
508+
if let ExprWhile(ref cond, _, _) = expr.node {
509+
check_infinite_loop(cx, cond, expr);
509510
}
510511
}
511512

@@ -2145,35 +2146,30 @@ fn path_name(e: &Expr) -> Option<Name> {
21452146
None
21462147
}
21472148

2148-
fn check_infinite_loop<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, cond: &'tcx Expr, block: &'tcx Block, expr: &'tcx Expr) {
2149+
fn check_infinite_loop<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, cond: &'tcx Expr, expr: &'tcx Expr) {
21492150
if constant(cx, cx.tables, cond).is_some() {
21502151
// A pure constant condition (e.g. while false) is not linted.
21512152
return;
21522153
}
21532154

2154-
let mut mut_var_visitor = VarCollectorVisitor {
2155+
let mut var_visitor = VarCollectorVisitor {
21552156
cx,
2156-
ids: HashMap::new(),
2157+
ids: HashSet::new(),
21572158
def_ids: HashMap::new(),
21582159
skip: false,
21592160
};
2160-
mut_var_visitor.visit_expr(cond);
2161-
if mut_var_visitor.skip {
2161+
var_visitor.visit_expr(cond);
2162+
if var_visitor.skip {
21622163
return;
21632164
}
2164-
2165-
let mut delegate = MutVarsDelegate {
2166-
used_mutably: mut_var_visitor.ids,
2167-
skip: false,
2165+
let used_in_condition = &var_visitor.ids;
2166+
let no_cond_variable_mutated = if let Some(used_mutably) = mutated_variables(expr, cx) {
2167+
used_in_condition.is_disjoint(&used_mutably)
2168+
} else {
2169+
return
21682170
};
2169-
let def_id = def_id::DefId::local(block.hir_id.owner);
2170-
let region_scope_tree = &cx.tcx.region_scope_tree(def_id);
2171-
ExprUseVisitor::new(&mut delegate, cx.tcx, cx.param_env, region_scope_tree, cx.tables, None).walk_expr(expr);
2172-
2173-
if delegate.skip {
2174-
return;
2175-
}
2176-
if !(delegate.used_mutably.iter().any(|(_, v)| *v) || mut_var_visitor.def_ids.iter().any(|(_, v)| *v)) {
2171+
let mutable_static_in_cond = var_visitor.def_ids.iter().any(|(_, v)| *v);
2172+
if no_cond_variable_mutated && !mutable_static_in_cond {
21772173
span_lint(
21782174
cx,
21792175
WHILE_IMMUTABLE_CONDITION,
@@ -2189,7 +2185,7 @@ fn check_infinite_loop<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, cond: &'tcx Expr, b
21892185
/// All variables definition IDs are collected
21902186
struct VarCollectorVisitor<'a, 'tcx: 'a> {
21912187
cx: &'a LateContext<'a, 'tcx>,
2192-
ids: HashMap<NodeId, bool>,
2188+
ids: HashSet<NodeId>,
21932189
def_ids: HashMap<def_id::DefId, bool>,
21942190
skip: bool,
21952191
}
@@ -2203,7 +2199,7 @@ impl<'a, 'tcx> VarCollectorVisitor<'a, 'tcx> {
22032199
then {
22042200
match def {
22052201
Def::Local(node_id) | Def::Upvar(node_id, ..) => {
2206-
self.ids.insert(node_id, false);
2202+
self.ids.insert(node_id);
22072203
},
22082204
Def::Static(def_id, mutable) => {
22092205
self.def_ids.insert(def_id, mutable);
@@ -2230,48 +2226,3 @@ impl<'a, 'tcx> Visitor<'tcx> for VarCollectorVisitor<'a, 'tcx> {
22302226
NestedVisitorMap::None
22312227
}
22322228
}
2233-
2234-
struct MutVarsDelegate {
2235-
used_mutably: HashMap<NodeId, bool>,
2236-
skip: bool,
2237-
}
2238-
2239-
impl<'tcx> MutVarsDelegate {
2240-
fn update(&mut self, cat: &'tcx Categorization) {
2241-
match *cat {
2242-
Categorization::Local(id) =>
2243-
if let Some(used) = self.used_mutably.get_mut(&id) {
2244-
*used = true;
2245-
},
2246-
Categorization::Upvar(_) => {
2247-
//FIXME: This causes false negatives. We can't get the `NodeId` from
2248-
//`Categorization::Upvar(_)`. So we search for any `Upvar`s in the
2249-
//`while`-body, not just the ones in the condition.
2250-
self.skip = true
2251-
},
2252-
Categorization::Deref(ref cmt, _) | Categorization::Interior(ref cmt, _) => self.update(&cmt.cat),
2253-
_ => {}
2254-
}
2255-
}
2256-
}
2257-
2258-
2259-
impl<'tcx> Delegate<'tcx> for MutVarsDelegate {
2260-
fn consume(&mut self, _: NodeId, _: Span, _: &cmt_<'tcx>, _: ConsumeMode) {}
2261-
2262-
fn matched_pat(&mut self, _: &Pat, _: &cmt_<'tcx>, _: MatchMode) {}
2263-
2264-
fn consume_pat(&mut self, _: &Pat, _: &cmt_<'tcx>, _: ConsumeMode) {}
2265-
2266-
fn borrow(&mut self, _: NodeId, _: Span, cmt: &cmt_<'tcx>, _: ty::Region, bk: ty::BorrowKind, _: LoanCause) {
2267-
if let ty::BorrowKind::MutBorrow = bk {
2268-
self.update(&cmt.cat)
2269-
}
2270-
}
2271-
2272-
fn mutate(&mut self, _: NodeId, _: Span, cmt: &cmt_<'tcx>, _: MutateMode) {
2273-
self.update(&cmt.cat)
2274-
}
2275-
2276-
fn decl_without_init(&mut self, _: NodeId, _: Span) {}
2277-
}

clippy_lints/src/unwrap.rs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
use rustc::lint::*;
2+
3+
use crate::utils::{in_macro, match_type, paths, span_lint_and_then, usage::is_potentially_mutated};
4+
use rustc::hir::intravisit::*;
5+
use rustc::hir::*;
6+
use syntax::ast::NodeId;
7+
use syntax::codemap::Span;
8+
9+
/// **What it does:** Checks for calls of unwrap[_err]() that cannot fail.
10+
///
11+
/// **Why is this bad?** Using `if let` or `match` is more idiomatic.
12+
///
13+
/// **Known problems:** Limitations of the borrow checker might make unwrap() necessary sometimes?
14+
///
15+
/// **Example:**
16+
/// ```rust
17+
/// if option.is_some() {
18+
/// do_something_with(option.unwrap())
19+
/// }
20+
/// ```
21+
///
22+
/// Could be written:
23+
///
24+
/// ```rust
25+
/// if let Some(value) = option {
26+
/// do_something_with(value)
27+
/// }
28+
/// ```
29+
declare_clippy_lint! {
30+
pub UNNECESSARY_UNWRAP,
31+
nursery,
32+
"checks for calls of unwrap[_err]() that cannot fail"
33+
}
34+
35+
pub struct Pass;
36+
37+
/// Visitor that keeps track of which variables are unwrappable.
38+
struct UnwrappableVariablesVisitor<'a, 'tcx: 'a> {
39+
unwrappables: Vec<UnwrapInfo<'tcx>>,
40+
cx: &'a LateContext<'a, 'tcx>,
41+
}
42+
/// Contains information about whether a variable can be unwrapped.
43+
#[derive(Copy, Clone, Debug)]
44+
struct UnwrapInfo<'tcx> {
45+
/// The variable that is checked
46+
ident: &'tcx Path,
47+
/// The check, like `x.is_ok()`
48+
check: &'tcx Expr,
49+
/// Whether `is_some()` or `is_ok()` was called (as opposed to `is_err()` or `is_none()`).
50+
safe_to_unwrap: bool,
51+
}
52+
53+
/// Collects the information about unwrappable variables from an if condition
54+
/// The `invert` argument tells us whether the condition is negated.
55+
fn collect_unwrap_info<'a, 'tcx: 'a>(
56+
cx: &'a LateContext<'a, 'tcx>,
57+
expr: &'tcx Expr,
58+
invert: bool,
59+
) -> Vec<UnwrapInfo<'tcx>> {
60+
if let Expr_::ExprBinary(op, left, right) = &expr.node {
61+
match (invert, op.node) {
62+
(false, BinOp_::BiAnd) | (false, BinOp_::BiBitAnd) | (true, BinOp_::BiOr) | (true, BinOp_::BiBitOr) => {
63+
let mut unwrap_info = collect_unwrap_info(cx, left, invert);
64+
unwrap_info.append(&mut collect_unwrap_info(cx, right, invert));
65+
return unwrap_info;
66+
},
67+
_ => (),
68+
}
69+
} else if let Expr_::ExprUnary(UnNot, expr) = &expr.node {
70+
return collect_unwrap_info(cx, expr, !invert);
71+
} else {
72+
if_chain! {
73+
if let Expr_::ExprMethodCall(method_name, _, args) = &expr.node;
74+
if let Expr_::ExprPath(QPath::Resolved(None, path)) = &args[0].node;
75+
let ty = cx.tables.expr_ty(&args[0]);
76+
if match_type(cx, ty, &paths::OPTION) || match_type(cx, ty, &paths::RESULT);
77+
let name = method_name.name.as_str();
78+
if ["is_some", "is_none", "is_ok", "is_err"].contains(&&*name);
79+
then {
80+
assert!(args.len() == 1);
81+
let unwrappable = match name.as_ref() {
82+
"is_some" | "is_ok" => true,
83+
"is_err" | "is_none" => false,
84+
_ => unreachable!(),
85+
};
86+
let safe_to_unwrap = unwrappable != invert;
87+
return vec![UnwrapInfo { ident: path, check: expr, safe_to_unwrap }];
88+
}
89+
}
90+
}
91+
Vec::new()
92+
}
93+
94+
impl<'a, 'tcx: 'a> UnwrappableVariablesVisitor<'a, 'tcx> {
95+
fn visit_branch(&mut self, cond: &'tcx Expr, branch: &'tcx Expr, else_branch: bool) {
96+
let prev_len = self.unwrappables.len();
97+
for unwrap_info in collect_unwrap_info(self.cx, cond, else_branch) {
98+
if is_potentially_mutated(unwrap_info.ident, cond, self.cx)
99+
|| is_potentially_mutated(unwrap_info.ident, branch, self.cx)
100+
{
101+
// if the variable is mutated, we don't know whether it can be unwrapped:
102+
continue;
103+
}
104+
self.unwrappables.push(unwrap_info);
105+
}
106+
walk_expr(self, branch);
107+
self.unwrappables.truncate(prev_len);
108+
}
109+
}
110+
111+
impl<'a, 'tcx: 'a> Visitor<'tcx> for UnwrappableVariablesVisitor<'a, 'tcx> {
112+
fn visit_expr(&mut self, expr: &'tcx Expr) {
113+
if let Expr_::ExprIf(cond, then, els) = &expr.node {
114+
walk_expr(self, cond);
115+
self.visit_branch(cond, then, false);
116+
if let Some(els) = els {
117+
self.visit_branch(cond, els, true);
118+
}
119+
} else {
120+
// find `unwrap[_err]()` calls:
121+
if_chain! {
122+
if let Expr_::ExprMethodCall(ref method_name, _, ref args) = expr.node;
123+
if let Expr_::ExprPath(QPath::Resolved(None, ref path)) = args[0].node;
124+
if ["unwrap", "unwrap_err"].contains(&&*method_name.name.as_str());
125+
let call_to_unwrap = method_name.name == "unwrap";
126+
if let Some(unwrappable) = self.unwrappables.iter()
127+
.find(|u| u.ident.def == path.def && call_to_unwrap == u.safe_to_unwrap);
128+
then {
129+
span_lint_and_then(
130+
self.cx,
131+
UNNECESSARY_UNWRAP,
132+
expr.span,
133+
&format!("You checked before that `{}()` cannot fail. \
134+
Instead of checking and unwrapping, it's better to use `if let` or `match`.",
135+
method_name.name),
136+
|db| { db.span_label(unwrappable.check.span, "the check is happening here"); },
137+
);
138+
}
139+
}
140+
walk_expr(self, expr);
141+
}
142+
}
143+
144+
fn nested_visit_map<'this>(&'this mut self) -> NestedVisitorMap<'this, 'tcx> {
145+
NestedVisitorMap::OnlyBodies(&self.cx.tcx.hir)
146+
}
147+
}
148+
149+
impl<'a> LintPass for Pass {
150+
fn get_lints(&self) -> LintArray {
151+
lint_array!(UNNECESSARY_UNWRAP)
152+
}
153+
}
154+
155+
impl<'a, 'tcx> LateLintPass<'a, 'tcx> for Pass {
156+
fn check_fn(
157+
&mut self,
158+
cx: &LateContext<'a, 'tcx>,
159+
kind: FnKind<'tcx>,
160+
decl: &'tcx FnDecl,
161+
body: &'tcx Body,
162+
span: Span,
163+
fn_id: NodeId,
164+
) {
165+
if in_macro(span) {
166+
return;
167+
}
168+
169+
let mut v = UnwrappableVariablesVisitor {
170+
cx,
171+
unwrappables: Vec::new(),
172+
};
173+
174+
walk_fn(&mut v, kind, decl, body.id(), span, fn_id);
175+
}
176+
}

clippy_lints/src/utils/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub mod inspector;
3232
pub mod internal_lints;
3333
pub mod author;
3434
pub mod ptr;
35+
pub mod usage;
3536
pub use self::hir_utils::{SpanlessEq, SpanlessHash};
3637

3738
pub type MethodArgs = HirVec<P<Expr>>;

0 commit comments

Comments
 (0)