Skip to content

Commit 635bb32

Browse files
committed
new lint local_assigned_single_value
1 parent 06b444b commit 635bb32

18 files changed

+605
-40
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4900,6 +4900,7 @@ Released 2018-09-13
49004900
[`lines_filter_map_ok`]: https://rust-lang.github.io/rust-clippy/master/index.html#lines_filter_map_ok
49014901
[`linkedlist`]: https://rust-lang.github.io/rust-clippy/master/index.html#linkedlist
49024902
[`little_endian_bytes`]: https://rust-lang.github.io/rust-clippy/master/index.html#little_endian_bytes
4903+
[`local_assigned_single_value`]: https://rust-lang.github.io/rust-clippy/master/index.html#local_assigned_single_value
49034904
[`logic_bug`]: https://rust-lang.github.io/rust-clippy/master/index.html#logic_bug
49044905
[`lossy_float_literal`]: https://rust-lang.github.io/rust-clippy/master/index.html#lossy_float_literal
49054906
[`macro_use_imports`]: https://rust-lang.github.io/rust-clippy/master/index.html#macro_use_imports

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
248248
crate::literal_representation::MISTYPED_LITERAL_SUFFIXES_INFO,
249249
crate::literal_representation::UNREADABLE_LITERAL_INFO,
250250
crate::literal_representation::UNUSUAL_BYTE_GROUPINGS_INFO,
251+
crate::local_assigned_single_value::LOCAL_ASSIGNED_SINGLE_VALUE_INFO,
251252
crate::loops::EMPTY_LOOP_INFO,
252253
crate::loops::EXPLICIT_COUNTER_LOOP_INFO,
253254
crate::loops::EXPLICIT_INTO_ITER_LOOP_INFO,

