Skip to content

Commit b39a734

Browse files
committed
feat({unnecessary,panicking}_unwrap): lint field accesses
1 parent ea77fcc commit b39a734

File tree

3 files changed

+310
-55
lines changed

3 files changed

+310
-55
lines changed

clippy_lints/src/unwrap.rs

Lines changed: 116 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1+
use std::borrow::Cow;
2+
13
use clippy_config::Conf;
24
use clippy_utils::diagnostics::span_lint_hir_and_then;
35
use clippy_utils::msrvs::Msrv;
46
use clippy_utils::res::{MaybeDef, MaybeResPath};
7+
use clippy_utils::source::snippet;
58
use clippy_utils::usage::is_potentially_local_place;
69
use clippy_utils::{can_use_if_let_chains, higher, sym};
10+
use rustc_abi::FieldIdx;
711
use rustc_errors::Applicability;
812
use rustc_hir::intravisit::{FnKind, Visitor, walk_expr, walk_fn};
913
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};
14+
use rustc_hir_typeck::expr_use_visitor::{Delegate, ExprUseVisitor, Place, PlaceWithHirId};
15+
use rustc_lint::{LateContext, LateLintPass, LintContext};
1216
use rustc_middle::hir::nested_filter;
17+
use rustc_middle::hir::place::ProjectionKind;
1318
use rustc_middle::mir::FakeReadCause;
1419
use rustc_middle::ty::{self, Ty, TyCtxt};
1520
use rustc_session::impl_lint_pass;
@@ -114,11 +119,89 @@ impl UnwrappableKind {
114119
}
115120
}
116121

