Skip to content

Commit 5ab16d1

Browse files
fix(let_unit_value): create the suggestion "differentially" (#15788)
I.e, instead of constructing a whole new `LetStmt` to replace the old one, suggest to only change the part of the `LetStmt` that we don't like. This makes the suggestion less likely to introduce spurious changes, but also makes the diagnostic much nicer imo. Fixes #15771 by not adding a semicolon that the initial `LetStmt` didn't have Fixes #15784 by using the `init.span` with the correct `SyntaxContext` changelog: [`let_unit_value`]: don't add spurious semicolon changelog: [`let_unit_value`]: don't mangle macro calls
2 parents 98a92bf + f6dd112 commit 5ab16d1

File tree

4 files changed

+152
-62
lines changed

4 files changed

+152
-62
lines changed

clippy_lints/src/unit_types/let_unit_value.rs

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use clippy_utils::diagnostics::span_lint_and_then;
22
use clippy_utils::macros::{FormatArgsStorage, find_format_arg_expr, is_format_macro, root_macro_call_first_node};
3-
use clippy_utils::source::{indent_of, reindent_multiline, snippet_with_context};
3+
use clippy_utils::source::{snippet_indent, walk_span_to_context};
44
use clippy_utils::visitors::{for_each_local_assignment, for_each_value_source};
55
use core::ops::ControlFlow;
66
use rustc_ast::{FormatArgs, FormatArgumentKind};
@@ -74,10 +74,10 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, format_args: &FormatArgsStorag
7474
"this let-binding has unit value",
7575
|diag| {
7676
let mut suggestions = Vec::new();
77+
let init_new_span = walk_span_to_context(init.span, local.span.ctxt()).unwrap();
7778

7879
// Suggest omitting the `let` binding
79-
let mut app = Applicability::MachineApplicable;
80-
let snip = snippet_with_context(cx, init.span, local.span.ctxt(), "()", &mut app).0;
80+
let app = Applicability::MachineApplicable;
8181

8282
// If this is a binding pattern, we need to add suggestions to remove any usages
8383
// of the variable
@@ -89,35 +89,48 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, format_args: &FormatArgsStorag
8989
walk_body(&mut visitor, body);
9090

9191
let mut has_in_format_capture = false;
92-
suggestions.extend(visitor.spans.iter().filter_map(|span| match span {
92+
suggestions.extend(visitor.spans.into_iter().filter_map(|span| match span {
9393
MaybeInFormatCapture::Yes => {
9494
has_in_format_capture = true;
9595
None
9696
},
97-
MaybeInFormatCapture::No(span) => Some((*span, "()".to_string())),
97+
MaybeInFormatCapture::No(span) => Some((span, "()".to_string())),
9898
}));
9999

100100
if has_in_format_capture {
101+
// In a case like this:
102+
// ```
103+
// let unit = returns_unit();
104+
// eprintln!("{unit}");
105+
// ```
106+
// we can't remove the `unit` binding and replace its uses with a `()`,
107+
// because the `eprintln!` would break.
108+
//
109+
// So do the following instead:
110+
// ```
111+
// let unit = ();
112+
// returns_unit();
113+
// eprintln!("{unit}");
114+
// ```
115+
// TODO: find a less awkward way to do this
101116
suggestions.push((
102-
init.span,
103-
format!("();\n{}", reindent_multiline(&snip, false, indent_of(cx, local.span))),
117+
init_new_span.shrink_to_lo(),
118+
format!("();\n{}", snippet_indent(cx, local.span).as_deref().unwrap_or("")),
104119
));
105-
diag.multipart_suggestion(
106-
"replace variable usages with `()`",
107-
suggestions,
108-
Applicability::MachineApplicable,
109-
);
120+
diag.multipart_suggestion_verbose("replace variable usages with `()`", suggestions, app);
110121
return;
111122
}
112123
}
113124

114-
suggestions.push((local.span, format!("{snip};")));
125+
// let local = returns_unit();
126+
// ^^^^^^^^^^^^ remove this
127+
suggestions.push((local.span.until(init_new_span), String::new()));
115128
let message = if suggestions.len() == 1 {
116129
"omit the `let` binding"
117130
} else {
118131
"omit the `let` binding and replace variable usages with `()`"
119132
};
120-
diag.multipart_suggestion(message, suggestions, Applicability::MachineApplicable);
133+
diag.multipart_suggestion_verbose(message, suggestions, app);
121134
},
122135
);
123136
}
@@ -132,6 +145,12 @@ struct UnitVariableCollector<'a, 'tcx> {
132145
macro_call: Option<&'a FormatArgs>,
133146
}
134147

148+
/// Whether the unit variable is captured in a `format!`:
149+
///
150+
/// ```ignore
151+
/// let unit = ();
152+
/// eprintln!("{unit}");
153+
/// ```
135154
enum MaybeInFormatCapture {
136155
Yes,
137156
No(Span),

tests/ui/let_unit.fixed

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
#![warn(clippy::let_unit_value)]
2-
#![allow(unused, clippy::no_effect, clippy::needless_late_init, path_statements)]
2+
#![allow(
3+
clippy::no_effect,
4+
clippy::needless_late_init,
5+
path_statements,
6+
clippy::match_single_binding
7+
)]
38

