Skip to content

Commit f15e601

Browse files
committed
refactor, and make it work for more cases
1 parent 635bb32 commit f15e601

6 files changed

+164
-212
lines changed

clippy_lints/src/local_assigned_single_value.rs

+125-154
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ use clippy_utils::fn_has_unsatisfiable_preds;
33
use clippy_utils::source::snippet_opt;
44
use itertools::Itertools;
55
use rustc_const_eval::interpret::Scalar;
6+
use rustc_data_structures::fx::FxHashSet;
67
use rustc_hir::def_id::LocalDefId;
78
use rustc_hir::{intravisit::FnKind, Body, FnDecl};
89
use rustc_index::IndexVec;
910
use rustc_lint::{LateContext, LateLintPass};
1011
use rustc_middle::mir::{
1112
self, interpret::ConstValue, visit::Visitor, Constant, Location, Mutability, Operand, Place, Rvalue,
1213
};
13-
use rustc_middle::mir::{AggregateKind, PlaceElem, Terminator, TerminatorKind};
14+
use rustc_middle::mir::{AggregateKind, CastKind, PlaceElem, Terminator, TerminatorKind};
1415
use rustc_session::{declare_lint_pass, declare_tool_lint};
1516
use rustc_span::source_map::Spanned;
1617
use rustc_span::Span;
@@ -61,78 +62,35 @@ impl LateLintPass<'_> for LocalAssignedSingleValue {
6162
};
6263
v.visit_body(mir);
6364

64-
// for (local, i) in v.map.iter_enumerated() {
65-
// dbg!(local, i);
66-
// }
67-
6865
for (local, i) in v.map.iter_enumerated() {
69-
if !i.assigned_non_const_rvalue
70-
&& !i.mut_ref_acquired
71-
&& nested_locals_are_not_from_bad_instr(&v.map, i)
72-
&& assignments_all_same_value(&v.map, i)
73-
{
74-
let LocalUsageValues {
75-
usage,
76-
mut_ref_acquired: _,
77-
assigned_non_const_rvalue: _,
78-
is_from_bad_instr: _,
79-
} = i;
80-
81-
if let Some(local_decl) = mir.local_decls.get(local)
82-
&& let [dbg_info] = &*mir
83-
.var_debug_info
84-
.iter()
85-
.filter(|info| info.source_info.span == local_decl.source_info.span)
86-
.collect_vec()
87-
// Don't handle function arguments.
88-
&& dbg_info.argument_index.is_none()
89-
// Ignore anything from a procedural macro, or locals we cannot prove aren't
90-
// temporaries
91-
&& let Some(snippet) = snippet_opt(cx, dbg_info.source_info.span)
92-
&& snippet.ends_with(dbg_info.name.as_str())
93-
{
94-
span_lint(
95-
cx,
96-
LOCAL_ASSIGNED_SINGLE_VALUE,
97-
usage.iter().map(|i| i.span).collect_vec(),
98-
"local only ever assigned single value",
99-
);
100-
}
101-
}
102-
}
103-
104-
/*
105-
for (local, usage) in &v.local_usage {
106-
if should_lint(&v.local_usage, *local, usage) {
107-
let LocalUsageValues {
108-
usage,
109-
mut_ref_acquired: _,
110-
assigned_non_const: _,
111-
} = usage;
66+
let LocalUsageValues {
67+
usage,
68+
mut_ref_acquired,
69+
} = i;
11270

113-
if let Some(local_decl) = mir.local_decls.get(*local)
71+
if !mut_ref_acquired && eval_nested_locals_are_constant(&v.map, i)
72+
&& eval_local(&v.map, i)
73+
&& let Some(local_decl) = mir.local_decls.get(local)
11474
&& let [dbg_info] = &*mir
11575
.var_debug_info
11676
.iter()
11777
.filter(|info| info.source_info.span == local_decl.source_info.span)
11878
.collect_vec()
119-
// Don't handle function arguments.
120-
&& dbg_info.argument_index.is_none()
121-
// Ignore anything from a procedural macro, or locals we cannot prove aren't
122-
// temporaries
123-
&& let Some(snippet) = snippet_opt(cx, dbg_info.source_info.span)
124-
&& snippet.ends_with(dbg_info.name.as_str())
125-
{
126-
span_lint(
127-
cx,
128-
LOCAL_ASSIGNED_SINGLE_VALUE,
129-
usage.iter().map(|(span, _)| *span).collect_vec(),
130-
"local only ever assigned single value",
131-
);
132-
}
79+
// Don't handle function arguments.
80+
&& dbg_info.argument_index.is_none()
81+
// Ignore anything from a procedural macro, or locals we cannot prove aren't
82+
// temporaries
83+
&& let Some(snippet) = snippet_opt(cx, dbg_info.source_info.span)
84+
&& snippet.ends_with(dbg_info.name.as_str())
85+
{
86+
span_lint(
87+
cx,
88+
LOCAL_ASSIGNED_SINGLE_VALUE,
89+
usage.iter().map(|i| i.span).collect_vec(),
90+
"local only ever assigned single value",
91+
);
13392
}
13493
}
135-
*/
13694
}
13795
}
13896

