Skip to content

Commit 15ad824

Browse files
committed
Auto merge of #13034 - rspencer01:trivial_map_over_range, r=y21
Add new `trivial_map_over_range` lint This lint checks for code that looks like ```rust let something : Vec<_> = (0..100).map(|_| { 1 + 2 + 3 }).collect(); ``` which is more clear as ```rust let something : Vec<_> = std::iter::repeat_with(|| { 1 + 2 + 3 }).take(100).collect(); ``` That is, a map over a range which does nothing with the parameter passed to it is simply a function (or closure) being called `n` times and could be more semantically expressed using `take`. - [x] Followed [lint naming conventions][lint_naming] - [x] Added passing UI tests (including committed `.stderr` file) - [x] `cargo test` passes locally - [x] Executed `cargo dev update_lints` - [x] Added lint documentation - [x] Run `cargo dev fmt` changelog: new lint: [`trivial_map_over_range`] `restriction`
2 parents 35a7095 + acc3842 commit 15ad824

16 files changed

+558
-8
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5697,6 +5697,7 @@ Released 2018-09-13
56975697
[`map_flatten`]: https://rust-lang.github.io/rust-clippy/master/index.html#map_flatten
56985698
[`map_identity`]: https://rust-lang.github.io/rust-clippy/master/index.html#map_identity
56995699
[`map_unwrap_or`]: https://rust-lang.github.io/rust-clippy/master/index.html#map_unwrap_or
5700+
[`map_with_unused_argument_over_ranges`]: https://rust-lang.github.io/rust-clippy/master/index.html#map_with_unused_argument_over_ranges
57005701
[`match_as_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#match_as_ref
57015702
[`match_bool`]: https://rust-lang.github.io/rust-clippy/master/index.html#match_bool
57025703
[`match_like_matches_macro`]: https://rust-lang.github.io/rust-clippy/master/index.html#match_like_matches_macro

book/src/lint_configuration.md

+1
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,7 @@ The minimum rust version that the project supports. Defaults to the `rust-versio
710710
* [`manual_try_fold`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_try_fold)
711711
* [`map_clone`](https://rust-lang.github.io/rust-clippy/master/index.html#map_clone)
712712
* [`map_unwrap_or`](https://rust-lang.github.io/rust-clippy/master/index.html#map_unwrap_or)
713+
* [`map_with_unused_argument_over_ranges`](https://rust-lang.github.io/rust-clippy/master/index.html#map_with_unused_argument_over_ranges)
713714
* [`match_like_matches_macro`](https://rust-lang.github.io/rust-clippy/master/index.html#match_like_matches_macro)
714715
* [`mem_replace_with_default`](https://rust-lang.github.io/rust-clippy/master/index.html#mem_replace_with_default)
715716
* [`missing_const_for_fn`](https://rust-lang.github.io/rust-clippy/master/index.html#missing_const_for_fn)

clippy_config/src/conf.rs

+1
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,7 @@ define_Conf! {
573573
manual_try_fold,
574574
map_clone,
575575
map_unwrap_or,
576+
map_with_unused_argument_over_ranges,
576577
match_like_matches_macro,
577578
mem_replace_with_default,
578579
missing_const_for_fn,

clippy_config/src/msrvs.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ macro_rules! msrv_aliases {
1919
// names may refer to stabilized feature flags or library items
2020
msrv_aliases! {
2121
1,83,0 { CONST_EXTERN_FN, CONST_FLOAT_BITS_CONV, CONST_FLOAT_CLASSIFY }
22-
1,82,0 { IS_NONE_OR }
22+
1,82,0 { IS_NONE_OR, REPEAT_N }
2323
1,81,0 { LINT_REASONS_STABILIZATION }
2424
1,80,0 { BOX_INTO_ITER}
2525
1,77,0 { C_STR_LITERALS }
@@ -55,7 +55,7 @@ msrv_aliases! {
5555
1,33,0 { UNDERSCORE_IMPORTS }
5656
1,30,0 { ITERATOR_FIND_MAP, TOOL_ATTRIBUTES }
5757
1,29,0 { ITER_FLATTEN }
58-
1,28,0 { FROM_BOOL }
58+
1,28,0 { FROM_BOOL, REPEAT_WITH }
5959
1,27,0 { ITERATOR_TRY_FOLD }
6060
1,26,0 { RANGE_INCLUSIVE, STRING_RETAIN }
6161
1,24,0 { IS_ASCII_DIGIT }

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
423423
crate::methods::MAP_FLATTEN_INFO,
424424
crate::methods::MAP_IDENTITY_INFO,
425425
crate::methods::MAP_UNWRAP_OR_INFO,
426+
crate::methods::MAP_WITH_UNUSED_ARGUMENT_OVER_RANGES_INFO,
426427
crate::methods::MUT_MUTEX_LOCK_INFO,
427428
crate::methods::NAIVE_BYTECOUNT_INFO,
428429
crate::methods::NEEDLESS_AS_BYTES_INFO,

clippy_lints/src/matches/single_match.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,10 @@ impl<'a> PatState<'a> {
249249
let states = match self {
250250
Self::Wild => return None,
251251
Self::Other => {
252-
*self = Self::StdEnum(cx.arena.alloc_from_iter((0..adt.variants().len()).map(|_| Self::Other)));
252+
*self = Self::StdEnum(
253+
cx.arena
254+
.alloc_from_iter(std::iter::repeat_with(|| Self::Other).take(adt.variants().len())),
255+
);
253256
let Self::StdEnum(x) = self else {
254257
unreachable!();
255258
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use crate::methods::MAP_WITH_UNUSED_ARGUMENT_OVER_RANGES;
2+
use clippy_config::msrvs::{self, Msrv};
3+
use clippy_utils::diagnostics::span_lint_and_then;
4+
use clippy_utils::source::snippet_with_applicability;
5+
use clippy_utils::sugg::Sugg;
6+
use clippy_utils::{eager_or_lazy, higher, usage};
7+
use rustc_ast::LitKind;
8+
use rustc_ast::ast::RangeLimits;
9+
use rustc_data_structures::packed::Pu128;
10+
use rustc_errors::Applicability;
11+
use rustc_hir::{Body, Closure, Expr, ExprKind};
12+
use rustc_lint::LateContext;
13+
use rustc_span::Span;
14+
15+
fn extract_count_with_applicability(
16+
cx: &LateContext<'_>,
17+
range: higher::Range<'_>,
18+
applicability: &mut Applicability,
19+
) -> Option<String> {
20+
let start = range.start?;
21+
let end = range.end?;
22+
// TODO: This doens't handle if either the start or end are negative literals, or if the start is
23+
// not a literal. In the first case, we need to be careful about how we handle computing the
24+
// count to avoid overflows. In the second, we may need to add parenthesis to make the
25+
// suggestion correct.
26+
if let ExprKind::Lit(lit) = start.kind
27+
&& let LitKind::Int(Pu128(lower_bound), _) = lit.node
28+
{
29+
if let ExprKind::Lit(lit) = end.kind
30+
&& let LitKind::Int(Pu128(upper_bound), _) = lit.node
31+
{
32+
// Here we can explicitly calculate the number of iterations
33+
let count = if upper_bound >= lower_bound {
34+
match range.limits {
35+
RangeLimits::HalfOpen => upper_bound - lower_bound,
36+
RangeLimits::Closed => (upper_bound - lower_bound).checked_add(1)?,
37+
}
38+
} else {
39+
0
40+
};
41+
return Some(format!("{count}"));
42+
}
43+
let end_snippet = Sugg::hir_with_applicability(cx, end, "...", applicability)
44+
.maybe_par()
45+
.into_string();
46+
if lower_bound == 0 {
47+
if range.limits == RangeLimits::Closed {
48+
return Some(format!("{end_snippet} + 1"));
49+
}
50+
return Some(end_snippet);
51+
}
52+
if range.limits == RangeLimits::Closed {
53+
return Some(format!("{end_snippet} - {}", lower_bound - 1));
54+
}
55+
return Some(format!("{end_snippet} - {lower_bound}"));
56+
}
57+
None
58+
}
59+
60+
pub(super) fn check(
61+
cx: &LateContext<'_>,
62+
ex: &Expr<'_>,
63+
receiver: &Expr<'_>,
64+
arg: &Expr<'_>,
65+
msrv: &Msrv,
66+
method_call_span: Span,
67+
) {
68+
let mut applicability = Applicability::MaybeIncorrect;
69+
if let Some(range) = higher::Range::hir(receiver)
70+
&& let ExprKind::Closure(Closure { body, .. }) = arg.kind
71+
&& let body_hir = cx.tcx.hir().body(*body)
72+
&& let Body {
73+
params: [param],
74+
value: body_expr,
75+
} = body_hir
76+
&& !usage::BindingUsageFinder::are_params_used(cx, body_hir)
77+
&& let Some(count) = extract_count_with_applicability(cx, range, &mut applicability)
78+
{
79+
let method_to_use_name;
80+
let new_span;
81+
let use_take;
82+
83+
if eager_or_lazy::switch_to_eager_eval(cx, body_expr) {
84+
if msrv.meets(msrvs::REPEAT_N) {
85+
method_to_use_name = "repeat_n";
86+
let body_snippet = snippet_with_applicability(cx, body_expr.span, "..", &mut applicability);
87+
new_span = (arg.span, format!("{body_snippet}, {count}"));
88+
use_take = false;
89+
} else {
90+
method_to_use_name = "repeat";
91+
let body_snippet = snippet_with_applicability(cx, body_expr.span, "..", &mut applicability);
92+
new_span = (arg.span, body_snippet.to_string());
93+
use_take = true;
94+
}
95+
} else if msrv.meets(msrvs::REPEAT_WITH) {
96+
method_to_use_name = "repeat_with";
97+
new_span = (param.span, String::new());
98+
use_take = true;
99+
} else {
100+
return;
101+
}
102+
103+
// We need to provide nonempty parts to diag.multipart_suggestion so we
104+
// collate all our parts here and then remove those that are empty.
105+
let mut parts = vec![
106+
(
107+
receiver.span.to(method_call_span),
108+
format!("std::iter::{method_to_use_name}"),
109+
),
110+
new_span,
111+
];
112+
if use_take {
113+
parts.push((ex.span.shrink_to_hi(), format!(".take({count})")));
114+
}
115+
116+
span_lint_and_then(
117+
cx,
118+
MAP_WITH_UNUSED_ARGUMENT_OVER_RANGES,
119+
ex.span,
120+
"map of a closure that does not depend on its parameter over a range",
121+
|diag| {
122+
diag.multipart_suggestion(
123+
if use_take {
124+
format!("remove the explicit range and use `{method_to_use_name}` and `take`")
125+
} else {
126+
format!("remove the explicit range and use `{method_to_use_name}`")
127+
},
128+
parts,
129+
applicability,
130+
);
131+
},
132+
);
133+
}
134+
}

clippy_lints/src/methods/mod.rs

+37
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ mod map_err_ignore;
6767
mod map_flatten;
6868
mod map_identity;
6969
mod map_unwrap_or;
70+
mod map_with_unused_argument_over_ranges;
7071
mod mut_mutex_lock;
7172
mod needless_as_bytes;
7273
mod needless_character_iteration;
@@ -4218,6 +4219,40 @@ declare_clippy_lint! {
42184219
"combine `.map(_)` followed by `.all(identity)`/`.any(identity)` into a single call"
42194220
}
42204221

4222+
declare_clippy_lint! {
4223+
/// ### What it does
4224+
///
4225+
/// Checks for `Iterator::map` over ranges without using the parameter which
4226+
/// could be more clearly expressed using `std::iter::repeat(...).take(...)`
4227+
/// or `std::iter::repeat_n`.
4228+
///
4229+
/// ### Why is this bad?
4230+
///
4231+
/// It expresses the intent more clearly to `take` the correct number of times
4232+
/// from a generating function than to apply a closure to each number in a
4233+
/// range only to discard them.
4234+
///
4235+
/// ### Example
4236+
///
4237+
/// ```no_run
4238+
/// let random_numbers : Vec<_> = (0..10).map(|_| { 3 + 1 }).collect();
4239+
/// ```
4240+
/// Use instead:
4241+
/// ```no_run
4242+
/// let f : Vec<_> = std::iter::repeat( 3 + 1 ).take(10).collect();
4243+
/// ```
4244+
///
4245+
/// ### Known Issues
4246+
///
4247+
/// This lint may suggest replacing a `Map<Range>` with a `Take<RepeatWith>`.
4248+
/// The former implements some traits that the latter does not, such as
4249+
/// `DoubleEndedIterator`.
4250+
#[clippy::version = "1.84.0"]
4251+
pub MAP_WITH_UNUSED_ARGUMENT_OVER_RANGES,
4252+
restriction,
4253+
"map of a trivial closure (not dependent on parameter) over a range"
4254+
}
4255+
42214256
pub struct Methods {
42224257
avoid_breaking_exported_api: bool,
42234258
msrv: Msrv,
@@ -4381,6 +4416,7 @@ impl_lint_pass!(Methods => [
43814416
UNNECESSARY_MIN_OR_MAX,
43824417
NEEDLESS_AS_BYTES,
43834418
MAP_ALL_ANY_IDENTITY,
4419+
MAP_WITH_UNUSED_ARGUMENT_OVER_RANGES,
43844420
]);
43854421

43864422
/// Extracts a method call name, args, and `Span` of the method name.
@@ -4876,6 +4912,7 @@ impl Methods {
48764912
if name == "map" {
48774913
unused_enumerate_index::check(cx, expr, recv, m_arg);
48784914
map_clone::check(cx, expr, recv, m_arg, &self.msrv);
4915+
map_with_unused_argument_over_ranges::check(cx, expr, recv, m_arg, &self.msrv, span);
48794916
match method_call(recv) {
48804917
Some((map_name @ ("iter" | "into_iter"), recv2, _, _, _)) => {
48814918
iter_kv_map::check(cx, map_name, expr, recv2, m_arg, &self.msrv);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#![allow(
2+
unused,
3+
clippy::redundant_closure,
4+
clippy::reversed_empty_ranges,
5+
clippy::identity_op
6+
)]
7+
#![warn(clippy::map_with_unused_argument_over_ranges)]
8+
9+
fn do_something() -> usize {
10+
todo!()
11+
}
12+
13+
fn do_something_interesting(x: usize, y: usize) -> usize {
14+
todo!()
15+
}
16+
17+
macro_rules! gen {
18+
() => {
19+
(0..10).map(|_| do_something());
20+
};
21+
}
22+
23+
fn main() {
24+
// These should be raised
25+
std::iter::repeat_with(|| do_something()).take(10);
26+
std::iter::repeat_with(|| do_something()).take(10);
27+
std::iter::repeat_with(|| do_something()).take(11);
28+
std::iter::repeat_with(|| do_something()).take(7);
29+
std::iter::repeat_with(|| do_something()).take(8);
30+
std::iter::repeat_n(3, 10);
31+
std::iter::repeat_with(|| {
32+
let x = 3;
33+
x + 2
34+
}).take(10);
35+
std::iter::repeat_with(|| do_something()).take(10).collect::<Vec<_>>();
36+
let upper = 4;
37+
std::iter::repeat_with(|| do_something()).take(upper);
38+
let upper_fn = || 4;
39+
std::iter::repeat_with(|| do_something()).take(upper_fn());
40+
std::iter::repeat_with(|| do_something()).take(upper_fn() + 1);
41+
std::iter::repeat_with(|| do_something()).take(upper_fn() - 2);
42+
std::iter::repeat_with(|| do_something()).take(upper_fn() - 1);
43+
(-3..9).map(|_| do_something());
44+
std::iter::repeat_with(|| do_something()).take(0);
45+
std::iter::repeat_with(|| do_something()).take(1);
46+
std::iter::repeat_with(|| do_something()).take((1 << 4) - 0);
47+
// These should not be raised
48+
gen!();
49+
let lower = 2;
50+
let lower_fn = || 2;
51+
(lower..upper_fn()).map(|_| do_something()); // Ranges not starting at zero not yet handled
52+
(lower_fn()..upper_fn()).map(|_| do_something()); // Ranges not starting at zero not yet handled
53+
(lower_fn()..=upper_fn()).map(|_| do_something()); // Ranges not starting at zero not yet handled
54+
(0..10).map(|x| do_something_interesting(x, 4)); // Actual map over range
55+
"Foobar".chars().map(|_| do_something()); // Not a map over range
56+
// i128::MAX == 340282366920938463463374607431768211455
57+
(0..=340282366920938463463374607431768211455).map(|_: u128| do_something()); // Can't be replaced due to overflow
58+
}
59+
60+
#[clippy::msrv = "1.27"]
61+
fn msrv_1_27() {
62+
(0..10).map(|_| do_something());
63+
}
64+
65+
#[clippy::msrv = "1.28"]
66+
fn msrv_1_28() {
67+
std::iter::repeat_with(|| do_something()).take(10);
68+
}
69+
70+
#[clippy::msrv = "1.81"]
71+
fn msrv_1_82() {
72+
std::iter::repeat(3).take(10);
73+
}

0 commit comments

Comments
 (0)