Skip to content

Commit ff0a368

Browse files
committed
Auto merge of #8520 - J-ZhengLi:issue8506, r=xFrednet
fix suggestion on `[map_flatten]` being cropped causing possible information loss fixes #8506 Multi-line suggestion given by the lint is missing its bottom part, which could potentially contains useful information about the fix. --- changelog: [`map_flatten`]: Long suggestions will now be splitup into two help messages
2 parents 47b93ea + 5b6295d commit ff0a368

File tree

9 files changed

+395
-124
lines changed

9 files changed

+395
-124
lines changed
Lines changed: 54 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,73 @@
1-
use clippy_utils::diagnostics::span_lint_and_sugg;
1+
use clippy_utils::diagnostics::span_lint_and_sugg_for_edges;
22
use clippy_utils::is_trait_method;
3-
use clippy_utils::source::snippet;
3+
use clippy_utils::source::snippet_with_applicability;
44
use clippy_utils::ty::is_type_diagnostic_item;
55
use rustc_errors::Applicability;
6-
use rustc_hir as hir;
6+
use rustc_hir::Expr;
77
use rustc_lint::LateContext;
88
use rustc_middle::ty;
9-
use rustc_span::symbol::sym;
9+
use rustc_span::{symbol::sym, Span};
1010

1111
use super::MAP_FLATTEN;
1212

