Skip to content

Commit 23ee56d

Browse files
committed
lint nested field accesses as well
1 parent b39a734 commit 23ee56d

File tree

3 files changed

+174
-44
lines changed

3 files changed

+174
-44
lines changed

clippy_lints/src/unwrap.rs

Lines changed: 53 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::borrow::Cow;
2+
use std::iter;
23

34
use clippy_config::Conf;
45
use clippy_utils::diagnostics::span_lint_hir_and_then;
@@ -119,12 +120,15 @@ impl UnwrappableKind {
119120
}
120121
}
121122

122-
#[derive(Copy, Clone, Debug, Eq)]
123+
#[derive(Clone, Debug, Eq)]
123124
enum Local {
124-
/// `x.opt`
125+
/// `x.field1.field2.field3`
125126
WithFieldAccess {
126127
local_id: HirId,
127-
field_idx: FieldIdx,
128+
/// The indices of the field accessed.
129+
///
130+
/// Stored last-to-first, i.e. for the example above: `[field3, field2, field1]`
131+
field_indices: Vec<FieldIdx>,
128132
/// The span of the whole expression
129133
span: Span,
130134
},
@@ -139,15 +143,15 @@ impl PartialEq for Local {
139143
(
140144
Self::WithFieldAccess {
141145
local_id: self_local_id,
142-
field_idx: self_field_idx,
146+
field_indices: self_field_indices,
143147
..
144148
},
145149
Self::WithFieldAccess {
146150
local_id: other_local_id,
147-
field_idx: other_field_idx,
151+
field_indices: other_field_indices,
148152
..
149153
},
150-
) => self_local_id == other_local_id && self_field_idx == other_field_idx,
154+
) => self_local_id == other_local_id && self_field_indices == other_field_indices,
151155
(
152156
Self::Pure {
153157
local_id: self_local_id,
@@ -162,43 +166,42 @@ impl PartialEq for Local {
162166
}
163167

164168
impl Local {
165-
fn snippet(self, cx: &LateContext<'_>) -> Cow<'static, str> {
166-
match self {
169+
fn snippet(&self, cx: &LateContext<'_>) -> Cow<'static, str> {
170+
match *self {
167171
Self::WithFieldAccess { span, .. } => snippet(cx.sess(), span, "_"),
168172
Self::Pure { local_id } => cx.tcx.hir_name(local_id).to_string().into(),
169173
}
170174
}
171175

172-
fn is_potentially_local_place(self, place: &Place<'_>) -> bool {
176+
fn is_potentially_local_place(&self, place: &Place<'_>) -> bool {
173177
match self {
174178
Self::WithFieldAccess {
175-
local_id, field_idx, ..
179+
local_id,
180+
field_indices,
181+
..
176182
} => {
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
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.
180186
//
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,
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+
}
188196
})
189-
{
190-
true
191-
} else {
192-
false
193-
}
194197
},
195-
Self::Pure { local_id } => is_potentially_local_place(local_id, place),
198+
Self::Pure { local_id } => is_potentially_local_place(*local_id, place),
196199
}
197200
}
198201
}
199202

200203
/// Contains information about whether a variable can be unwrapped.
201-
#[derive(Copy, Clone, Debug)]
204+
#[derive(Clone, Debug)]
202205
struct UnwrapInfo<'tcx> {
203206
/// The variable that is checked
204207
local: Local,
@@ -287,20 +290,27 @@ fn collect_unwrap_info<'tcx>(
287290
out
288291
}
289292

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()
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
297299
&& let Some(field_idx) = cx.typeck_results().opt_field_index(expr.hir_id)
298300
{
299-
Some(Local::WithFieldAccess {
300-
local_id,
301-
field_idx,
302-
span: expr.span,
303-
})
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 {
309+
local_id,
310+
field_indices,
311+
span,
312+
})
313+
}
304314
} else {
305315
None
306316
}
@@ -313,9 +323,9 @@ fn extract_local(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<Local> {
313323
/// `is_some` + `unwrap` is equivalent to `if let Some(..) = ..`, which it would not be if
314324
/// the option is changed to None between `is_some` and `unwrap`, ditto for `Result`.
315325
/// (And also `.as_mut()` is a somewhat common method that is still worth linting on.)
316-
struct MutationVisitor<'tcx> {
326+
struct MutationVisitor<'tcx, 'lcl> {
317327
is_mutated: bool,
318-
local: Local,
328+
local: &'lcl Local,
319329
tcx: TyCtxt<'tcx>,
320330
}
321331

@@ -336,7 +346,7 @@ fn is_as_mut_use(tcx: TyCtxt<'_>, expr_id: HirId) -> bool {
336346
}
337347
}
338348

339-
impl<'tcx> Delegate<'tcx> for MutationVisitor<'tcx> {
349+
impl<'tcx> Delegate<'tcx> for MutationVisitor<'tcx, '_> {
340350
fn borrow(&mut self, cat: &PlaceWithHirId<'tcx>, diag_expr_id: HirId, bk: ty::BorrowKind) {
341351
if let ty::BorrowKind::Mutable = bk
342352
&& self.local.is_potentially_local_place(&cat.place)
@@ -371,7 +381,7 @@ impl<'tcx> UnwrappableVariablesVisitor<'_, 'tcx> {
371381
for unwrap_info in collect_unwrap_info(self.cx, if_expr, cond, branch, else_branch, true) {
372382
let mut delegate = MutationVisitor {
373383
is_mutated: false,
374-
local: unwrap_info.local,
384+
local: &unwrap_info.local,
375385
tcx: self.cx.tcx,
376386
};
377387

tests/ui/checked_unwrap/simple_conditionals.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,4 +369,71 @@ fn issue15321() {
369369
topt.0 = None;
370370
topt.0.unwrap()
371371
};
372+
373+
// Nested field accesses get linted as well
374+
struct Soption2 {
375+
other: bool,
376+
option: Soption,
377+
}
378+
let mut sopt2 = Soption2 {
379+
other: true,
380+
option: Soption {
381+
option: Some(true),
382+
other: true,
383+
},
384+
};
385+
// Lint: no fields were mutated
386+
let _res = if sopt2.option.option.is_some() {
387+
sopt2.option.option.unwrap()
388+
//~^ unnecessary_unwrap
389+
} else {
390+
sopt2.option.option.unwrap()
391+
//~^ panicking_unwrap
392+
};
393+
// Lint: an unrelated outer field was mutated -- don't get confused by `Soption2.other` having the
394+
// same `FieldIdx` of 1 as `Soption.option`
395+
let _res = if sopt2.option.option.is_some() {
396+
sopt2.other = false;
397+
sopt2.option.option.unwrap()
398+
//~^ unnecessary_unwrap
399+
} else {
400+
sopt2.other = false;
401+
sopt2.option.option.unwrap()
402+
//~^ panicking_unwrap
403+
};
404+
// Lint: an unrelated inner field was mutated
405+
let _res = if sopt2.option.option.is_some() {
406+
sopt2.option.other = false;
407+
sopt2.option.option.unwrap()
408+
//~^ unnecessary_unwrap
409+
} else {
410+
sopt2.option.other = false;
411+
sopt2.option.option.unwrap()
412+
//~^ panicking_unwrap
413+
};
414+
// Don't lint: the whole local was mutated
415+
let _res = if sopt2.option.option.is_some() {
416+
sopt2 = sopt2;
417+
sopt2.option.option.unwrap()
418+
} else {
419+
sopt2 = sopt2;
420+
sopt2.option.option.unwrap()
421+
};
422+
// Don't lint: a parent field of the field we're looking at was mutated, and with that the
423+
// field we're looking at
424+
let _res = if sopt2.option.option.is_some() {
425+
sopt2.option = sopt;
426+
sopt2.option.option.unwrap()
427+
} else {
428+
sopt2.option = sopt;
429+
sopt2.option.option.unwrap()
430+
};
431+
// Don't lint: the field we're looking at was mutated directly
432+
let _res = if sopt2.option.option.is_some() {
433+
sopt2.option.option = None;
434+
sopt2.option.option.unwrap()
435+
} else {
436+
sopt2.option.option = None;
437+
sopt2.option.option.unwrap()
438+
};
372439
}

tests/ui/checked_unwrap/simple_conditionals.stderr

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,5 +415,58 @@ LL | let _res = if topt.0.is_some() {
415415
LL | topt.0.unwrap()
416416
| ^^^^^^^^^^^^^^^
417417

418-
error: aborting due to 47 previous errors
418+
error: called `unwrap` on `sopt2.option.option` after checking its variant with `is_some`
419+
--> tests/ui/checked_unwrap/simple_conditionals.rs:387:9
420+
|
421+
LL | let _res = if sopt2.option.option.is_some() {
422+
| -------------------------------- help: try: `if let Some(<item>) = sopt2.option.option`
423+
LL | sopt2.option.option.unwrap()
424+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
425+
426+
error: this call to `unwrap()` will always panic
427+
--> tests/ui/checked_unwrap/simple_conditionals.rs:390:9
428+
|
429+
LL | let _res = if sopt2.option.option.is_some() {
430+
| ----------------------------- because of this check
431+
...
432+
LL | sopt2.option.option.unwrap()
433+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
434+
435+
error: called `unwrap` on `sopt2.option.option` after checking its variant with `is_some`
436+
--> tests/ui/checked_unwrap/simple_conditionals.rs:397:9
437+
|
438+
LL | let _res = if sopt2.option.option.is_some() {
439+
| -------------------------------- help: try: `if let Some(<item>) = sopt2.option.option`
440+
LL | sopt2.other = false;
441+
LL | sopt2.option.option.unwrap()
442+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
443+
444+
error: this call to `unwrap()` will always panic
445+
--> tests/ui/checked_unwrap/simple_conditionals.rs:401:9
446+
|
447+
LL | let _res = if sopt2.option.option.is_some() {
448+
| ----------------------------- because of this check
449+
...
450+
LL | sopt2.option.option.unwrap()
451+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
452+
453+
error: called `unwrap` on `sopt2.option.option` after checking its variant with `is_some`
454+
--> tests/ui/checked_unwrap/simple_conditionals.rs:407:9
455+
|
456+
LL | let _res = if sopt2.option.option.is_some() {
457+
| -------------------------------- help: try: `if let Some(<item>) = sopt2.option.option`
458+
LL | sopt2.option.other = false;
459+
LL | sopt2.option.option.unwrap()
460+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
461+
462+
error: this call to `unwrap()` will always panic
463+
--> tests/ui/checked_unwrap/simple_conditionals.rs:411:9
464+
|
465+
LL | let _res = if sopt2.option.option.is_some() {
466+
| ----------------------------- because of this check
467+
...
468+
LL | sopt2.option.option.unwrap()
469+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
470+
471+
error: aborting due to 53 previous errors
419472

0 commit comments

Comments
 (0)