@@ -145,13 +103,6 @@ struct LocalUsageValues<'tcx> {
145103
usage: Vec<Spanned<LocalUsage<'tcx>>>,
146104
/// Whether it's mutably borrowed, ever. We should not lint this.
147105
mut_ref_acquired: bool,
148-
/// Whether it's assigned a value we cannot prove is constant, ever. We should not lint this.
149-
assigned_non_const_rvalue: bool,
150-
/// Whether it's assigned a value that we know cannot be constant. This is differentiated from
151-
/// `assigned_non_const` since we check this for nested locals.
152-
///
153-
/// This is set to `true` for function calls or binary operations.
154-
is_from_bad_instr: bool,
155106
}
156107

157108
#[derive(Debug)]
@@ -165,6 +116,10 @@ enum LocalUsage<'tcx> {
165116
/// When a `Local` depends on the value of another local by accessing any of its fields or
166117
/// indexing
167118
DependsOnWithProj(mir::Local, &'tcx PlaceElem<'tcx>),
119+
/// Used when a local's assigned a value we cannot prove is constant.
120+
NonConst,
121+
/// This is always overwritten.
122+
Pending,
168123
}
169124

170125
struct V<'a, 'tcx> {
@@ -193,30 +148,28 @@ impl<'a, 'tcx> Visitor<'tcx> for V<'a, 'tcx> {
193148
}
194149

195150
let usage = &mut map[place.local];
151+
let mut spanned = Spanned {
152+
node: LocalUsage::Pending,
153+
span: stmt.source_info.span,
154+
};
196155

