Skip to content

Commit c8e5fff

Browse files
authored
feat({unnecessary,panicking}_unwrap): lint field accesses (#15949)
Resolves #15321 Unresolved questions: - [ ] I did implement support for nested field accesses, but I'm not sure if that's desired - It brings some additional complexity, though not too much - It might be surprising for the user to get a lint on a direct field access, but not a nested one - It's unclear how often such nested case comes up changelog: [`unnecessary_unwrap`]: lint field accesses changelog: [`panicking_unwrap`]: lint field accesses
2 parents cd61be7 + 215cfed commit c8e5fff

File tree

9 files changed

+605
-232
lines changed

9 files changed

+605
-232
lines changed

clippy_lints/src/unwrap.rs

Lines changed: 176 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1+
use std::borrow::Cow;
2+
use std::iter;
3+
14
use clippy_config::Conf;
25
use clippy_utils::diagnostics::span_lint_hir_and_then;
36
use clippy_utils::msrvs::Msrv;
47
use clippy_utils::res::{MaybeDef, MaybeResPath};
8+
use clippy_utils::source::snippet;
59
use clippy_utils::usage::is_potentially_local_place;
610
use clippy_utils::{can_use_if_let_chains, higher, sym};
11+
use rustc_abi::FieldIdx;
712
use rustc_errors::Applicability;
813
use rustc_hir::intravisit::{FnKind, Visitor, walk_expr, walk_fn};
914
use rustc_hir::{BinOpKind, Body, Expr, ExprKind, FnDecl, HirId, Node, UnOp};
10-
use rustc_hir_typeck::expr_use_visitor::{Delegate, ExprUseVisitor, PlaceWithHirId};
11-
use rustc_lint::{LateContext, LateLintPass};
15+
use rustc_hir_typeck::expr_use_visitor::{Delegate, ExprUseVisitor, Place, PlaceWithHirId};
16+
use rustc_lint::{LateContext, LateLintPass, LintContext};
1217
use rustc_middle::hir::nested_filter;
18+
use rustc_middle::hir::place::ProjectionKind;
1319
use rustc_middle::mir::FakeReadCause;
1420
use rustc_middle::ty::{self, Ty, TyCtxt};
1521
use rustc_session::impl_lint_pass;
@@ -114,11 +120,91 @@ impl UnwrappableKind {
114120
}
115121
}
116122

123+
#[derive(Clone, Debug, Eq)]
124+
enum Local {
125+
/// `x.field1.field2.field3`
126+
WithFieldAccess {
127+
local_id: HirId,
128+
/// The indices of the field accessed.
129+
///
130+
/// Stored last-to-first, e.g. for the example above: `[field3, field2, field1]`
131+
field_indices: Vec<FieldIdx>,
132+
/// The span of the whole expression
133+
span: Span,
134+
},
135+
/// `x`
136+
Pure { local_id: HirId },
137+
}
138+
139+
/// Identical to derived impl, but ignores `span` on [`Local::WithFieldAccess`]
140+
impl PartialEq for Local {
141+
fn eq(&self, other: &Self) -> bool {
142+
match (self, other) {
143+
(
144+
Self::WithFieldAccess {
145+
local_id: self_local_id,
146+
field_indices: self_field_indices,
147+
..
148+
},
149+
Self::WithFieldAccess {
150+
local_id: other_local_id,
151+
field_indices: other_field_indices,
152+
..
153+
},
154+
) => self_local_id == other_local_id && self_field_indices == other_field_indices,
155+
(
156+
Self::Pure {
157+
local_id: self_local_id,
158+
},
159+
Self::Pure {
160+
local_id: other_local_id,
161+
},
162+
) => self_local_id == other_local_id,
163+
_ => false,
164+
}
165+
}
166+
}
167+
168+
impl Local {
169+
fn snippet(&self, cx: &LateContext<'_>) -> Cow<'static, str> {
170+
match *self {
171+
Self::WithFieldAccess { span, .. } => snippet(cx.sess(), span, "_"),
172+
Self::Pure { local_id } => cx.tcx.hir_name(local_id).to_string().into(),
173+
}
174+
}
175+
176+
fn is_potentially_local_place(&self, place: &Place<'_>) -> bool {
177+
match self {
178+
Self::WithFieldAccess {
179+
local_id,
180+
field_indices,
181+
..
182+
} => {
183+
is_potentially_local_place(*local_id, place)
184+
// If there were projections other than field projections, err on the side of caution and say that they
185+
// _might_ be mutating something.
186+
//
187+
// The reason we use `<=` and not `==` is that a mutation of `struct` or `struct.field1` should count as
188+
// mutation of the child fields such as `struct.field1.field2`
189+
&& place.projections.len() <= field_indices.len()
190+
&& iter::zip(&place.projections, field_indices.iter().copied().rev()).all(|(proj, field_idx)| {
191+
match proj.kind {
192+
ProjectionKind::Field(f_idx, _) => f_idx == field_idx,
193+
// If this is a projection we don't expect, it _might_ be mutating something
194+
_ => false,
195+
}
196+
})
197+
},
198+
Self::Pure { local_id } => is_potentially_local_place(*local_id, place),
199+
}
200+
}
201+
}
202+
117203
/// Contains information about whether a variable can be unwrapped.
118-
#[derive(Copy, Clone, Debug)]
204+
#[derive(Clone, Debug)]
119205
struct UnwrapInfo<'tcx> {
120206
/// The variable that is checked
121-
local_id: HirId,
207+
local: Local,
122208
/// The if itself
123209
if_expr: &'tcx Expr<'tcx>,
124210
/// The check, like `x.is_ok()`
@@ -156,38 +242,77 @@ fn collect_unwrap_info<'tcx>(
156242
}
157243
}
158244