1313
/// lint use of `map().flatten()` for `Iterators` and 'Options'
14-
pub(super) fn check<'tcx>(
15-
cx: &LateContext<'tcx>,
16-
expr: &'tcx hir::Expr<'_>,
17-
recv: &'tcx hir::Expr<'_>,
18-
map_arg: &'tcx hir::Expr<'_>,
19-
) {
20-
// lint if caller of `.map().flatten()` is an Iterator
21-
if is_trait_method(cx, expr, sym::Iterator) {
22-
let map_closure_ty = cx.typeck_results().expr_ty(map_arg);
23-
let is_map_to_option = match map_closure_ty.kind() {
24-
ty::Closure(_, _) | ty::FnDef(_, _) | ty::FnPtr(_) => {
25-
let map_closure_sig = match map_closure_ty.kind() {
26-
ty::Closure(_, substs) => substs.as_closure().sig(),
27-
_ => map_closure_ty.fn_sig(cx.tcx),
28-
};
29-
let map_closure_return_ty = cx.tcx.erase_late_bound_regions(map_closure_sig.output());
30-
is_type_diagnostic_item(cx, map_closure_return_ty, sym::Option)
31-
},
32-
_ => false,
33-
};
34-
35-
let method_to_use = if is_map_to_option {
36-
// `(...).map(...)` has type `impl Iterator<Item=Option<...>>
37-
"filter_map"
38-
} else {
39-
// `(...).map(...)` has type `impl Iterator<Item=impl Iterator<...>>
40-
"flat_map"
41-
};
42-
let func_snippet = snippet(cx, map_arg.span, "..");
43-
let hint = format!(".{0}({1})", method_to_use, func_snippet);
44-
span_lint_and_sugg(
14+
pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, map_arg: &Expr<'_>, map_span: Span) {
15+
if let Some((caller_ty_name, method_to_use)) = try_get_caller_ty_name_and_method_name(cx, expr, recv, map_arg) {
16+
let mut applicability = Applicability::MachineApplicable;
17+
let help_msgs = [
18+
&format!("try replacing `map` with `{}`", method_to_use),
19+
"and remove the `.flatten()`",
20+
];
21+
let closure_snippet = snippet_with_applicability(cx, map_arg.span, "..", &mut applicability);
22+
span_lint_and_sugg_for_edges(
4523
cx,
4624
MAP_FLATTEN,
47-
expr.span.with_lo(recv.span.hi()),
48-
"called `map(..).flatten()` on an `Iterator`",
49-
&format!("try using `{}` instead", method_to_use),
50-
hint,
51-
Applicability::MachineApplicable,
25+
expr.span.with_lo(map_span.lo()),
26+
&format!("called `map(..).flatten()` on `{}`", caller_ty_name),
27+
&help_msgs,
28+
format!("{}({})", method_to_use, closure_snippet),
29+
applicability,
5230
);
5331
}
32+
}
5433

55-
// lint if caller of `.map().flatten()` is an Option or Result
56-
let caller_type = match cx.typeck_results().expr_ty(recv).kind() {
57-
ty::Adt(adt, _) => {
34+
fn try_get_caller_ty_name_and_method_name(
35+
cx: &LateContext<'_>,
36+
expr: &Expr<'_>,
37+
caller_expr: &Expr<'_>,
38+
map_arg: &Expr<'_>,
39+
) -> Option<(&'static str, &'static str)> {
40+
if is_trait_method(cx, expr, sym::Iterator) {
41+
if is_map_to_option(cx, map_arg) {
42+
// `(...).map(...)` has type `impl Iterator<Item=Option<...>>
43+
Some(("Iterator", "filter_map"))
44+
} else {
45+
// `(...).map(...)` has type `impl Iterator<Item=impl Iterator<...>>
46+
Some(("Iterator", "flat_map"))
47+
}
48+
} else {
49+
if let ty::Adt(adt, _) = cx.typeck_results().expr_ty(caller_expr).kind() {
5850
if cx.tcx.is_diagnostic_item(sym::Option, adt.did()) {
59-
"Option"
51+
return Some(("Option", "and_then"));
6052
} else if cx.tcx.is_diagnostic_item(sym::Result, adt.did()) {
61-
"Result"
62-
} else {
63-
return;
53+
return Some(("Result", "and_then"));
6454
}
65-
},
66-
_ => {
67-
return;
68-
},
69-
};
55+
}
56+
None
57+
}
58+
}
7059

71-
let func_snippet = snippet(cx, map_arg.span, "..");
72-
let hint = format!(".and_then({})", func_snippet);
73-
let lint_info = format!("called `map(..).flatten()` on an `{}`", caller_type);
74-
span_lint_and_sugg(
75-
cx,
76-
MAP_FLATTEN,
77-
expr.span.with_lo(recv.span.hi()),
78-
&lint_info,
79-
"try using `and_then` instead",
80-
hint,
81-
Applicability::MachineApplicable,
82-
);
60+
fn is_map_to_option(cx: &LateContext<'_>, map_arg: &Expr<'_>) -> bool {
61+
let map_closure_ty = cx.typeck_results().expr_ty(map_arg);
62+
match map_closure_ty.kind() {
63+
ty::Closure(_, _) | ty::FnDef(_, _) | ty::FnPtr(_) => {
64+
let map_closure_sig = match map_closure_ty.kind() {
65+
ty::Closure(_, substs) => substs.as_closure().sig(),
66+
_ => map_closure_ty.fn_sig(cx.tcx),
67+
};
68+
let map_closure_return_ty = cx.tcx.erase_late_bound_regions(map_closure_sig.output());
69+
is_type_diagnostic_item(cx, map_closure_return_ty, sym::Option)
70+
},
71+
_ => false,
72+
}
8373
}

clippy_lints/src/methods/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2377,7 +2377,7 @@ fn check_methods<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, msrv: Optio
23772377
flat_map_option::check(cx, expr, arg, span);
23782378
},
23792379
(name @ "flatten", args @ []) => match method_call(recv) {
2380-
Some(("map", [recv, map_arg], _)) => map_flatten::check(cx, expr, recv, map_arg),
2380+
Some(("map", [recv, map_arg], map_span)) => map_flatten::check(cx, expr, recv, map_arg, map_span),
23812381
Some(("cloned", [recv2], _)) => iter_overeager_cloned::check(cx, expr, recv2, name, args),
23822382
_ => {},
23832383
},

clippy_lints/src/utils/internal_lints/metadata_collector.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,15 @@ macro_rules! CONFIGURATION_VALUE_TEMPLATE {
8585
};
8686
}
8787

