Skip to content

Commit ae571c8

Browse files
Refactor useless_vec lint (#14503)
Currently there's an "implicit" invariant in this lint's code, which has been bugging me for a while (but especially so because I recently wanted to extend this lint): when we see a `vec![]` expression that we can't suggest changing to an array because of surrounding expressions (e.g. it's passed to a function that requires a `Vec`), we *must* insert that into this `self.span_to_lint_map` map with `None`. See FP issue #11861 for the motivating example of why this lint is doing that (and the doc comment in the code I added). This invariant is really easy to miss and error prone: in the old code, every other `} else {` branch needed a `self.span_to_lint_map.insert(.., None)` call. This PR moves out the logic that checks if an expression *needs* to be a vec based on its surrounding environment. This way we can cover all cases with one `else` at its callsite and only need to insert `None` in one isolated place. I was also working on extending this lint with another pattern, where refactoring the "does this expression need to be a vec or does a slice work too" into its own function would be very convenient. (Tbh, I don't think this invariant/FP actually matters much in practice, because it's a bit of an edge case IMO. I don't think it's really worth all the complexity (even though I was the one to suggest how we could fix this in the linked issue). This is also already an issue with every other lint that can change an expression's type. I also don't know if this is compatible with "incremental lints", which IIUC work per-module (I know that MSRV handling recently had to change due to that). In any case, I just decided to keep it for now so that this is purely an internal refactor without user facing changes.) Fixes #14531 (indirectly, added a test for it) changelog: none
2 parents 65bac18 + 47e8c31 commit ae571c8

File tree

4 files changed

+169
-89
lines changed

4 files changed

+169
-89
lines changed

clippy_lints/src/vec.rs

Lines changed: 144 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use std::collections::BTreeMap;
2+
use std::collections::btree_map::Entry;
3+
use std::mem;
24
use std::ops::ControlFlow;
35