197-
if let Rvalue::Use(Operand::Constant(constant)) = rvalue
198-
&& let Constant { literal, .. } = **constant
199-
&& let Some(ConstValue::Scalar(scalar)) = literal.try_to_value(cx.tcx)
200-
{
201-
usage.usage.push(Spanned { node: LocalUsage::Scalar(scalar), span: stmt.source_info.span });
202-
} else if let Rvalue::Use(operand) = rvalue
203-
&& let Some(place) = operand.place()
204-
{
205-
if let [base_proj, ..] = place.projection.as_slice()
206-
// Handle `let [x, y] = [1, 1]` and `let (x, y) = (1, 1)`
207-
&& matches!(base_proj, PlaceElem::Field(..) | PlaceElem::Index(..))
156+
if let Rvalue::Use(operand) = rvalue {
157+
if let Operand::Constant(constant) = operand
158+
&& let Constant { literal, .. } = **constant
159+
&& let Some(ConstValue::Scalar(scalar)) = literal.try_to_value(cx.tcx)
208160
{
209-
usage.usage.push(Spanned {
210-
node: LocalUsage::DependsOnWithProj(place.local, base_proj),
211-
span: stmt.source_info.span,
212-
});
213-
} else {
214-
// It seems sometimes a local's just moved, no projections, so let's make sure we
215-
// don't set `assigned_non_const` to true for these cases
216-
usage.usage.push(Spanned {
217-
node: LocalUsage::DependsOn(place.local),
218-
span: stmt.source_info.span
219-
});
161+
spanned.node = LocalUsage::Scalar(scalar);
162+
} else if let Some(place) = operand.place() {
163+
if let [base_proj, ..] = place.projection.as_slice()
164+
// Handle `let [x, y] = [1, 1]` and `let (x, y) = (1, 1)`
165+
&& matches!(base_proj, PlaceElem::Field(..) | PlaceElem::Index(..))
166+
{
167+
spanned.node = LocalUsage::DependsOnWithProj(place.local, base_proj);
168+
} else {
169+
// It seems sometimes a local's just moved, no projections, so let's make sure we
170+
// don't set `assigned_non_const` to true for these cases
171+
spanned.node = LocalUsage::DependsOn(place.local);
172+
}
220173
}
221174
}
222175
// Handle creation of tuples/arrays, otherwise the above would be useless
@@ -226,25 +179,45 @@ impl<'a, 'tcx> Visitor<'tcx> for V<'a, 'tcx> {
226179
// TODO: Let's remove this `cloned` call, if possible.
227180
&& let Some(scalars) = extract_scalars(cx, fields.into_iter().cloned())
228181
{
229-
usage.usage.push(Spanned {
230-
node: LocalUsage::Scalars(scalars),
231-
span: stmt.source_info.span,
232-
})
233-
} else if let Rvalue::BinaryOp(..) | Rvalue::CheckedBinaryOp(..) = rvalue {
234-
usage.is_from_bad_instr = true;
182+
spanned.node = LocalUsage::Scalars(scalars);
183+
} else if let Rvalue::Cast(kind, operand, _) = rvalue {
184+
if let Operand::Constant(constant) = operand
185+
&& matches!(
186+
kind,
187+
CastKind::IntToInt | CastKind::FloatToInt | CastKind::FloatToFloat | CastKind::IntToFloat,
188+
)
189+
&& let Constant { literal, .. } = **constant
190+
&& let Some(ConstValue::Scalar(scalar)) = literal.try_to_value(cx.tcx)
191+
{
192+
spanned.node = LocalUsage::Scalar(scalar);
193+
} else if let Operand::Move(place) = operand {
194+
if let [base_proj, ..] = place.projection.as_slice()
195+
&& matches!(base_proj, PlaceElem::Field(..) | PlaceElem::Index(..))
196+
{
197+
// Probably unnecessary
198+
spanned.node = LocalUsage::DependsOnWithProj(place.local, base_proj);
199+
} else {
200+
// Handle casts from enum discriminants
201+
spanned.node = LocalUsage::DependsOn(place.local);
202+
}
203+
}
235204
} else {
236-
// We can also probably handle stuff like `x += 1` here, maybe. But this would be
237-
// very very complex. Let's keep it simple enough.
238-
usage.assigned_non_const_rvalue = true;
205+
spanned.node = LocalUsage::NonConst;
206+
}
207+
208+
if !matches!(spanned.node, LocalUsage::Pending) {
209+
usage.usage.push(spanned);
239210
}
240211
}
241212

242213
fn visit_terminator(&mut self, terminator: &Terminator<'_>, _: Location) {
243214
let Self { body: _, cx: _, map } = self;
244215

245-
// TODO: Maybe we can allow const fns, if we can evaluate them of course
246216
if let TerminatorKind::Call { destination, .. } = terminator.kind {
247-
map[destination.local].is_from_bad_instr = true;
217+
map[destination.local].usage.push(Spanned {
218+
node: LocalUsage::NonConst,
219+
span: terminator.source_info.span,
220+
});
248221
}
249222
}
250223
}
@@ -269,87 +242,85 @@ where
269242
.collect::<Option<Vec<_>>>()
270243
}
271244