159-
match expr.kind {
160-
ExprKind::Binary(op, left, right)
161-
if matches!(
162-
(invert, op.node),
163-
(false, BinOpKind::And | BinOpKind::BitAnd) | (true, BinOpKind::Or | BinOpKind::BitOr)
164-
) =>
165-
{
166-
let mut unwrap_info = collect_unwrap_info(cx, if_expr, left, branch, invert, false);
167-
unwrap_info.extend(collect_unwrap_info(cx, if_expr, right, branch, invert, false));
168-
unwrap_info
169-
},
170-
ExprKind::Unary(UnOp::Not, expr) => collect_unwrap_info(cx, if_expr, expr, branch, !invert, false),
171-
ExprKind::MethodCall(method_name, receiver, [], _)
172-
if let Some(local_id) = receiver.res_local_id()
173-
&& let ty = cx.typeck_results().expr_ty(receiver)
174-
&& let name = method_name.ident.name
175-
&& let Some((kind, unwrappable)) = option_or_result_call(cx, ty, name) =>
176-
{
177-
let safe_to_unwrap = unwrappable != invert;
245+
fn inner<'tcx>(
246+
cx: &LateContext<'tcx>,
247+
if_expr: &'tcx Expr<'_>,
248+
expr: &'tcx Expr<'_>,
249+
branch: &'tcx Expr<'_>,
250+
invert: bool,
251+
is_entire_condition: bool,
252+
out: &mut Vec<UnwrapInfo<'tcx>>,
253+
) {
254+
match expr.kind {
255+
ExprKind::Binary(op, left, right)
256+
if matches!(
257+
(invert, op.node),
258+
(false, BinOpKind::And | BinOpKind::BitAnd) | (true, BinOpKind::Or | BinOpKind::BitOr)
259+
) =>
260+
{
261+
inner(cx, if_expr, left, branch, invert, false, out);
262+
inner(cx, if_expr, right, branch, invert, false, out);
263+
},
264+
ExprKind::Unary(UnOp::Not, expr) => inner(cx, if_expr, expr, branch, !invert, false, out),
265+
ExprKind::MethodCall(method_name, receiver, [], _)
266+
if let Some(local) = extract_local(cx, receiver)
267+
&& let ty = cx.typeck_results().expr_ty(receiver)
268+
&& let name = method_name.ident.name
269+
&& let Some((kind, unwrappable)) = option_or_result_call(cx, ty, name) =>
270+
{
271+
let safe_to_unwrap = unwrappable != invert;
272+
273+
out.push(UnwrapInfo {
274+
local,
275+
if_expr,
276+
check: expr,
277+
check_name: name,
278+
branch,
279+
safe_to_unwrap,
280+
kind,
281+
is_entire_condition,
282+
});
283+
},
284+
_ => {},
285+
}
286+
}
287+
288+
let mut out = vec![];
289+
inner(cx, if_expr, expr, branch, invert, is_entire_condition, &mut out);
290+
out
291+
}
178292

179-
vec![UnwrapInfo {
293+
/// Extracts either a local used by itself ([`Local::Pure`]), or (one or more levels of) field
294+
/// access to a local ([`Local::WithFieldAccess`])
295+
fn extract_local(cx: &LateContext<'_>, mut expr: &Expr<'_>) -> Option<Local> {
296+
let span = expr.span;
297+
let mut field_indices = vec![];
298+
while let ExprKind::Field(recv, _) = expr.kind
299+
&& let Some(field_idx) = cx.typeck_results().opt_field_index(expr.hir_id)
300+
{
301+
field_indices.push(field_idx);
302+
expr = recv;
303+
}
304+
if let Some(local_id) = expr.res_local_id() {
305+
if field_indices.is_empty() {
306+
Some(Local::Pure { local_id })
307+
} else {
308+
Some(Local::WithFieldAccess {
180309
local_id,
181-
if_expr,
182-
check: expr,
183-
check_name: name,
184-
branch,
185-
safe_to_unwrap,
186-
kind,
187-
is_entire_condition,
188-
}]
189-
},
190-
_ => vec![],
310+
field_indices,
311+
span,
312+
})
313+
}
314+
} else {
315+
None
191316
}
192317
}
193318