88-
const LINT_EMISSION_FUNCTIONS: [&[&str]; 7] = [
88+
const LINT_EMISSION_FUNCTIONS: [&[&str]; 8] = [
8989
&["clippy_utils", "diagnostics", "span_lint"],
9090
&["clippy_utils", "diagnostics", "span_lint_and_help"],
9191
&["clippy_utils", "diagnostics", "span_lint_and_note"],
9292
&["clippy_utils", "diagnostics", "span_lint_hir"],
9393
&["clippy_utils", "diagnostics", "span_lint_and_sugg"],
9494
&["clippy_utils", "diagnostics", "span_lint_and_then"],
9595
&["clippy_utils", "diagnostics", "span_lint_hir_and_then"],
96+
&["clippy_utils", "diagnostics", "span_lint_and_sugg_for_edges"],
9697
];
9798
const SUGGESTION_DIAGNOSTIC_BUILDER_METHODS: [(&str, bool); 9] = [
9899
("span_suggestion", false),

clippy_utils/src/diagnostics.rs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
//! Thank you!
99
//! ~The `INTERNAL_METADATA_COLLECTOR` lint
1010
11-
use rustc_errors::{Applicability, Diagnostic};
11+
use rustc_errors::{emitter::MAX_SUGGESTION_HIGHLIGHT_LINES, Applicability, Diagnostic};
1212
use rustc_hir::HirId;
1313
use rustc_lint::{LateContext, Lint, LintContext};
1414
use rustc_span::source_map::{MultiSpan, Span};
@@ -213,6 +213,90 @@ pub fn span_lint_and_sugg<'a, T: LintContext>(
213213
});
214214
}
215215

216+
/// Like [`span_lint_and_sugg`] with a focus on the edges. The output will either
217+
/// emit single span or multispan suggestion depending on the number of its lines.
218+
///
219+
/// If the given suggestion string has more lines than the maximum display length defined by
220+
/// [`MAX_SUGGESTION_HIGHLIGHT_LINES`][`rustc_errors::emitter::MAX_SUGGESTION_HIGHLIGHT_LINES`],
221+
/// this function will split the suggestion and span to showcase the change for the top and
222+
/// bottom edge of the code. For normal suggestions, in one display window, the help message
223+
/// will be combined with a colon.
224+
///
225+
/// Multipart suggestions like the one being created here currently cannot be
226+
/// applied by rustfix (See [rustfix#141](https://github.com/rust-lang/rustfix/issues/141)).
227+
/// Testing rustfix with this lint emission function might require a file with
228+
/// suggestions that can be fixed and those that can't. See
229+
/// [clippy#8520](https://github.com/rust-lang/rust-clippy/pull/8520/files) for
230+
/// an example and of this.
231+
///
232+
/// # Example for a long suggestion
233+
///
234+
/// ```text
235+
/// error: called `map(..).flatten()` on `Option`
236+
/// --> $DIR/map_flatten.rs:8:10
237+
/// |
238+
/// LL | .map(|x| {
239+
/// | __________^
240+
/// LL | | if x <= 5 {
241+
/// LL | | Some(x)
242+
/// LL | | } else {
243+
/// ... |
244+
/// LL | | })
245+
/// LL | | .flatten();
246+
/// | |__________________^
247+
/// |
248+
/// = note: `-D clippy::map-flatten` implied by `-D warnings`
249+
/// help: try replacing `map` with `and_then`
250+
/// |
251+
/// LL ~ .and_then(|x| {
252+
/// LL + if x <= 5 {
253+
/// LL + Some(x)
254+
/// |
255+
/// help: and remove the `.flatten()`
256+
/// |
257+
/// LL + None
258+
/// LL + }
259+
/// LL ~ });
260+
/// |
261+
/// ```
262+
pub fn span_lint_and_sugg_for_edges(
263+
cx: &LateContext<'_>,
264+
lint: &'static Lint,
265+
sp: Span,
266+
msg: &str,
267+
helps: &[&str; 2],
268+
sugg: String,
269+
applicability: Applicability,
270+
) {
271+
span_lint_and_then(cx, lint, sp, msg, |diag| {
272+
let sugg_lines_count = sugg.lines().count();
273+
if sugg_lines_count > MAX_SUGGESTION_HIGHLIGHT_LINES {
274+
let sm = cx.sess().source_map();
275+
if let (Ok(line_upper), Ok(line_bottom)) = (sm.lookup_line(sp.lo()), sm.lookup_line(sp.hi())) {
276+
let split_idx = MAX_SUGGESTION_HIGHLIGHT_LINES / 2;
277+
let span_upper = sm.span_until_char(sp.with_hi(line_upper.sf.lines[line_upper.line + split_idx]), '\n');
278+
let span_bottom = sp.with_lo(line_bottom.sf.lines[line_bottom.line - split_idx]);
279+
280+
let sugg_lines_vec = sugg.lines().collect::<Vec<&str>>();
281+
let sugg_upper = sugg_lines_vec[..split_idx].join("\n");
282+
let sugg_bottom = sugg_lines_vec[sugg_lines_count - split_idx..].join("\n");
283+
284+
diag.span_suggestion(span_upper, helps[0], sugg_upper, applicability);
285+
diag.span_suggestion(span_bottom, helps[1], sugg_bottom, applicability);
286+
287+
return;
288+
}
289+
}
290+
diag.span_suggestion_with_style(
291+
sp,
292+
&helps.join(", "),
293+
sugg,
294+
applicability,
295+
rustc_errors::SuggestionStyle::ShowAlways,
296+
);
297+
});
298+
}
299+
216300
/// Create a suggestion made from several `span → replacement`.
217301
///
218302
/// Note: in the JSON format (used by `compiletest_rs`), the help message will