272-
fn assignments_all_same_value(map: &LocalUsageMap<'_>, usage: &LocalUsageValues<'_>) -> bool {
273-
let LocalUsageValues {
274-
usage,
275-
mut_ref_acquired: _,
276-
assigned_non_const_rvalue: _,
277-
is_from_bad_instr: _,
278-
} = usage;
245+
fn eval_local(map: &LocalUsageMap<'_>, local: &LocalUsageValues<'_>) -> bool {
246+
let mut assignments = vec![];
279247

280-
if usage.len() <= 1 {
248+
if local.usage.len() == 1 {
281249
return false;
282250
}
283251

284-
// TODO: This code is clone-hell.
285-
286-
let mut all_assignments = vec![];
287-
for assignment in usage {
288-
match &assignment.node {
289-
LocalUsage::Scalar(scalar) => {
290-
all_assignments.push(scalar.clone());
291-
},
292-
// FIXME: This doesn't handle assignment of tuples, arrays or anything else currently.
293-
LocalUsage::Scalars(_) => {},
294-
// FIXME: This doesn't allow nested dependencies, currently.
295-
// FIXME: This only allows one assignment for dependencies.
252+
for assignment in &local.usage {
253+
match assignment.node {
254+
LocalUsage::Scalar(scalar) => assignments.push(scalar),
296255
LocalUsage::DependsOn(local) => {
297-
let [assignment] = &*map[*local].usage else {
298-
continue;
256+
let [assignment] = &*map[local].usage else {
257+
return false;
299258
};
300-
match assignment.node {
301-
LocalUsage::Scalar(scalar) => all_assignments.push(scalar.clone()),
302-
LocalUsage::Scalars(_) => {},
303-
_ => return false,
259+
if let LocalUsage::Scalar(scalar) = assignment.node {
260+
assignments.push(scalar);
261+
} else {
262+
return false;
304263
}
305264
},
306265
LocalUsage::DependsOnWithProj(local, base_proj) => {
307-
let [assignment] = &*map[*local].usage else {
308-
continue;
266+
let [assignment] = &*map[local].usage else {
267+
return false;
309268
};
310269
match base_proj {
311270
PlaceElem::Field(idx, _) if let LocalUsage::Scalars(scalars) = &assignment.node => {
312-
all_assignments.push(scalars[idx.as_usize()].clone());
271+
assignments.push(scalars[idx.as_usize()]);
313272
},
314273
PlaceElem::Index(idx) if let LocalUsage::Scalars(scalars) = &assignment.node => {
315-
all_assignments.push(scalars[idx.as_usize()].clone());
274+
assignments.push(scalars[idx.as_usize()]);
316275
},
317276
_ => return false,
318277
}
319278
},
279+
_ => return false,
320280
}
321281
}
322282

323-
if let [head, tail @ ..] = &*all_assignments && tail.iter().all(|i| i == head) {
283+
if let Some(assignments) = assignments.iter().map(|i| {
284+
if let Scalar::Int(int) = i {
285+
return Some(int.to_bits(int.size()).ok()).flatten();
286+
};
287+
288+
None
289+
})
290+
.collect::<Option<Vec<_>>>()
291+
&& let [head, tail @ ..] = &*assignments
292+
&& tail.iter().all(|&i| i == *head)
293+
{
324294
return true;
325295
}
326296

327297
false
328298
}
329299

330-
fn nested_locals_are_not_from_bad_instr(map: &LocalUsageMap<'_>, usage: &LocalUsageValues<'_>) -> bool {
331-
// FIXME: This is a hacky workaround to not have a stack overflow. Instead, we should fix the root
332-
// cause.
333-
nested_locals_are_not_from_bad_instr_inner(map, usage, 0)
300+
fn eval_nested_locals_are_constant(map: &LocalUsageMap<'_>, local: &LocalUsageValues<'_>) -> bool {
301+
eval_nested_locals_are_constant_with_visited_locals(map, local, &mut FxHashSet::default())
334302
}
335303

336-
fn nested_locals_are_not_from_bad_instr_inner(
304+
/// Do not call this manually - use `eval_nested_locals_are_constant` instead.
305+
fn eval_nested_locals_are_constant_with_visited_locals(
337306
map: &LocalUsageMap<'_>,
338-
usage: &LocalUsageValues<'_>,
339-
depth: usize,
307+
local: &LocalUsageValues<'_>,
308+
visited_locals: &mut FxHashSet<mir::Local>,
340309
) -> bool {
341-
if depth < 10 && !usage.is_from_bad_instr {
342-
let mut all_good_instrs = true;
343-
for assignment in &usage.usage {
344-
match assignment.node {
345-
LocalUsage::Scalar(_) | LocalUsage::Scalars(_) => continue,
346-
LocalUsage::DependsOn(local) | LocalUsage::DependsOnWithProj(local, _) => {
347-
all_good_instrs &= nested_locals_are_not_from_bad_instr_inner(map, &map[local], depth + 1);
348-
},
349-
}
310+
let mut constness = true;
311+
for assignment in &local.usage {
312+
match assignment.node {
313+
LocalUsage::DependsOn(local) | LocalUsage::DependsOnWithProj(local, _) => {
314+
if !visited_locals.insert(local) {
315+
// Short-circuit to ensure we don't get stuck in a loop
316+
return false;
317+
}
318+
319+
constness &= eval_nested_locals_are_constant_with_visited_locals(map, &map[local], visited_locals);
320+
},
321+
LocalUsage::NonConst => return false,
322+
_ => {},
350323
}
351-
return all_good_instrs;
352324
}
353-
354-
false
325+
constness
355326
}

0 commit comments

Comments
 (0)