Skip to content

Commit 9960a3e

Browse files
Rollup merge of rust-lang#107890 - obeis:mapping-to-unit, r=cjgillot
Lint against `Iterator::map` receiving a callable that returns `()` Close rust-lang#106991
2 parents 181e38d + 99344a8 commit 9960a3e

File tree

10 files changed

+260
-1
lines changed

10 files changed

+260
-1
lines changed

compiler/rustc_lint/locales/en-US.ftl

+7
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ lint_for_loops_over_fallibles =
2424
.use_while_let = to check pattern in a loop use `while let`
2525
.use_question_mark = consider unwrapping the `Result` with `?` to iterate over its contents
2626
27+
lint_map_unit_fn = `Iterator::map` call that discard the iterator's values
28+
.note = `Iterator::map`, like many of the methods on `Iterator`, gets executed lazily, meaning that its effects won't be visible until it is iterated
29+
.function_label = this function returns `()`, which is likely not what you wanted
30+
.argument_label = called `Iterator::map` with callable that returns `()`
31+
.map_label = after this call to map, the resulting iterator is `impl Iterator<Item = ()>`, which means the only information carried by the iterator is the number of items
32+
.suggestion = you might have meant to use `Iterator::for_each`
33+
2734
lint_non_binding_let_on_sync_lock =
2835
non-binding let on a synchronization lock
2936

compiler/rustc_lint/src/lib.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ mod late;
6363
mod let_underscore;
6464
mod levels;
6565
mod lints;
66+
mod map_unit_fn;
6667
mod methods;
6768
mod multiple_supertrait_upcastable;
6869
mod non_ascii_idents;
@@ -100,6 +101,7 @@ use for_loops_over_fallibles::*;
100101
use hidden_unicode_codepoints::*;
101102
use internal::*;
102103
use let_underscore::*;
104+
use map_unit_fn::*;
103105
use methods::*;
104106
use multiple_supertrait_upcastable::*;
105107
use non_ascii_idents::*;
@@ -239,6 +241,7 @@ late_lint_methods!(
239241
NamedAsmLabels: NamedAsmLabels,
240242
OpaqueHiddenInferredBound: OpaqueHiddenInferredBound,
241243
MultipleSupertraitUpcastable: MultipleSupertraitUpcastable,
244+
MapUnitFn: MapUnitFn,
242245
]
243246
]
244247
);
@@ -298,7 +301,8 @@ fn register_builtins(store: &mut LintStore) {
298301
UNUSED_LABELS,
299302
UNUSED_PARENS,
300303
UNUSED_BRACES,
301-
REDUNDANT_SEMICOLONS
304+
REDUNDANT_SEMICOLONS,
305+
MAP_UNIT_FN
302306
);
303307

304308
add_lint_group!("let_underscore", LET_UNDERSCORE_DROP, LET_UNDERSCORE_LOCK);

compiler/rustc_lint/src/lints.rs

+16
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,22 @@ impl AddToDiagnostic for HiddenUnicodeCodepointsDiagSub {
748748
}
749749
}
750750