46
use clippy_config::Conf;
@@ -20,15 +22,36 @@ use rustc_span::{DesugaringKind, Span};
2022
pub struct UselessVec {
2123
too_large_for_stack: u64,
2224
msrv: Msrv,
23-
span_to_lint_map: BTreeMap<Span, Option<(HirId, SuggestedType, String, Applicability)>>,
25+
/// Maps from a `vec![]` source callsite invocation span to the "state" (i.e., whether we can
26+
/// emit a warning there or not).
27+
///
28+
/// The purpose of this is to buffer lints up until `check_crate_post` so that we can cancel a
29+
/// lint while visiting, because a `vec![]` invocation span can appear multiple times when
30+
/// it is passed as a macro argument, once in a context that doesn't require a `Vec<_>` and
31+
/// another time that does. Consider:
32+
/// ```
33+
/// macro_rules! m {
34+
/// ($v:expr) => {
35+
/// let a = $v;
36+
/// $v.push(3);
37+
/// }
38+
/// }
39+
/// m!(vec![1, 2]);
40+
/// ```
41+
/// The macro invocation expands to two `vec![1, 2]` invocations. If we eagerly suggest changing
42+
/// the first `vec![1, 2]` (which is shared with the other expn) to an array which indeed would
43+
/// work, we get a false positive warning on the `$v.push(3)` which really requires `$v` to
44+
/// be a vector.
45+
span_to_state: BTreeMap<Span, VecState>,
2446
allow_in_test: bool,
2547
}
48+
2649
impl UselessVec {
2750
pub fn new(conf: &'static Conf) -> Self {
2851
Self {
2952
too_large_for_stack: conf.too_large_for_stack,
3053
msrv: conf.msrv,
31-
span_to_lint_map: BTreeMap::new(),
54+
span_to_state: BTreeMap::new(),
3255
allow_in_test: conf.allow_useless_vec_in_tests,
3356
}
3457
}
@@ -62,17 +85,28 @@ declare_clippy_lint! {
6285

6386
impl_lint_pass!(UselessVec => [USELESS_VEC]);
6487

65-
impl<'tcx> LateLintPass<'tcx> for UselessVec {
66-
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
67-
let Some(vec_args) = higher::VecArgs::hir(cx, expr.peel_borrows()) else {
68-
return;
69-
};
70-
if self.allow_in_test && is_in_test(cx.tcx, expr.hir_id) {
71-
return;
72-
}
73-
// the parent callsite of this `vec!` expression, or span to the borrowed one such as `&vec!`
74-
let callsite = expr.span.parent_callsite().unwrap_or(expr.span);
88+
/// The "state" of a `vec![]` invocation, indicating whether it can or cannot be changed.
89+
enum VecState {
90+
Change {
91+
suggest_ty: SuggestedType,
92+
vec_snippet: String,
93+
expr_hir_id: HirId,
94+
},
95+
NoChange,
96+
}
97+
98+
enum VecToArray {
99+
/// Expression does not need to be a `Vec<_>` and its type can be changed to an array (or
100+
/// slice).
101+
Possible,
102+
/// Expression must be a `Vec<_>`. Type cannot change.
103+
Impossible,
104+
}
75105

106+
impl UselessVec {
107+
/// Checks if the surrounding environment requires this expression to actually be of type
108+
/// `Vec<_>`, or if it can be changed to `&[]`/`[]` without causing type errors.
109+
fn expr_usage_requires_vec(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) -> VecToArray {
76110
match cx.tcx.parent_hir_node(expr.hir_id) {
77111
// search for `let foo = vec![_]` expressions where all uses of `foo`
78112
// adjust to slices or call a method that exist on slices (e.g. len)
@@ -100,110 +134,126 @@ impl<'tcx> LateLintPass<'tcx> for UselessVec {
100134
.is_continue();
101135

102136
if only_slice_uses {
103-
self.check_vec_macro(cx, &vec_args, callsite, expr.hir_id, SuggestedType::Array);
137+
VecToArray::Possible
104138
} else {
105-
self.span_to_lint_map.insert(callsite, None);
139+
VecToArray::Impossible
106140
}
107141
},
108142
// if the local pattern has a specified type, do not lint.
109143
Node::LetStmt(LetStmt { ty: Some(_), .. }) if higher::VecArgs::hir(cx, expr).is_some() => {
110-
self.span_to_lint_map.insert(callsite, None);
144+
VecToArray::Impossible
111145
},
112146
// search for `for _ in vec![...]`
113147
Node::Expr(Expr { span, .. })
114148
if span.is_desugaring(DesugaringKind::ForLoop) && self.msrv.meets(cx, msrvs::ARRAY_INTO_ITERATOR) =>
115149
{
116-
let suggest_slice = suggest_type(expr);
117-
self.check_vec_macro(cx, &vec_args, callsite, expr.hir_id, suggest_slice);
150+
VecToArray::Possible
118151
},
119152
// search for `&vec![_]` or `vec![_]` expressions where the adjusted type is `&[_]`
120153
_ => {
121-
let suggest_slice = suggest_type(expr);
122-
123154
if adjusts_to_slice(cx, expr) {
124-
self.check_vec_macro(cx, &vec_args, callsite, expr.hir_id, suggest_slice);
155+
VecToArray::Possible
125156
} else {
126-
self.span_to_lint_map.insert(callsite, None);
157+
VecToArray::Impossible
127158
}
128159
},
129160
}
130161
}
162+
}
163+
164+
impl<'tcx> LateLintPass<'tcx> for UselessVec {
165+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
166+
if let Some(vec_args) = higher::VecArgs::hir(cx, expr.peel_borrows())
167+
// The `vec![]` or `&vec![]` invocation span.
168+
&& let vec_span = expr.span.parent_callsite().unwrap_or(expr.span)
169+
&& !vec_span.from_expansion()
170+
{
171+
if self.allow_in_test && is_in_test(cx.tcx, expr.hir_id) {
172+
return;
173+
}
174+
175+
match self.expr_usage_requires_vec(cx, expr) {
176+
VecToArray::Possible => {
177+
let suggest_ty = suggest_type(expr);
178+
179+
// Size and `Copy` checks don't depend on the enclosing usage of the expression
180+
// and don't need to be inserted into the state map.
181+
let vec_snippet = match vec_args {
182+
higher::VecArgs::Repeat(expr, len) => {
183+
if is_copy(cx, cx.typeck_results().expr_ty(expr))
184+
&& let Some(Constant::Int(length)) = ConstEvalCtxt::new(cx).eval(len)
185+
&& let Ok(length) = u64::try_from(length)
186+
&& size_of(cx, expr)
187+
.checked_mul(length)
188+
.is_some_and(|size| size <= self.too_large_for_stack)
189+
{
190+
suggest_ty.snippet(
191+
cx,
192+
Some(expr.span.source_callsite()),
193+
Some(len.span.source_callsite()),
194+
)
195+
} else {
196+
return;
197+
}
198+
},
199+
higher::VecArgs::Vec(args) => {
200+
if let Ok(length) = u64::try_from(args.len())
201+
&& size_of(cx, expr)
202+
.checked_mul(length)
203+
.is_some_and(|size| size <= self.too_large_for_stack)
204+
{
205+
suggest_ty.snippet(
206+
cx,
207+
args.first().zip(args.last()).map(|(first, last)| {
208+
first.span.source_callsite().to(last.span.source_callsite())
209+
}),
210+
None,
211+
)
212+
} else {
213+
return;
214+
}
215+
},
216+
};
217+
218+
if let Entry::Vacant(entry) = self.span_to_state.entry(vec_span) {
219+
entry.insert(VecState::Change {
220+
suggest_ty,
221+
vec_snippet,
222+
expr_hir_id: expr.hir_id,
223+
});
224+
}
225+
},
226+
VecToArray::Impossible => {
227+
self.span_to_state.insert(vec_span, VecState::NoChange);
228+
},
229+
}
230+
}
231+
}
131232

132233
fn check_crate_post(&mut self, cx: &LateContext<'tcx>) {
133-
for (span, lint_opt) in &self.span_to_lint_map {
134-
if let Some((hir_id, suggest_slice, snippet, applicability)) = lint_opt {
135-
let help_msg = format!("you can use {} directly", suggest_slice.desc());
136-
span_lint_hir_and_then(cx, USELESS_VEC, *hir_id, *span, "useless use of `vec!`", |diag| {
137-
// If the `vec!` macro contains comment, better not make the suggestion machine
138-
// applicable as it would remove them.
139-
let applicability = if *applicability != Applicability::Unspecified
140-
&& let source_map = cx.tcx.sess.source_map()
141-
&& span_contains_comment(source_map, *span)
142-
{
234+
for (span, state) in mem::take(&mut self.span_to_state) {
235+
if let VecState::Change {
236+
suggest_ty,
237+
vec_snippet,
238+
expr_hir_id,
239+
} = state
240+
{
241+
span_lint_hir_and_then(cx, USELESS_VEC, expr_hir_id, span, "useless use of `vec!`", |diag| {
242+
let help_msg = format!("you can use {} directly", suggest_ty.desc());
243+
// If the `vec!` macro contains comment, better not make the suggestion machine applicable as it
244+
// would remove them.
245+
let applicability = if span_contains_comment(cx.tcx.sess.source_map(), span) {
143246
Applicability::Unspecified
144247
} else {
145-
*applicability
248+
Applicability::MachineApplicable
146249
};
147-
diag.span_suggestion(*span, help_msg, snippet, applicability);
250+
diag.span_suggestion(span, help_msg, vec_snippet, applicability);
148251
});
149252
}
150253
}
151254
}
152255
}
153256

154-
impl UselessVec {
155-
fn check_vec_macro<'tcx>(
156-
&mut self,
157-
cx: &LateContext<'tcx>,
158-
vec_args: &higher::VecArgs<'tcx>,
159-
span: Span,
160-
hir_id: HirId,
161-
suggest_slice: SuggestedType,
162-
) {
163-
if span.from_expansion() {
164-
return;
165-
}
166-
167-
let snippet = match *vec_args {
168-
higher::VecArgs::Repeat(elem, len) => {
169-
if let Some(Constant::Int(len_constant)) = ConstEvalCtxt::new(cx).eval(len) {
170-
// vec![ty; N] works when ty is Clone, [ty; N] requires it to be Copy also
171-
if !is_copy(cx, cx.typeck_results().expr_ty(elem)) {
172-
return;
173-
}
174-
175-
#[expect(clippy::cast_possible_truncation)]
176-
if len_constant as u64 * size_of(cx, elem) > self.too_large_for_stack {
177-
return;
178-
}
179-
180-
suggest_slice.snippet(cx, Some(elem.span), Some(len.span))
181-
} else {
182-
return;
183-
}
184-
},
185-
higher::VecArgs::Vec(args) => {
186-
let args_span = if let Some(last) = args.iter().last() {
187-
if args.len() as u64 * size_of(cx, last) > self.too_large_for_stack {
188-
return;
189-
}
190-
Some(args[0].span.source_callsite().to(last.span.source_callsite()))
191-
} else {
192-
None
193-
};
194-
suggest_slice.snippet(cx, args_span, None)
195-
},
196-
};
197-
198-
self.span_to_lint_map.entry(span).or_insert(Some((
199-
hir_id,
200-
suggest_slice,
201-
snippet,
202-
Applicability::MachineApplicable,
203-
)));
204-
}
205-
}
206-
207257
#[derive(Copy, Clone)]
208258
pub(crate) enum SuggestedType {
209259
/// Suggest using a slice `&[..]` / `&mut [..]`
@@ -221,11 +271,17 @@ impl SuggestedType {
221271
}
222272

223273
fn snippet(self, cx: &LateContext<'_>, args_span: Option<Span>, len_span: Option<Span>) -> String {
274+
// Invariant of the lint as implemented: all spans are from the root context (and as a result,
275+
// always trivially crate-local).
276+
assert!(args_span.is_none_or(|s| !s.from_expansion()));
277+
assert!(len_span.is_none_or(|s| !s.from_expansion()));
278+
224279
let maybe_args = args_span
225-
.and_then(|sp| sp.get_source_text(cx))
280+
.map(|sp| sp.get_source_text(cx).expect("spans are always crate-local"))
226281
.map_or(String::new(), |x| x.to_owned());
227282
let maybe_len = len_span
228-
.and_then(|sp| sp.get_source_text(cx).map(|s| format!("; {s}")))
283+
.map(|sp| sp.get_source_text(cx).expect("spans are always crate-local"))
284+
.map(|st| format!("; {st}"))
229285
.unwrap_or_default();
230286

231287
match self {

tests/ui/vec.fixed

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,12 @@ fn issue_12101() {
242242
for a in &[1, 2] {}
243243
//~^ useless_vec
244244
}
245+
246+
fn issue_14531() {
247+
// The lint used to suggest using an array rather than a reference to a slice.
248+
249+
fn requires_ref_slice(v: &[()]) {}
250+
let v = &[];
251+
//~^ useless_vec
252+
requires_ref_slice(v);
253+
}

tests/ui/vec.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,12 @@ fn issue_12101() {
242242
for a in &(vec![1, 2]) {}
243243
//~^ useless_vec
244244
}
245+
246+
fn issue_14531() {
247+
// The lint used to suggest using an array rather than a reference to a slice.
248+
249+
fn requires_ref_slice(v: &[()]) {}
250+
let v = &vec![];
251+
//~^ useless_vec
252+
requires_ref_slice(v);
253+
}

tests/ui/vec.stderr

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,5 +127,11 @@ error: useless use of `vec!`
127127
LL | for a in &(vec![1, 2]) {}
128128
| ^^^^^^^^^^^^^ help: you can use a slice directly: `&[1, 2]`
129129

130-
error: aborting due to 21 previous errors
130+
error: useless use of `vec!`
131+
--> tests/ui/vec.rs:250:13
132+
|
133+
LL | let v = &vec![];
134+
| ^^^^^^^ help: you can use a slice directly: `&[]`
135+
136+
error: aborting due to 22 previous errors
131137

0 commit comments

Comments
 (0)