49
macro_rules! let_and_return {
510
($n:expr) => {{
@@ -15,12 +20,12 @@ fn main() {
1520
if true {
1621
// do not lint this, since () is explicit
1722
let _a = ();
18-
let () = dummy();
23+
let () = returns_unit();
1924
let () = ();
20-
() = dummy();
25+
() = returns_unit();
2126
() = ();
2227
let _a: () = ();
23-
let _a: () = dummy();
28+
let _a: () = returns_unit();
2429
}
2530

2631
consume_units_with_for_loop(); // should be fine as well
@@ -30,7 +35,7 @@ fn main() {
3035
let_and_return!(()) // should be fine
3136
}
3237

33-
fn dummy() {}
38+
fn returns_unit() {}
3439

3540
// Related to issue #1964
3641
fn consume_units_with_for_loop() {
@@ -181,8 +186,6 @@ async fn issue10433() {
181186
pub async fn issue11502(a: ()) {}
182187

183188
pub fn issue12594() {
184-
fn returns_unit() {}
185-
186189
fn returns_result<T>(res: T) -> Result<T, ()> {
187190
Ok(res)
188191
}
@@ -199,13 +202,30 @@ pub fn issue12594() {
199202
}
200203
}
201204

205+
fn takes_unit(x: ()) {}
206+
202207
fn issue15061() {
203-
fn return_unit() {}
204-
fn do_something(x: ()) {}
208+
let res = ();
209+
returns_unit();
210+
//~^ let_unit_value
211+
takes_unit(());
212+
println!("{res:?}");
213+
}
214+
215+
fn issue15771() {
216+
match "Example String" {
217+
_ => returns_unit(),
218+
//~^ let_unit_value
219+
}
220+
221+
if true {}
222+
//~^ let_unit_value
223+
}
205224

225+
fn issue_15784() {
206226
let res = ();
207-
return_unit();
227+
eprintln!("I return unit");
208228
//~^ let_unit_value
209-
do_something(());
229+
takes_unit(());
210230
println!("{res:?}");
211231
}

tests/ui/let_unit.rs

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
#![warn(clippy::let_unit_value)]
2-
#![allow(unused, clippy::no_effect, clippy::needless_late_init, path_statements)]
2+
#![allow(
3+
clippy::no_effect,
4+
clippy::needless_late_init,
5+
path_statements,
6+
clippy::match_single_binding
7+
)]
38

49
macro_rules! let_and_return {
510
($n:expr) => {{
@@ -15,12 +20,12 @@ fn main() {
1520
if true {
1621
// do not lint this, since () is explicit
1722
let _a = ();
18-
let () = dummy();
23+
let () = returns_unit();
1924
let () = ();
20-
() = dummy();
25+
() = returns_unit();
2126
() = ();
2227
let _a: () = ();
23-
let _a: () = dummy();
28+
let _a: () = returns_unit();
2429
}
2530

2631
consume_units_with_for_loop(); // should be fine as well
@@ -30,7 +35,7 @@ fn main() {
3035
let_and_return!(()) // should be fine
3136
}
3237

33-
fn dummy() {}
38+
fn returns_unit() {}
3439

3540
// Related to issue #1964
3641
fn consume_units_with_for_loop() {
@@ -181,8 +186,6 @@ async fn issue10433() {
181186
pub async fn issue11502(a: ()) {}
182187

183188
pub fn issue12594() {
184-
fn returns_unit() {}
185-
186189
fn returns_result<T>(res: T) -> Result<T, ()> {
187190
Ok(res)
188191
}
@@ -199,12 +202,28 @@ pub fn issue12594() {
199202
}
200203
}
201204

205+
fn takes_unit(x: ()) {}
206+
202207
fn issue15061() {
203-
fn return_unit() {}
204-
fn do_something(x: ()) {}
208+
let res = returns_unit();
209+
//~^ let_unit_value
210+
takes_unit(res);
211+
println!("{res:?}");
212+
}
213+
214+
fn issue15771() {
215+
match "Example String" {
216+
_ => _ = returns_unit(),
217+
//~^ let_unit_value
218+
}
219+
220+
_ = if true {}
221+
//~^ let_unit_value
222+
}
205223

206-
let res = return_unit();
224+
fn issue_15784() {
225+
let res = eprintln!("I return unit");
207226
//~^ let_unit_value
208-
do_something(res);
227+
takes_unit(res);
209228
println!("{res:?}");
210229
}

tests/ui/let_unit.stderr

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
error: this let-binding has unit value
2-
--> tests/ui/let_unit.rs:11:5
2+
--> tests/ui/let_unit.rs:16:5
33
|
44
LL | let _x = println!("x");
5-
| ^^^^^^^^^^^^^^^^^^^^^^^ help: omit the `let` binding: `println!("x");`
5+
| ^^^^^^^^^^^^^^^^^^^^^^^
66
|
77
= note: `-D clippy::let-unit-value` implied by `-D warnings`
88
= help: to override `-D warnings` add `#[allow(clippy::let_unit_value)]`
9+
help: omit the `let` binding
10+
|
11+
LL - let _x = println!("x");
12+
LL + println!("x");
13+
|
914

1015
error: this let-binding has unit value
11-
--> tests/ui/let_unit.rs:60:5
16+
--> tests/ui/let_unit.rs:65:5
1217
|
1318
LL | / let _ = v
1419
LL | |
@@ -21,18 +26,12 @@ LL | | .unwrap();
2126
|
2227
help: omit the `let` binding
2328
|
24-
LL ~ v
25-
LL +
26-
LL + .into_iter()
27-
LL + .map(|i| i * 2)
28-
LL + .filter(|i| i.is_multiple_of(2))
29-
LL + .map(|_| ())
30-
LL + .next()
31-
LL + .unwrap();
29+
LL - let _ = v
30+
LL + v
3231
|
3332

3433
error: this let-binding has unit value
35-
--> tests/ui/let_unit.rs:110:5
34+
--> tests/ui/let_unit.rs:115:5
3635
|
3736
LL | / let x = match Some(0) {
3837
LL | |
@@ -45,17 +44,12 @@ LL | | };
4544
|
4645
help: omit the `let` binding
4746
|
48-
LL ~ match Some(0) {
49-
LL +
50-
LL + None => f2(1),
51-
LL + Some(0) => f(),
52-
LL + Some(1) => f2(3),
53-
LL + Some(_) => (),
54-
LL + };
47+
LL - let x = match Some(0) {
48+
LL + match Some(0) {
5549
|
5650

5751
error: this let-binding has unit value
58-
--> tests/ui/let_unit.rs:192:9
52+
--> tests/ui/let_unit.rs:195:9
5953
|
6054
LL | let res = returns_unit();
6155
| ^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -69,18 +63,56 @@ LL ~ returns_result(()).unwrap();
6963
|
7064

7165
error: this let-binding has unit value
72-
--> tests/ui/let_unit.rs:206:5
66+
--> tests/ui/let_unit.rs:208:5
67+
|
68+
LL | let res = returns_unit();
69+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
70+
|
71+
help: replace variable usages with `()`
72+
|
73+
LL ~ let res = ();
74+
LL ~ returns_unit();
75+
LL |
76+
LL ~ takes_unit(());
77+
|
78+
79+
error: this let-binding has unit value
80+
--> tests/ui/let_unit.rs:216:14
81+
|
82+
LL | _ => _ = returns_unit(),
83+
| ^^^^^^^^^^^^^^^^^^
84+
|
85+
help: omit the `let` binding
86+
|
87+
LL - _ => _ = returns_unit(),
88+
LL + _ => returns_unit(),
89+
|
90+
91+
error: this let-binding has unit value
92+
--> tests/ui/let_unit.rs:220:5
93+
|
94+
LL | _ = if true {}
95+
| ^^^^^^^^^^^^^^
96+
|
97+
help: omit the `let` binding
98+
|
99+
LL - _ = if true {}
100+
LL + if true {}
101+
|
102+
103+
error: this let-binding has unit value
104+
--> tests/ui/let_unit.rs:225:5
73105
|
74-
LL | let res = return_unit();
75-
| ^^^^^^^^^^^^^^^^^^^^^^^^
106+
LL | let res = eprintln!("I return unit");
107+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
76108
|
77109
help: replace variable usages with `()`
78110
|
79111
LL ~ let res = ();
80-
LL ~ return_unit();
112+
LL ~ eprintln!("I return unit");
81113
LL |
82-
LL ~ do_something(());
114+
LL ~ takes_unit(());
83115
|
84116

85-
error: aborting due to 5 previous errors
117+
error: aborting due to 8 previous errors
86118

0 commit comments

Comments
 (0)