122+
#[derive(Copy, Clone, Debug, Eq)]
123+
enum Local {
124+
/// `x.opt`
125+
WithFieldAccess {
126+
local_id: HirId,
127+
field_idx: FieldIdx,
128+
/// The span of the whole expression
129+
span: Span,
130+
},
131+
/// `x`
132+
Pure { local_id: HirId },
133+
}
134+
135+
/// Identical to derived impl, but ignores `span` on [`Local::WithFieldAccess`]
136+
impl PartialEq for Local {
137+
fn eq(&self, other: &Self) -> bool {
138+
match (self, other) {
139+
(
140+
Self::WithFieldAccess {
141+
local_id: self_local_id,
142+
field_idx: self_field_idx,
143+
..
144+
},
145+
Self::WithFieldAccess {
146+
local_id: other_local_id,
147+
field_idx: other_field_idx,
148+
..
149+
},
150+
) => self_local_id == other_local_id && self_field_idx == other_field_idx,
151+
(
152+
Self::Pure {
153+
local_id: self_local_id,
154+
},
155+
Self::Pure {
156+
local_id: other_local_id,
157+
},
158+
) => self_local_id == other_local_id,
159+
_ => false,
160+
}
161+
}
162+
}
163+
164+
impl Local {
165+
fn snippet(self, cx: &LateContext<'_>) -> Cow<'static, str> {
166+
match self {
167+
Self::WithFieldAccess { span, .. } => snippet(cx.sess(), span, "_"),
168+
Self::Pure { local_id } => cx.tcx.hir_name(local_id).to_string().into(),
169+
}
170+
}
171+
172+
fn is_potentially_local_place(self, place: &Place<'_>) -> bool {
173+
match self {
174+
Self::WithFieldAccess {
175+
local_id, field_idx, ..
176+
} => {
177+
if is_potentially_local_place(local_id, place)
178+
// If there were projections other than the field projection, err on the side of caution and say
179+
// that they _might_ have mutated the field
180+
//
181+
// The reason we use `<=` and not `==` is that, if there were no projections, then the whole local
182+
// was mutated, which means that our field was mutated as well
183+
&& place.projections.len() <= 1
184+
&& place.projections.last().is_none_or(|proj| match proj.kind {
185+
ProjectionKind::Field(f_idx, _) => f_idx == field_idx,
186+
// If this is a projection we don't expect, it _might_ be mutating something
187+
_ => false,
188+
})
189+
{
190+
true
191+
} else {
192+
false
193+
}
194+
},
195+
Self::Pure { local_id } => is_potentially_local_place(local_id, place),
196+
}
197+
}
198+
}
199+
117200
/// Contains information about whether a variable can be unwrapped.
118201
#[derive(Copy, Clone, Debug)]
119202
struct UnwrapInfo<'tcx> {
120203
/// The variable that is checked
121-
local_id: HirId,
204+
local: Local,
122205
/// The if itself
123206
if_expr: &'tcx Expr<'tcx>,
124207
/// The check, like `x.is_ok()`
@@ -177,15 +260,15 @@ fn collect_unwrap_info<'tcx>(
177260
},
178261
ExprKind::Unary(UnOp::Not, expr) => inner(cx, if_expr, expr, branch, !invert, false, out),
179262
ExprKind::MethodCall(method_name, receiver, [], _)
180-
if let Some(local_id) = receiver.res_local_id()
263+
if let Some(local) = extract_local(cx, receiver)
181264
&& let ty = cx.typeck_results().expr_ty(receiver)
182265
&& let name = method_name.ident.name
183266
&& let Some((kind, unwrappable)) = option_or_result_call(cx, ty, name) =>
184267
{
185268
let safe_to_unwrap = unwrappable != invert;
186269

187270
out.push(UnwrapInfo {
188-
local_id,
271+
local,
189272
if_expr,
190273
check: expr,
191274
check_name: name,
@@ -204,6 +287,25 @@ fn collect_unwrap_info<'tcx>(
204287
out
205288
}
206289

290+
/// Extracts either a local used by itself ([`Local::Pure`]), or a field access to a local
291+
/// ([`Local::WithFieldAccess`])
292+
fn extract_local(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<Local> {
293+
if let Some(local_id) = expr.res_local_id() {
294+
Some(Local::Pure { local_id })
295+
} else if let ExprKind::Field(recv, _) = expr.kind
296+
&& let Some(local_id) = recv.res_local_id()
297+
&& let Some(field_idx) = cx.typeck_results().opt_field_index(expr.hir_id)
298+
{
299+
Some(Local::WithFieldAccess {
300+
local_id,
301+
field_idx,
302+
span: expr.span,
303+
})
304+
} else {
305+
None
306+
}
307+
}
308+
207309
/// A HIR visitor delegate that checks if a local variable of type `Option` or `Result` is mutated,
208310
/// *except* for if `.as_mut()` is called.
209311
/// The reason for why we allow that one specifically is that `.as_mut()` cannot change
@@ -213,7 +315,7 @@ fn collect_unwrap_info<'tcx>(
213315
/// (And also `.as_mut()` is a somewhat common method that is still worth linting on.)
214316
struct MutationVisitor<'tcx> {
215317
is_mutated: bool,
216-
local_id: HirId,
318+
local: Local,
217319
tcx: TyCtxt<'tcx>,
218320
}
219321

@@ -237,15 +339,15 @@ fn is_as_mut_use(tcx: TyCtxt<'_>, expr_id: HirId) -> bool {
237339
impl<'tcx> Delegate<'tcx> for MutationVisitor<'tcx> {
238340
fn borrow(&mut self, cat: &PlaceWithHirId<'tcx>, diag_expr_id: HirId, bk: ty::BorrowKind) {
239341
if let ty::BorrowKind::Mutable = bk
240-
&& is_potentially_local_place(self.local_id, &cat.place)
342+
&& self.local.is_potentially_local_place(&cat.place)
241343
&& !is_as_mut_use(self.tcx, diag_expr_id)
242344
{
243345
self.is_mutated = true;
244346
}
245347
}
246348

247349
fn mutate(&mut self, cat: &PlaceWithHirId<'tcx>, _: HirId) {
248-
if is_potentially_local_place(self.local_id, &cat.place) {
350+
if self.local.is_potentially_local_place(&cat.place) {
249351
self.is_mutated = true;
250352
}
251353
}
@@ -269,7 +371,7 @@ impl<'tcx> UnwrappableVariablesVisitor<'_, 'tcx> {
269371
for unwrap_info in collect_unwrap_info(self.cx, if_expr, cond, branch, else_branch, true) {
270372
let mut delegate = MutationVisitor {
271373
is_mutated: false,
272-
local_id: unwrap_info.local_id,
374+
local: unwrap_info.local,
273375
tcx: self.cx.tcx,
274376
};
275377

@@ -331,25 +433,25 @@ impl<'tcx> Visitor<'tcx> for UnwrappableVariablesVisitor<'_, 'tcx> {
331433
// find `unwrap[_err]()` or `expect("...")` calls:
332434
if let ExprKind::MethodCall(method_name, self_arg, ..) = expr.kind
333435
&& let (self_arg, as_ref_kind) = consume_option_as_ref(self_arg)
334-
&& let Some(id) = self_arg.res_local_id()
436+
&& let Some(local) = extract_local(self.cx, self_arg)
335437
&& matches!(method_name.ident.name, sym::unwrap | sym::expect | sym::unwrap_err)
336438
&& let call_to_unwrap = matches!(method_name.ident.name, sym::unwrap | sym::expect)
337-
&& let Some(unwrappable) = self.unwrappables.iter().find(|u| u.local_id == id)
439+
&& let Some(unwrappable) = self.unwrappables.iter().find(|u| u.local == local)
338440
// Span contexts should not differ with the conditional branch
339441
&& let span_ctxt = expr.span.ctxt()
340442
&& unwrappable.branch.span.ctxt() == span_ctxt
341443
&& unwrappable.check.span.ctxt() == span_ctxt
342444
{
343445
if call_to_unwrap == unwrappable.safe_to_unwrap {
344-
let unwrappable_variable_name = self.cx.tcx.hir_name(unwrappable.local_id);
446+
let unwrappable_variable_str = unwrappable.local.snippet(self.cx);
345447

346448
span_lint_hir_and_then(
347449
self.cx,
348450
UNNECESSARY_UNWRAP,
349451
expr.hir_id,
350452
expr.span,
351453
format!(
352-
"called `{}` on `{unwrappable_variable_name}` after checking its variant with `{}`",
454+
"called `{}` on `{unwrappable_variable_str}` after checking its variant with `{}`",
353455
method_name.ident.name, unwrappable.check_name,
354456
),
355457
|diag| {
@@ -358,7 +460,7 @@ impl<'tcx> Visitor<'tcx> for UnwrappableVariablesVisitor<'_, 'tcx> {
358460
unwrappable.check.span.with_lo(unwrappable.if_expr.span.lo()),
359461
"try",
360462
format!(
361-
"if let {suggested_pattern} = {borrow_prefix}{unwrappable_variable_name}",
463+
"if let {suggested_pattern} = {borrow_prefix}{unwrappable_variable_str}",
362464
suggested_pattern = if call_to_unwrap {
363465
unwrappable.kind.success_variant_pattern()
364466
} else {

tests/ui/checked_unwrap/simple_conditionals.rs

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
#![expect(
44
clippy::if_same_then_else,
55
clippy::branches_sharing_code,
6-
clippy::unnecessary_literal_unwrap
6+
clippy::unnecessary_literal_unwrap,
7+
clippy::self_assignment
78
)]
89

910
macro_rules! m {
@@ -287,3 +288,85 @@ fn check_expect() {
287288
x.expect("an error message");
288289
}
289290
}
291+
292+
fn issue15321() {
293+
struct Soption {
294+
option: Option<bool>,
295+
other: bool,
296+
}
297+
let mut sopt = Soption {
298+
option: Some(true),
299+
other: true,
300+
};
301+
// Lint: nothing was mutated
302+
let _res = if sopt.option.is_some() {
303+
sopt.option.unwrap()
304+
//~^ unnecessary_unwrap
305+
} else {
306+
sopt.option.unwrap()
307+
//~^ panicking_unwrap
308+
};
309+
// Lint: an unrelated field was mutated
310+
let _res = if sopt.option.is_some() {
311+
sopt.other = false;
312+
sopt.option.unwrap()
313+
//~^ unnecessary_unwrap
314+
} else {
315+
sopt.other = false;
316+
sopt.option.unwrap()
317+
//~^ panicking_unwrap
318+
};
319+
// No lint: the whole local was mutated
320+
let _res = if sopt.option.is_some() {
321+
sopt = sopt;
322+
sopt.option.unwrap()
323+
} else {
324+
sopt.option = None;
325+
sopt.option.unwrap()
326+
};
327+
// No lint: the field we're looking at was mutated
328+
let _res = if sopt.option.is_some() {
329+
sopt = sopt;
330+
sopt.option.unwrap()
331+
} else {
332+
sopt.option = None;
333+
sopt.option.unwrap()
334+
};
335+
336+
struct Toption(Option<bool>, bool);
337+
let mut topt = Toption(Some(true), true);
338+
// Lint: nothing was mutated
339+
let _res = if topt.0.is_some() {
340+
topt.0.unwrap()
341+
//~^ unnecessary_unwrap
342+
} else {
343+
topt.0.unwrap()
344+
//~^ panicking_unwrap
345+
};
346+
// Lint: an unrelated field was mutated
347+
let _res = if topt.0.is_some() {
348+
topt.1 = false;
349+
topt.0.unwrap()
350+
//~^ unnecessary_unwrap
351+
} else {
352+
topt.1 = false;
353+
topt.0.unwrap()
354+
//~^ panicking_unwrap
355+
};
356+
// No lint: the whole local was mutated
357+
let _res = if topt.0.is_some() {
358+
topt = topt;
359+
topt.0.unwrap()
360+
} else {
361+
topt = topt;
362+
topt.0.unwrap()
363+
};
364+
// No lint: the field we're looking at was mutated
365+
let _res = if topt.0.is_some() {
366+
topt.0 = None;
367+
topt.0.unwrap()
368+
} else {
369+
topt.0 = None;
370+
topt.0.unwrap()
371+
};
372+
}

0 commit comments

Comments
 (0)