@@ -198,9 +323,9 @@ fn collect_unwrap_info<'tcx>(
198323
/// `is_some` + `unwrap` is equivalent to `if let Some(..) = ..`, which it would not be if
199324
/// the option is changed to None between `is_some` and `unwrap`, ditto for `Result`.
200325
/// (And also `.as_mut()` is a somewhat common method that is still worth linting on.)
201-
struct MutationVisitor<'tcx> {
326+
struct MutationVisitor<'tcx, 'lcl> {
202327
is_mutated: bool,
203-
local_id: HirId,
328+
local: &'lcl Local,
204329
tcx: TyCtxt<'tcx>,
205330
}
206331

@@ -221,18 +346,18 @@ fn is_as_mut_use(tcx: TyCtxt<'_>, expr_id: HirId) -> bool {
221346
}
222347
}
223348

224-
impl<'tcx> Delegate<'tcx> for MutationVisitor<'tcx> {
349+
impl<'tcx> Delegate<'tcx> for MutationVisitor<'tcx, '_> {
225350
fn borrow(&mut self, cat: &PlaceWithHirId<'tcx>, diag_expr_id: HirId, bk: ty::BorrowKind) {
226351
if let ty::BorrowKind::Mutable = bk
227-
&& is_potentially_local_place(self.local_id, &cat.place)
352+
&& self.local.is_potentially_local_place(&cat.place)
228353
&& !is_as_mut_use(self.tcx, diag_expr_id)
229354
{
230355
self.is_mutated = true;
231356
}
232357
}
233358

234359
fn mutate(&mut self, cat: &PlaceWithHirId<'tcx>, _: HirId) {
235-
if is_potentially_local_place(self.local_id, &cat.place) {
360+
if self.local.is_potentially_local_place(&cat.place) {
236361
self.is_mutated = true;
237362
}
238363
}
@@ -256,7 +381,7 @@ impl<'tcx> UnwrappableVariablesVisitor<'_, 'tcx> {
256381
for unwrap_info in collect_unwrap_info(self.cx, if_expr, cond, branch, else_branch, true) {
257382
let mut delegate = MutationVisitor {
258383
is_mutated: false,
259-
local_id: unwrap_info.local_id,
384+
local: &unwrap_info.local,
260385
tcx: self.cx.tcx,
261386
};
262387

@@ -318,41 +443,39 @@ impl<'tcx> Visitor<'tcx> for UnwrappableVariablesVisitor<'_, 'tcx> {
318443
// find `unwrap[_err]()` or `expect("...")` calls:
319444
if let ExprKind::MethodCall(method_name, self_arg, ..) = expr.kind
320445
&& let (self_arg, as_ref_kind) = consume_option_as_ref(self_arg)
321-
&& let Some(id) = self_arg.res_local_id()
446+
&& let Some(local) = extract_local(self.cx, self_arg)
322447
&& matches!(method_name.ident.name, sym::unwrap | sym::expect | sym::unwrap_err)
323448
&& let call_to_unwrap = matches!(method_name.ident.name, sym::unwrap | sym::expect)
324-
&& let Some(unwrappable) = self.unwrappables.iter()
325-
.find(|u| u.local_id == id)
449+
&& let Some(unwrappable) = self.unwrappables.iter().find(|u| u.local == local)
326450
// Span contexts should not differ with the conditional branch
327451
&& let span_ctxt = expr.span.ctxt()
328452
&& unwrappable.branch.span.ctxt() == span_ctxt
329453
&& unwrappable.check.span.ctxt() == span_ctxt
330454
{
331455
if call_to_unwrap == unwrappable.safe_to_unwrap {
332-
let is_entire_condition = unwrappable.is_entire_condition;
333-
let unwrappable_variable_name = self.cx.tcx.hir_name(unwrappable.local_id);
334-
let suggested_pattern = if call_to_unwrap {
335-
unwrappable.kind.success_variant_pattern()
336-
} else {
337-
unwrappable.kind.error_variant_pattern()
338-
};
456+
let unwrappable_variable_str = unwrappable.local.snippet(self.cx);
339457

340458
span_lint_hir_and_then(
341459
self.cx,
342460
UNNECESSARY_UNWRAP,
343461
expr.hir_id,
344462
expr.span,
345463
format!(
346-
"called `{}` on `{unwrappable_variable_name}` after checking its variant with `{}`",
464+
"called `{}` on `{unwrappable_variable_str}` after checking its variant with `{}`",
347465
method_name.ident.name, unwrappable.check_name,
348466
),
349467
|diag| {
350-
if is_entire_condition {
468+
if unwrappable.is_entire_condition {
351469
diag.span_suggestion(
352470
unwrappable.check.span.with_lo(unwrappable.if_expr.span.lo()),
353471
"try",
354472
format!(
355-
"if let {suggested_pattern} = {borrow_prefix}{unwrappable_variable_name}",
473+
"if let {suggested_pattern} = {borrow_prefix}{unwrappable_variable_str}",
474+
suggested_pattern = if call_to_unwrap {
475+
unwrappable.kind.success_variant_pattern()
476+
} else {
477+
unwrappable.kind.error_variant_pattern()
478+
},
356479
borrow_prefix = match as_ref_kind {
357480
Some(AsRefKind::AsRef) => "&",
358481
Some(AsRefKind::AsMut) => "&mut ",

0 commit comments

Comments
 (0)