751+
// map_unit_fn.rs
752+
#[derive(LintDiagnostic)]
753+
#[diag(lint_map_unit_fn)]
754+
#[note]
755+
pub struct MappingToUnit {
756+
#[label(lint_function_label)]
757+
pub function_label: Span,
758+
#[label(lint_argument_label)]
759+
pub argument_label: Span,
760+
#[label(lint_map_label)]
761+
pub map_label: Span,
762+
#[suggestion(style = "verbose", code = "{replace}", applicability = "maybe-incorrect")]
763+
pub suggestion: Span,
764+
pub replace: String,
765+
}
766+
751767
// internal.rs
752768
#[derive(LintDiagnostic)]
753769
#[diag(lint_default_hash_types)]
+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use crate::lints::MappingToUnit;
2+
use crate::{LateContext, LateLintPass, LintContext};
3+
4+
use rustc_hir::{Expr, ExprKind, HirId, Stmt, StmtKind};
5+
use rustc_middle::{
6+
query::Key,
7+
ty::{self, Ty},
8+
};
9+
10+
declare_lint! {
11+
/// The `map_unit_fn` lint checks for `Iterator::map` receive
12+
/// a callable that returns `()`.
13+
///
14+
/// ### Example
15+
///
16+
/// ```rust
17+
/// fn foo(items: &mut Vec<u8>) {
18+
/// items.sort();
19+
/// }
20+
///
21+
/// fn main() {
22+
/// let mut x: Vec<Vec<u8>> = vec![
23+
/// vec![0, 2, 1],
24+
/// vec![5, 4, 3],
25+
/// ];
26+
/// x.iter_mut().map(foo);
27+
/// }
28+
/// ```
29+
///
30+
/// {{produces}}
31+
///
32+
/// ### Explanation
33+
///
34+
/// Mapping to `()` is almost always a mistake.
35+
pub MAP_UNIT_FN,
36+
Warn,
37+
"`Iterator::map` call that discard the iterator's values"
38+
}
39+
40+
declare_lint_pass!(MapUnitFn => [MAP_UNIT_FN]);
41+
42+
impl<'tcx> LateLintPass<'tcx> for MapUnitFn {
43+
fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &Stmt<'_>) {
44+
if stmt.span.from_expansion() {
45+
return;
46+
}
47+
48+
if let StmtKind::Semi(expr) = stmt.kind {
49+
if let ExprKind::MethodCall(path, receiver, args, span) = expr.kind {
50+
if path.ident.name.as_str() == "map" {
51+
if receiver.span.from_expansion()
52+
|| args.iter().any(|e| e.span.from_expansion())
53+
|| !is_impl_slice(cx, receiver)
54+
|| !is_diagnostic_name(cx, expr.hir_id, "IteratorMap")
55+
{
56+
return;
57+
}
58+
let arg_ty = cx.typeck_results().expr_ty(&args[0]);
59+
if let ty::FnDef(id, _) = arg_ty.kind() {
60+
let fn_ty = cx.tcx.fn_sig(id).skip_binder();
61+
let ret_ty = fn_ty.output().skip_binder();
62+
if is_unit_type(ret_ty) {
63+
cx.emit_spanned_lint(
64+
MAP_UNIT_FN,
65+
span,
66+
MappingToUnit {
67+
function_label: cx.tcx.span_of_impl(*id).unwrap(),
68+
argument_label: args[0].span,
69+
map_label: arg_ty.default_span(cx.tcx),
70+
suggestion: path.ident.span,
71+
replace: "for_each".to_string(),
72+
},
73+
)
74+
}
75+
} else if let ty::Closure(id, subs) = arg_ty.kind() {
76+
let cl_ty = subs.as_closure().sig();
77+
let ret_ty = cl_ty.output().skip_binder();
78+
if is_unit_type(ret_ty) {
79+
cx.emit_spanned_lint(
80+
MAP_UNIT_FN,
81+
span,
82+
MappingToUnit {
83+
function_label: cx.tcx.span_of_impl(*id).unwrap(),
84+
argument_label: args[0].span,
85+
map_label: arg_ty.default_span(cx.tcx),
86+
suggestion: path.ident.span,
87+
replace: "for_each".to_string(),
88+
},
89+
)
90+
}
91+
}
92+
}
93+
}
94+
}
95+
}
96+
}
97+
98+
fn is_impl_slice(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
99+
if let Some(method_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id) {
100+
if let Some(impl_id) = cx.tcx.impl_of_method(method_id) {
101+
return cx.tcx.type_of(impl_id).skip_binder().is_slice();
102+
}
103+
}
104+
false
105+
}
106+
107+
fn is_unit_type(ty: Ty<'_>) -> bool {
108+
ty.is_unit() || ty.is_never()
109+
}
110+
111+
fn is_diagnostic_name(cx: &LateContext<'_>, id: HirId, name: &str) -> bool {
112+
if let Some(def_id) = cx.typeck_results().type_dependent_def_id(id) {
113+
if let Some(item) = cx.tcx.get_diagnostic_name(def_id) {
114+
if item.as_str() == name {
115+
return true;
116+
}
117+
}
118+
}
119+
false
120+
}

library/core/src/iter/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@
278278
//!
279279
//! ```
280280
//! # #![allow(unused_must_use)]
281+
//! # #![cfg_attr(not(bootstrap), allow(map_unit_fn))]
281282
//! let v = vec![1, 2, 3, 4, 5];
282283
//! v.iter().map(|x| println!("{x}"));
283284
//! ```

library/core/src/iter/traits/iterator.rs

