Skip to content

Commit 180adb3

Browse files
Fix unit_arg suggests wrongly for Default::default (#14881)
Closes rust-lang/rust-clippy#14857 changelog: [`unit_arg`] fix wrong suggestion for `Default::default`
2 parents ce8d263 + 8029208 commit 180adb3

File tree

11 files changed

+410
-161
lines changed

11 files changed

+410
-161
lines changed

clippy_lints/src/default.rs

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
use clippy_utils::diagnostics::{span_lint_and_note, span_lint_and_sugg};
22
use clippy_utils::source::snippet_with_context;
33
use clippy_utils::ty::{has_drop, is_copy};
4-
use clippy_utils::{contains_name, get_parent_expr, in_automatically_derived, is_from_proc_macro};
4+
use clippy_utils::{contains_name, get_parent_expr, in_automatically_derived, is_expr_default, is_from_proc_macro};
55
use rustc_data_structures::fx::FxHashSet;
66
use rustc_errors::Applicability;
7-
use rustc_hir::def::Res;
87
use rustc_hir::{Block, Expr, ExprKind, PatKind, QPath, Stmt, StmtKind, StructTailExpr};
98
use rustc_lint::{LateContext, LateLintPass};
109
use rustc_middle::ty;
@@ -129,7 +128,7 @@ impl<'tcx> LateLintPass<'tcx> for Default {
129128
// only take bindings to identifiers
130129
&& let PatKind::Binding(_, binding_id, ident, _) = local.pat.kind
131130
// only when assigning `... = Default::default()`
132-
&& is_expr_default(expr, cx)
131+
&& is_expr_default(cx, expr)
133132
&& let binding_type = cx.typeck_results().node_type(binding_id)
134133
&& let ty::Adt(adt, args) = *binding_type.kind()
135134
&& adt.is_struct()
@@ -251,19 +250,6 @@ impl<'tcx> LateLintPass<'tcx> for Default {
251250
}
252251
}
253252

254-
/// Checks if the given expression is the `default` method belonging to the `Default` trait.
255-
fn is_expr_default<'tcx>(expr: &'tcx Expr<'tcx>, cx: &LateContext<'tcx>) -> bool {
256-
if let ExprKind::Call(fn_expr, []) = &expr.kind
257-
&& let ExprKind::Path(qpath) = &fn_expr.kind
258-
&& let Res::Def(_, def_id) = cx.qpath_res(qpath, fn_expr.hir_id)
259-
{
260-
// right hand side of assignment is `Default::default`
261-
cx.tcx.is_diagnostic_item(sym::default_fn, def_id)
262-
} else {
263-
false
264-
}
265-
}
266-
267253
/// Returns the reassigned field and the assigning expression (right-hand side of assign).
268254
fn field_reassigned_by_stmt<'tcx>(this: &Stmt<'tcx>, binding_name: Symbol) -> Option<(Ident, &'tcx Expr<'tcx>)> {
269255
if let StmtKind::Semi(later_expr) = this.kind

clippy_lints/src/unit_types/unit_arg.rs

Lines changed: 91 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
use std::iter;
2+
13
use clippy_utils::diagnostics::span_lint_and_then;
2-
use clippy_utils::is_from_proc_macro;
3-
use clippy_utils::source::{SourceText, SpanRangeExt, indent_of, reindent_multiline};
4+
use clippy_utils::source::{SpanRangeExt, indent_of, reindent_multiline};
5+
use clippy_utils::sugg::Sugg;
6+
use clippy_utils::ty::expr_type_is_certain;
7+
use clippy_utils::{is_expr_default, is_from_proc_macro};
48
use rustc_errors::Applicability;
59
use rustc_hir::{Block, Expr, ExprKind, MatchSource, Node, StmtKind};
610
use rustc_lint::LateContext;
11+
use rustc_span::SyntaxContext;
712

813
use super::{UNIT_ARG, utils};
914

@@ -59,7 +64,7 @@ fn is_questionmark_desugar_marked_call(expr: &Expr<'_>) -> bool {
5964
}
6065
}
6166