clippy_lints/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
extern crate rustc_arena;
2525
extern crate rustc_ast;
2626
extern crate rustc_ast_pretty;
27+
extern crate rustc_const_eval;
2728
extern crate rustc_data_structures;
2829
extern crate rustc_driver;
2930
extern crate rustc_errors;
@@ -177,6 +178,7 @@ mod let_with_type_underscore;
177178
mod lifetimes;
178179
mod lines_filter_map_ok;
179180
mod literal_representation;
181+
mod local_assigned_single_value;
180182
mod loops;
181183
mod macro_use;
182184
mod main_recursion;
@@ -1062,6 +1064,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
10621064
def_id_to_usage: rustc_data_structures::fx::FxHashMap::default(),
10631065
})
10641066
});
1067+
store.register_late_pass(|_| Box::new(local_assigned_single_value::LocalAssignedSingleValue));
10651068
// add lints here, do not remove this comment, it's used in `new_lint`
10661069
}
10671070

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
use clippy_utils::diagnostics::span_lint;
2+
use clippy_utils::fn_has_unsatisfiable_preds;
3+
use clippy_utils::source::snippet_opt;
4+
use itertools::Itertools;
5+
use rustc_const_eval::interpret::Scalar;
6+
use rustc_hir::def_id::LocalDefId;
7+
use rustc_hir::{intravisit::FnKind, Body, FnDecl};
8+
use rustc_index::IndexVec;
9+
use rustc_lint::{LateContext, LateLintPass};
10+
use rustc_middle::mir::{
11+
self, interpret::ConstValue, visit::Visitor, Constant, Location, Mutability, Operand, Place, Rvalue,
12+
};
13+
use rustc_middle::mir::{AggregateKind, PlaceElem, Terminator, TerminatorKind};
14+
use rustc_session::{declare_lint_pass, declare_tool_lint};
15+
use rustc_span::source_map::Spanned;
16+
use rustc_span::Span;
17+
18+
declare_clippy_lint! {
19+
/// ### What it does
20+
/// Checks for locals that are always assigned the same value.
21+
///
22+
/// ### Why is this bad?
23+
/// It's almost always a typo. If not, it can be made immutable, or turned into a constant.
24+
///
25+
/// ### Example
26+
/// ```rust
27+
/// let mut x = 1;
28+
/// x = 1;
29+
/// ```
30+
/// Use instead:
31+
/// ```rust
32+
/// let x = 1;
33+
/// ```
34+
#[clippy::version = "1.72.0"]
35+
pub LOCAL_ASSIGNED_SINGLE_VALUE,
36+
correctness,
37+
"disallows assigning locals many times with the same value"
38+
}
39+
declare_lint_pass!(LocalAssignedSingleValue => [LOCAL_ASSIGNED_SINGLE_VALUE]);
40+
41+
impl LateLintPass<'_> for LocalAssignedSingleValue {
42+
fn check_fn(
43+
&mut self,
44+
cx: &LateContext<'_>,
45+
_: FnKind<'_>,
46+
_: &FnDecl<'_>,
47+
_: &Body<'_>,
48+
_: Span,
49+
def_id: LocalDefId,
50+
) {
51+
// Building MIR for `fn`s with unsatisfiable preds results in ICE.
52+
if fn_has_unsatisfiable_preds(cx, def_id.to_def_id()) {
53+
return;
54+
}
55+
56+
let mir = cx.tcx.optimized_mir(def_id.to_def_id());
57+
let mut v = V {
58+
body: mir,
59+
cx,
60+
map: mir.local_decls.iter().map(|_| LocalUsageValues::default()).collect(),
61+
};
62+
v.visit_body(mir);
63+
64+
// for (local, i) in v.map.iter_enumerated() {
65+
// dbg!(local, i);
66+
// }
67+
68+
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;
112+
113+
if let Some(local_decl) = mir.local_decls.get(*local)
114+
&& let [dbg_info] = &*mir
115+
.var_debug_info
116+
.iter()
117+
.filter(|info| info.source_info.span == local_decl.source_info.span)
118+
.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+
}
133+
}
134+
}
135+
*/
136+
}
137+
}
138+
139+
type LocalUsageMap<'tcx> = IndexVec<mir::Local, LocalUsageValues<'tcx>>;
140+
141+
/// Holds the data we have for the usage of a local.
142+
#[derive(Debug, Default)]
143+
struct LocalUsageValues<'tcx> {
144+
/// Where and what this local is assigned.
145+
usage: Vec<Spanned<LocalUsage<'tcx>>>,
146+
/// Whether it's mutably borrowed, ever. We should not lint this.
147+
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,
155+
}
156+
157+
#[derive(Debug)]
158+
enum LocalUsage<'tcx> {
159+
/// A single `Scalar`, for stuff like `i32` or `bool`.
160+
Scalar(Scalar),
161+
/// Any number of `Scalar`s. This is used for handling arrays and tuples
162+
Scalars(Vec<Scalar>),
163+
/// When a `Local` depends on the value of another local
164+
DependsOn(mir::Local),
165+
/// When a `Local` depends on the value of another local by accessing any of its fields or
166+
/// indexing
167+
DependsOnWithProj(mir::Local, &'tcx PlaceElem<'tcx>),
168+
}
169+
170+
struct V<'a, 'tcx> {
171+
body: &'a mir::Body<'tcx>,
172+
cx: &'a LateContext<'tcx>,
173+
map: LocalUsageMap<'tcx>,
174+
}
175+
176+
impl<'a, 'tcx> Visitor<'tcx> for V<'a, 'tcx> {
177+
fn visit_assign(&mut self, place: &Place<'tcx>, rvalue: &Rvalue<'tcx>, loc: Location) {
178+
let Self { body, cx, map } = self;
179+
let Some(stmt) = body.stmt_at(loc).left() else {
180+
return;
181+
};
182+
183+
if stmt.source_info.span.from_expansion() {
184+
return;
185+
}
186+
187+
// Do not lint if there are any mutable borrows to a local
188+
if let Rvalue::Ref(_, mir::BorrowKind::Unique | mir::BorrowKind::Mut { .. }, place)
189+
| Rvalue::AddressOf(Mutability::Mut, place) = rvalue
190+
{
191+
map[place.local].mut_ref_acquired = true;
192+
return;
193+
}
194+
195+
let usage = &mut map[place.local];
196+
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(..))
208+
{
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+
});
220+
}
221+
}
222+
// Handle creation of tuples/arrays, otherwise the above would be useless
223+
else if let Rvalue::Aggregate(kind, fields) = rvalue
224+
// TODO: Handle `Adt`s, if possible.
225+
&& let AggregateKind::Array(..) | AggregateKind::Tuple = **kind
226+
// TODO: Let's remove this `cloned` call, if possible.
227+
&& let Some(scalars) = extract_scalars(cx, fields.into_iter().cloned())
228+
{
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;
235+
} 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;
239+
}
240+
}
241+
242+
fn visit_terminator(&mut self, terminator: &Terminator<'_>, _: Location) {
243+
let Self { body: _, cx: _, map } = self;
244+
245+
// TODO: Maybe we can allow const fns, if we can evaluate them of course
246+
if let TerminatorKind::Call { destination, .. } = terminator.kind {
247+
map[destination.local].is_from_bad_instr = true;
248+
}
249+
}
250+
}
251+
252+
/// `None` means any one of the `Operand`s is not an `Operand::Constant`.
253+
fn extract_scalars<'tcx, O>(cx: &LateContext<'tcx>, operands: O) -> Option<Vec<Scalar>>
254+
where
255+
O: IntoIterator<Item = Operand<'tcx>>,
256+
{
257+
operands
258+
.into_iter()
259+
.map(|operand| -> Option<_> {
260+
if let Operand::Constant(constant) = operand
261+
&& let Constant { literal, .. } = *constant
262+
&& let ConstValue::Scalar(scalar) = literal.try_to_value(cx.tcx)?
263+
{
264+
return Some(scalar);
265+
}
266+
267+
None
268+
})
269+
.collect::<Option<Vec<_>>>()
270+
}
271+
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;
279+
280+
if usage.len() <= 1 {
281+
return false;
282+
}
283+
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.
296+
LocalUsage::DependsOn(local) => {
297+
let [assignment] = &*map[*local].usage else {
298+
continue;
299+
};
300+
match assignment.node {
301+
LocalUsage::Scalar(scalar) => all_assignments.push(scalar.clone()),
302+
LocalUsage::Scalars(_) => {},
303+
_ => return false,
304+
}
305+
},
306+
LocalUsage::DependsOnWithProj(local, base_proj) => {
307+
let [assignment] = &*map[*local].usage else {
308+
continue;
309+
};
310+
match base_proj {
311+
PlaceElem::Field(idx, _) if let LocalUsage::Scalars(scalars) = &assignment.node => {
312+
all_assignments.push(scalars[idx.as_usize()].clone());
313+
},
314+
PlaceElem::Index(idx) if let LocalUsage::Scalars(scalars) = &assignment.node => {
315+
all_assignments.push(scalars[idx.as_usize()].clone());
316+
},
317+
_ => return false,
318+
}
319+
},
320+
}
321+
}
322+
323+
if let [head, tail @ ..] = &*all_assignments && tail.iter().all(|i| i == head) {
324+
return true;
325+
}
326+
327+
false
328+
}
329+
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)
334+
}
335+
336+
fn nested_locals_are_not_from_bad_instr_inner(
337+
map: &LocalUsageMap<'_>,
338+
usage: &LocalUsageValues<'_>,
339+
depth: usize,
340+
) -> 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+
}
350+
}
351+
return all_good_instrs;
352+
}
353+
354+
false
355+
}

0 commit comments

Comments
 (0)