+1
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,7 @@ pub trait Iterator {
789789
/// println!("{x}");
790790
/// }
791791
/// ```
792+
#[rustc_diagnostic_item = "IteratorMap"]
792793
#[inline]
793794
#[stable(feature = "rust1", since = "1.0.0")]
794795
#[rustc_do_not_const_check]

tests/ui/lint/issue-106991.rs

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
fn foo(items: &mut Vec<u8>) {
2+
items.sort();
3+
}
4+
5+
fn bar() -> impl Iterator<Item = i32> {
6+
//~^ ERROR expected `foo` to be a fn item that returns `i32`, but it returns `()` [E0271]
7+
let mut x: Vec<Vec<u8>> = vec![vec![0, 2, 1], vec![5, 4, 3]];
8+
x.iter_mut().map(foo)
9+
}
10+
11+
fn main() {
12+
bar();
13+
}

tests/ui/lint/issue-106991.stderr

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
error[E0271]: expected `foo` to be a fn item that returns `i32`, but it returns `()`
2+
--> $DIR/issue-106991.rs:5:13
3+
|
4+
LL | fn bar() -> impl Iterator<Item = i32> {
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `()`, found `i32`
6+
|
7+
= note: required for `Map<std::slice::IterMut<'_, Vec<u8>>, for<'a> fn(&'a mut Vec<u8>) {foo}>` to implement `Iterator`
8+
9+
error: aborting due to previous error
10+
11+
For more information about this error, try `rustc --explain E0271`.

tests/ui/lint/lint_map_unit_fn.rs

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#![deny(map_unit_fn)]
2+
3+
fn foo(items: &mut Vec<u8>) {
4+
items.sort();
5+
}
6+
7+
fn main() {
8+
let mut x: Vec<Vec<u8>> = vec![vec![0, 2, 1], vec![5, 4, 3]];
9+
x.iter_mut().map(foo);
10+
//~^ ERROR `Iterator::map` call that discard the iterator's values
11+
x.iter_mut().map(|items| {
12+
//~^ ERROR `Iterator::map` call that discard the iterator's values
13+
items.sort();
14+
});
15+
let f = |items: &mut Vec<u8>| {
16+
items.sort();
17+
};
18+
x.iter_mut().map(f);
19+
//~^ ERROR `Iterator::map` call that discard the iterator's values
20+
}

tests/ui/lint/lint_map_unit_fn.stderr

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
error: `Iterator::map` call that discard the iterator's values
2+
--> $DIR/lint_map_unit_fn.rs:9:18
3+
|
4+
LL | fn foo(items: &mut Vec<u8>) {
5+
| --------------------------- this function returns `()`, which is likely not what you wanted
6+
...
7+
LL | x.iter_mut().map(foo);
8+
| ^^^^---^
9+
| | |
10+
| | called `Iterator::map` with callable that returns `()`
11+
| after this call to map, the resulting iterator is `impl Iterator<Item = ()>`, which means the only information carried by the iterator is the number of items
12+
|
13+
= note: `Iterator::map`, like many of the methods on `Iterator`, gets executed lazily, meaning that its effects won't be visible until it is iterated
14+
note: the lint level is defined here
15+
--> $DIR/lint_map_unit_fn.rs:1:9
16+
|
17+
LL | #![deny(map_unit_fn)]
18+
| ^^^^^^^^^^^
19+
help: you might have meant to use `Iterator::for_each`
20+
|
21+
LL | x.iter_mut().for_each(foo);
22+
| ~~~~~~~~
23+
24+
error: `Iterator::map` call that discard the iterator's values
25+
--> $DIR/lint_map_unit_fn.rs:11:18
26+
|
27+
LL | x.iter_mut().map(|items| {
28+
| ^ -------
29+
| | |
30+
| ____________________|___this function returns `()`, which is likely not what you wanted
31+
| | __________________|
32+
| | |
33+
LL | | |
34+
LL | | | items.sort();
35+
LL | | | });
36+
| | | -^ after this call to map, the resulting iterator is `impl Iterator<Item = ()>`, which means the only information carried by the iterator is the number of items
37+
| | |_____||
38+
| |_______|
39+
| called `Iterator::map` with callable that returns `()`
40+
|
41+
= note: `Iterator::map`, like many of the methods on `Iterator`, gets executed lazily, meaning that its effects won't be visible until it is iterated
42+
help: you might have meant to use `Iterator::for_each`
43+
|
44+
LL | x.iter_mut().for_each(|items| {
45+
| ~~~~~~~~
46+
47+
error: `Iterator::map` call that discard the iterator's values
48+
--> $DIR/lint_map_unit_fn.rs:18:18
49+
|
50+
LL | let f = |items: &mut Vec<u8>| {
51+
| --------------------- this function returns `()`, which is likely not what you wanted
52+
...
53+
LL | x.iter_mut().map(f);
54+
| ^^^^-^
55+
| | |
56+
| | called `Iterator::map` with callable that returns `()`
57+
| after this call to map, the resulting iterator is `impl Iterator<Item = ()>`, which means the only information carried by the iterator is the number of items
58+
|
59+
= note: `Iterator::map`, like many of the methods on `Iterator`, gets executed lazily, meaning that its effects won't be visible until it is iterated
60+
help: you might have meant to use `Iterator::for_each`
61+
|
62+
LL | x.iter_mut().for_each(f);
63+
| ~~~~~~~~
64+
65+
error: aborting due to 3 previous errors
66+

0 commit comments

Comments
 (0)