tests/ui/map_flatten.rs

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,55 @@
1-
// run-rustfix
2-
3-
#![warn(clippy::all, clippy::pedantic)]
4-
#![allow(clippy::let_underscore_drop)]
5-
#![allow(clippy::missing_docs_in_private_items)]
6-
#![allow(clippy::map_identity)]
7-
#![allow(clippy::redundant_closure)]
8-
#![allow(clippy::unnecessary_wraps)]
1+
#![warn(clippy::map_flatten)]
92
#![feature(result_flattening)]
103

11-
fn main() {
12-
// mapping to Option on Iterator
13-
fn option_id(x: i8) -> Option<i8> {
14-
Some(x)
15-
}
16-
let option_id_ref: fn(i8) -> Option<i8> = option_id;
17-
let option_id_closure = |x| Some(x);
18-
let _: Vec<_> = vec![5_i8; 6].into_iter().map(option_id).flatten().collect();
19-
let _: Vec<_> = vec![5_i8; 6].into_iter().map(option_id_ref).flatten().collect();
20-
let _: Vec<_> = vec![5_i8; 6].into_iter().map(option_id_closure).flatten().collect();
21-
let _: Vec<_> = vec![5_i8; 6].into_iter().map(|x| x.checked_add(1)).flatten().collect();
4+
// issue #8506, multi-line
5+
#[rustfmt::skip]
6+
fn long_span() {
7+
let _: Option<i32> = Some(1)
8+
.map(|x| {
9+
if x <= 5 {
10+
Some(x)
11+
} else {
12+
None
13+
}
14+
})
15+
.flatten();
2216

23-
// mapping to Iterator on Iterator
24-
let _: Vec<_> = vec![5_i8; 6].into_iter().map(|x| 0..x).flatten().collect();
17+
let _: Result<i32, i32> = Ok(1)
18+
.map(|x| {
19+
if x == 1 {
20+
Ok(x)
21+
} else {
22+
Err(0)
23+
}
24+
})
25+
.flatten();
2526

26-
// mapping to Option on Option
27-
let _: Option<_> = (Some(Some(1))).map(|x| x).flatten();
27+
let result: Result<i32, i32> = Ok(2);
28+
fn do_something() { }
29+
let _: Result<i32, i32> = result
30+
.map(|res| {
31+
if res > 0 {
32+
do_something();
33+
Ok(res)
34+
} else {
35+
Err(0)
36+
}
37+
})
38+
.flatten();
39+
40+
let _: Vec<_> = vec![5_i8; 6]
41+
.into_iter()
42+
.map(|some_value| {
43+
if some_value > 3 {
44+
Some(some_value)
45+
} else {
46+
None
47+
}
48+
})
49+
.flatten()
50+
.collect();
51+
}
2852

29-
// mapping to Result on Result
30-
let _: Result<_, &str> = (Ok(Ok(1))).map(|x| x).flatten();
53+
fn main() {
54+
long_span();
3155
}

0 commit comments

Comments
 (0)