62-
fn lint_unit_args(cx: &LateContext<'_>, expr: &Expr<'_>, args_to_recover: &[&Expr<'_>]) {
67+
fn lint_unit_args<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, args_to_recover: &[&'tcx Expr<'tcx>]) {
6368
let mut applicability = Applicability::MachineApplicable;
6469
let (singular, plural) = if args_to_recover.len() > 1 {
6570
("", "s")
@@ -100,34 +105,41 @@ fn lint_unit_args(cx: &LateContext<'_>, expr: &Expr<'_>, args_to_recover: &[&Exp
100105

101106
let arg_snippets: Vec<_> = args_to_recover
102107
.iter()
103-
.filter_map(|arg| arg.span.get_source_text(cx))
108+
// If the argument is from an expansion and is a `Default::default()`, we skip it
109+
.filter(|arg| !arg.span.from_expansion() || !is_expr_default_nested(cx, arg))
110+
.filter_map(|arg| get_expr_snippet(cx, arg))
104111
.collect();
105-
let arg_snippets_without_empty_blocks: Vec<_> = args_to_recover
112+
113+
// If the argument is an empty block or `Default::default()`, we can replace it with `()`.
114+
let arg_snippets_without_redundant_exprs: Vec<_> = args_to_recover
106115
.iter()
107-
.filter(|arg| !is_empty_block(arg))
108-
.filter_map(|arg| arg.span.get_source_text(cx))
116+
.filter(|arg| !is_expr_default_nested(cx, arg) && (arg.span.from_expansion() || !is_empty_block(arg)))
117+
.filter_map(|arg| get_expr_snippet_with_type_certainty(cx, arg))
109118
.collect();
110119

111120
if let Some(call_snippet) = expr.span.get_source_text(cx) {
112-
let sugg = fmt_stmts_and_call(
113-
cx,
114-
expr,
115-
&call_snippet,
116-
&arg_snippets,
117-
&arg_snippets_without_empty_blocks,
118-
);
119-
120-
if arg_snippets_without_empty_blocks.is_empty() {
121+
if arg_snippets_without_redundant_exprs.is_empty()
122+
&& let suggestions = args_to_recover
123+
.iter()
124+
.filter(|arg| !arg.span.from_expansion() || !is_expr_default_nested(cx, arg))
125+
.map(|arg| (arg.span.parent_callsite().unwrap_or(arg.span), "()".to_string()))
126+
.collect::<Vec<_>>()
127+
&& !suggestions.is_empty()
128+
{
121129
db.multipart_suggestion(
122130
format!("use {singular}unit literal{plural} instead"),
123-
args_to_recover
124-
.iter()
125-
.map(|arg| (arg.span, "()".to_string()))
126-
.collect::<Vec<_>>(),
131+
suggestions,
127132
applicability,
128133
);
129134
} else {
130-
let plural = arg_snippets_without_empty_blocks.len() > 1;
135+
let plural = arg_snippets_without_redundant_exprs.len() > 1;
136+
let sugg = fmt_stmts_and_call(
137+
cx,
138+
expr,
139+
&call_snippet,
140+
arg_snippets,
141+
arg_snippets_without_redundant_exprs,
142+
);
131143
let empty_or_s = if plural { "s" } else { "" };
132144
let it_or_them = if plural { "them" } else { "it" };
133145
db.span_suggestion(
@@ -144,6 +156,55 @@ fn lint_unit_args(cx: &LateContext<'_>, expr: &Expr<'_>, args_to_recover: &[&Exp
144156
);
145157
}
146158

159+
fn is_expr_default_nested<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> bool {
160+
is_expr_default(cx, expr)
161+
|| matches!(expr.kind, ExprKind::Block(block, _)
162+
if block.expr.is_some() && is_expr_default_nested(cx, block.expr.unwrap()))
163+
}
164+
165+
enum MaybeTypeUncertain<'tcx> {
166+
Certain(Sugg<'tcx>),
167+
Uncertain(Sugg<'tcx>),
168+
}
169+
170+
impl From<MaybeTypeUncertain<'_>> for String {
171+
fn from(value: MaybeTypeUncertain<'_>) -> Self {
172+
match value {
173+
MaybeTypeUncertain::Certain(sugg) => sugg.to_string(),
174+
MaybeTypeUncertain::Uncertain(sugg) => format!("let _: () = {sugg}"),
175+
}
176+
}
177+
}
178+
179+
fn get_expr_snippet<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<Sugg<'tcx>> {
180+
let mut app = Applicability::MachineApplicable;
181+
let snip = Sugg::hir_with_context(cx, expr, SyntaxContext::root(), "..", &mut app);
182+
if app != Applicability::MachineApplicable {
183+
return None;
184+
}
185+
186+
Some(snip)
187+
}
188+
189+
fn get_expr_snippet_with_type_certainty<'tcx>(
190+
cx: &LateContext<'tcx>,
191+
expr: &'tcx Expr<'tcx>,
192+
) -> Option<MaybeTypeUncertain<'tcx>> {
193+
get_expr_snippet(cx, expr).map(|snip| {
194+
// If the type of the expression is certain, we can use it directly.
195+
// Otherwise, we wrap it in a `let _: () = ...` to ensure the type is correct.
196+
if !expr_type_is_certain(cx, expr) && !is_block_with_no_expr(expr) {
197+
MaybeTypeUncertain::Uncertain(snip)
198+
} else {
199+
MaybeTypeUncertain::Certain(snip)
200+
}
201+
})
202+
}
203+
204+
fn is_block_with_no_expr(expr: &Expr<'_>) -> bool {
205+
matches!(expr.kind, ExprKind::Block(Block { expr: None, .. }, _))
206+
}
207+
147208
fn is_empty_block(expr: &Expr<'_>) -> bool {
148209
matches!(
149210
expr.kind,
@@ -162,23 +223,20 @@ fn fmt_stmts_and_call(
162223
cx: &LateContext<'_>,
163224
call_expr: &Expr<'_>,
164225
call_snippet: &str,
165-
args_snippets: &[SourceText],
166-
non_empty_block_args_snippets: &[SourceText],
226+
args_snippets: Vec<Sugg<'_>>,
227+
non_empty_block_args_snippets: Vec<MaybeTypeUncertain<'_>>,
167228
) -> String {
168229
let call_expr_indent = indent_of(cx, call_expr.span).unwrap_or(0);
169-
let call_snippet_with_replacements = args_snippets
170-
.iter()
171-
.fold(call_snippet.to_owned(), |acc, arg| acc.replacen(arg.as_ref(), "()", 1));
230+
let call_snippet_with_replacements = args_snippets.into_iter().fold(call_snippet.to_owned(), |acc, arg| {
231+
acc.replacen(&arg.to_string(), "()", 1)
232+
});
172233

173-
let mut stmts_and_call = non_empty_block_args_snippets
174-
.iter()
175-
.map(|it| it.as_ref().to_owned())
176-
.collect::<Vec<_>>();
177-
stmts_and_call.push(call_snippet_with_replacements);
178-
stmts_and_call = stmts_and_call
234+
let stmts_and_call = non_empty_block_args_snippets
179235
.into_iter()
236+
.map(Into::into)
237+
.chain(iter::once(call_snippet_with_replacements))
180238
.map(|v| reindent_multiline(&v, true, Some(call_expr_indent)))
181-
.collect();
239+
.collect::<Vec<_>>();
182240

183241
let mut stmts_and_call_snippet = stmts_and_call.join(&format!("{}{}", ";\n", " ".repeat(call_expr_indent)));
184242
// expr is not in a block statement or result expression position, wrap in a block

clippy_utils/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3473,3 +3473,15 @@ pub fn desugar_await<'tcx>(expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> {
34733473
None
34743474
}
34753475
}
3476+
3477+
/// Checks if the given expression is a call to `Default::default()`.
3478+
pub fn is_expr_default<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> bool {
3479+
if let ExprKind::Call(fn_expr, []) = &expr.kind
3480+
&& let ExprKind::Path(qpath) = &fn_expr.kind
3481+
&& let Res::Def(_, def_id) = cx.qpath_res(qpath, fn_expr.hir_id)
3482+
{
3483+
cx.tcx.is_diagnostic_item(sym::default_fn, def_id)
3484+
} else {
3485+
false
3486+
}
3487+
}

tests/ui/unit_arg.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,27 @@ fn main() {
151151
bad();
152152
ok();
153153
}
154+
155+
fn issue14857() {
156+
let fn_take_unit = |_: ()| {};
157+
fn some_other_fn(_: &i32) {}
158+
159+
macro_rules! mac {
160+
(def) => {
161+
Default::default()
162+
};
163+
(func $f:expr) => {
164+
$f()
165+
};
166+
(nonempty_block $e:expr) => {{
167+
some_other_fn(&$e);
168+
$e
169+
}};
170+
}
171+
fn_take_unit(mac!(def));
172+
//~^ unit_arg
173+
fn_take_unit(mac!(func Default::default));
174+
//~^ unit_arg
175+
fn_take_unit(mac!(nonempty_block Default::default()));
176+
//~^ unit_arg
177+
}

tests/ui/unit_arg.stderr

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,5 +199,26 @@ LL ~ foo(1);
199199
LL + Some(())
200200
|
201201

202-
error: aborting due to 10 previous errors
202+
error: passing a unit value to a function
203+
--> tests/ui/unit_arg.rs:171:5
204+
|
205+
LL | fn_take_unit(mac!(def));
206+
| ^^^^^^^^^^^^^^^^^^^^^^^
207+
|
208+
209+
error: passing a unit value to a function
210+
--> tests/ui/unit_arg.rs:173:5
211+
|
212+
LL | fn_take_unit(mac!(func Default::default));
213+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
214+
|
215+
216+
error: passing a unit value to a function
217+
--> tests/ui/unit_arg.rs:175:5
218+
|
219+
LL | fn_take_unit(mac!(nonempty_block Default::default()));
220+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
221+
|
222+
223+
error: aborting due to 13 previous errors
203224

tests/ui/unit_arg_empty_blocks.fixed

Lines changed: 0 additions & 34 deletions
This file was deleted.

tests/ui/unit_arg_empty_blocks.rs

Lines changed: 0 additions & 31 deletions
This file was deleted.

0 commit comments

Comments
 (0)