Skip to content

Commit 55efa96

Browse files
committed
Auto merge of #5931 - montrivo:unit-arg, r=flip1995
improve the suggestion of the lint `unit-arg` Fixes #5823 Fixes #6015 Changes ``` help: move the expression in front of the call... | 3 | g(); | help: ...and use a unit literal instead | 3 | o.map_or((), |i| f(i)) | ``` into ``` help: move the expression in front of the call and replace it with the unit literal `()` | 3 | g(); | o.map_or((), |i| f(i)) | ``` changelog: improve the suggestion of the lint `unit-arg`
2 parents 0e9769e + b220ddf commit 55efa96

File tree

5 files changed

+203
-137
lines changed

5 files changed

+203
-137
lines changed

clippy_lints/src/types.rs

+85-36
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use rustc_hir as hir;
1111
use rustc_hir::intravisit::{walk_body, walk_expr, walk_ty, FnKind, NestedVisitorMap, Visitor};
1212
use rustc_hir::{
1313
BinOpKind, Block, Body, Expr, ExprKind, FnDecl, FnRetTy, FnSig, GenericArg, GenericParamKind, HirId, ImplItem,
14-
ImplItemKind, Item, ItemKind, Lifetime, Local, MatchSource, MutTy, Mutability, QPath, Stmt, StmtKind, TraitFn,
15-
TraitItem, TraitItemKind, TyKind, UnOp,
14+
ImplItemKind, Item, ItemKind, Lifetime, Local, MatchSource, MutTy, Mutability, Node, QPath, Stmt, StmtKind,
15+
TraitFn, TraitItem, TraitItemKind, TyKind, UnOp,
1616
};
1717
use rustc_lint::{LateContext, LateLintPass, LintContext};
1818
use rustc_middle::hir::map::Map;
@@ -31,8 +31,8 @@ use crate::utils::paths;
3131
use crate::utils::{
3232
clip, comparisons, differing_macro_contexts, higher, in_constant, indent_of, int_bits, is_type_diagnostic_item,
3333
last_path_segment, match_def_path, match_path, method_chain_args, multispan_sugg, numeric_literal::NumericLiteral,
34-
qpath_res, sext, snippet, snippet_block_with_applicability, snippet_opt, snippet_with_applicability,
35-
snippet_with_macro_callsite, span_lint, span_lint_and_help, span_lint_and_sugg, span_lint_and_then, unsext,
34+
qpath_res, reindent_multiline, sext, snippet, snippet_opt, snippet_with_applicability, snippet_with_macro_callsite,
35+
span_lint, span_lint_and_help, span_lint_and_sugg, span_lint_and_then, unsext,
3636
};
3737

3838
declare_clippy_lint! {
@@ -805,6 +805,45 @@ impl<'tcx> LateLintPass<'tcx> for UnitArg {
805805
}
806806
}
807807

808+
fn fmt_stmts_and_call(
809+
cx: &LateContext<'_>,
810+
call_expr: &Expr<'_>,
811+
call_snippet: &str,
812+
args_snippets: &[impl AsRef<str>],
813+
non_empty_block_args_snippets: &[impl AsRef<str>],
814+
) -> String {
815+
let call_expr_indent = indent_of(cx, call_expr.span).unwrap_or(0);
816+
let call_snippet_with_replacements = args_snippets
817+
.iter()
818+
.fold(call_snippet.to_owned(), |acc, arg| acc.replacen(arg.as_ref(), "()", 1));
819+
820+
let mut stmts_and_call = non_empty_block_args_snippets
821+
.iter()
822+
.map(|it| it.as_ref().to_owned())
823+
.collect::<Vec<_>>();
824+
stmts_and_call.push(call_snippet_with_replacements);
825+
stmts_and_call = stmts_and_call
826+
.into_iter()
827+
.map(|v| reindent_multiline(v.into(), true, Some(call_expr_indent)).into_owned())
828+
.collect();
829+
830+
let mut stmts_and_call_snippet = stmts_and_call.join(&format!("{}{}", ";\n", " ".repeat(call_expr_indent)));
831+
// expr is not in a block statement or result expression position, wrap in a block
832+
let parent_node = cx.tcx.hir().find(cx.tcx.hir().get_parent_node(call_expr.hir_id));
833+
if !matches!(parent_node, Some(Node::Block(_))) && !matches!(parent_node, Some(Node::Stmt(_))) {
834+
let block_indent = call_expr_indent + 4;
835+
stmts_and_call_snippet =
836+
reindent_multiline(stmts_and_call_snippet.into(), true, Some(block_indent)).into_owned();
837+
stmts_and_call_snippet = format!(
838+
"{{\n{}{}\n{}}}",
839+
" ".repeat(block_indent),
840+
&stmts_and_call_snippet,
841+
" ".repeat(call_expr_indent)
842+
);
843+
}
844+
stmts_and_call_snippet
845+
}
846+
808847
fn lint_unit_args(cx: &LateContext<'_>, expr: &Expr<'_>, args_to_recover: &[&Expr<'_>]) {
809848
let mut applicability = Applicability::MachineApplicable;
810849
let (singular, plural) = if args_to_recover.len() > 1 {
@@ -847,43 +886,52 @@ fn lint_unit_args(cx: &LateContext<'_>, expr: &Expr<'_>, args_to_recover: &[&Exp
847886
Applicability::MaybeIncorrect,
848887
);
849888
or = "or ";
889+
applicability = Applicability::MaybeIncorrect;
850890
});
851-
let sugg = args_to_recover
891+
892+
let arg_snippets: Vec<String> = args_to_recover
893+
.iter()
894+
.filter_map(|arg| snippet_opt(cx, arg.span))
895+
.collect();
896+
let arg_snippets_without_empty_blocks: Vec<String> = args_to_recover
852897
.iter()
853898
.filter(|arg| !is_empty_block(arg))
854-
.enumerate()
855-
.map(|(i, arg)| {
856-
let indent = if i == 0 {
857-
0
858-
} else {
859-
indent_of(cx, expr.span).unwrap_or(0)
860-
};
861-
format!(
862-
"{}{};",
863-
" ".repeat(indent),
864-
snippet_block_with_applicability(cx, arg.span, "..", Some(expr.span), &mut applicability)
865-
)
866-
})
867-
.collect::<Vec<String>>();
868-
let mut and = "";
869-
if !sugg.is_empty() {
870-
let plural = if sugg.len() > 1 { "s" } else { "" };
871-
db.span_suggestion(
872-
expr.span.with_hi(expr.span.lo()),
873-
&format!("{}move the expression{} in front of the call...", or, plural),
874-
format!("{}\n", sugg.join("\n")),
875-
applicability,
899+
.filter_map(|arg| snippet_opt(cx, arg.span))
900+
.collect();
901+
902+
if let Some(call_snippet) = snippet_opt(cx, expr.span) {
903+
let sugg = fmt_stmts_and_call(
904+
cx,
905+
expr,
906+
&call_snippet,
907+
&arg_snippets,
908+
&arg_snippets_without_empty_blocks,
876909
);
877-
and = "...and "
910+
911+
if arg_snippets_without_empty_blocks.is_empty() {
912+
db.multipart_suggestion(
913+
&format!("use {}unit literal{} instead", singular, plural),
914+
args_to_recover
915+
.iter()
916+
.map(|arg| (arg.span, "()".to_string()))
917+
.collect::<Vec<_>>(),
918+
applicability,
919+
);
920+
} else {
921+
let plural = arg_snippets_without_empty_blocks.len() > 1;
922+
let empty_or_s = if plural { "s" } else { "" };
923+
let it_or_them = if plural { "them" } else { "it" };
924+
db.span_suggestion(
925+
expr.span,
926+
&format!(
927+
"{}move the expression{} in front of the call and replace {} with the unit literal `()`",
928+
or, empty_or_s, it_or_them
929+
),
930+
sugg,
931+
applicability,
932+
);
933+
}
878934
}
879-
db.multipart_suggestion(
880-
&format!("{}use {}unit literal{} instead", and, singular, plural),
881-
args_to_recover
882-
.iter()
883-
.map(|arg| (arg.span, "()".to_string()))
884-
.collect::<Vec<_>>(),
885-
applicability,
886-
);
887935
},
888936
);
889937
}
@@ -2058,6 +2106,7 @@ impl PartialOrd for FullInt {
20582106
})
20592107
}
20602108
}
2109+
20612110
impl Ord for FullInt {
20622111
#[must_use]
20632112
fn cmp(&self, other: &Self) -> Ordering {

clippy_lints/src/utils/mod.rs

+54-42
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub mod paths;
1919
pub mod ptr;
2020
pub mod sugg;
2121
pub mod usage;
22+
2223
pub use self::attrs::*;
2324
pub use self::diagnostics::*;
2425
pub use self::hir_utils::{both, eq_expr_value, over, SpanlessEq, SpanlessHash};
@@ -108,6 +109,7 @@ pub fn in_macro(span: Span) -> bool {
108109
false
109110
}
110111
}
112+
111113
// If the snippet is empty, it's an attribute that was inserted during macro
112114
// expansion and we want to ignore those, because they could come from external
113115
// sources that the user has no control over.
@@ -571,7 +573,7 @@ pub fn snippet_block<'a, T: LintContext>(
571573
) -> Cow<'a, str> {
572574
let snip = snippet(cx, span, default);
573575
let indent = indent_relative_to.and_then(|s| indent_of(cx, s));
574-
trim_multiline(snip, true, indent)
576+
reindent_multiline(snip, true, indent)
575577
}
576578

577579
/// Same as `snippet_block`, but adapts the applicability level by the rules of
@@ -585,7 +587,7 @@ pub fn snippet_block_with_applicability<'a, T: LintContext>(
585587
) -> Cow<'a, str> {
586588
let snip = snippet_with_applicability(cx, span, default, applicability);
587589
let indent = indent_relative_to.and_then(|s| indent_of(cx, s));
588-
trim_multiline(snip, true, indent)
590+
reindent_multiline(snip, true, indent)
589591
}
590592

591593
/// Returns a new Span that extends the original Span to the first non-whitespace char of the first
@@ -661,16 +663,16 @@ pub fn expr_block<'a, T: LintContext>(
661663
}
662664
}
663665

664-
/// Trim indentation from a multiline string with possibility of ignoring the
665-
/// first line.
666-
fn trim_multiline(s: Cow<'_, str>, ignore_first: bool, indent: Option<usize>) -> Cow<'_, str> {
667-
let s_space = trim_multiline_inner(s, ignore_first, indent, ' ');
668-
let s_tab = trim_multiline_inner(s_space, ignore_first, indent, '\t');
669-
trim_multiline_inner(s_tab, ignore_first, indent, ' ')
666+
/// Reindent a multiline string with possibility of ignoring the first line.
667+
#[allow(clippy::needless_pass_by_value)]
668+
pub fn reindent_multiline(s: Cow<'_, str>, ignore_first: bool, indent: Option<usize>) -> Cow<'_, str> {
669+
let s_space = reindent_multiline_inner(&s, ignore_first, indent, ' ');
670+
let s_tab = reindent_multiline_inner(&s_space, ignore_first, indent, '\t');
671+
reindent_multiline_inner(&s_tab, ignore_first, indent, ' ').into()
670672
}
671673

672-
fn trim_multiline_inner(s: Cow<'_, str>, ignore_first: bool, indent: Option<usize>, ch: char) -> Cow<'_, str> {
673-
let mut x = s
674+
fn reindent_multiline_inner(s: &str, ignore_first: bool, indent: Option<usize>, ch: char) -> String {
675+
let x = s
674676
.lines()
675677
.skip(ignore_first as usize)
676678
.filter_map(|l| {
@@ -683,26 +685,20 @@ fn trim_multiline_inner(s: Cow<'_, str>, ignore_first: bool, indent: Option<usiz
683685
})
684686
.min()
685687
.unwrap_or(0);
686-
if let Some(indent) = indent {
687-
x = x.saturating_sub(indent);
688-
}
689-
if x > 0 {
690-
Cow::Owned(
691-
s.lines()
692-
.enumerate()
693-
.map(|(i, l)| {
694-
if (ignore_first && i == 0) || l.is_empty() {
695-
l
696-
} else {
697-
l.split_at(x).1
698-
}
699-
})
700-
.collect::<Vec<_>>()
701-
.join("\n"),
702-
)
703-
} else {
704-
s
705-
}
688+
let indent = indent.unwrap_or(0);
689+
s.lines()
690+
.enumerate()
691+
.map(|(i, l)| {
692+
if (ignore_first && i == 0) || l.is_empty() {
693+
l.to_owned()
694+
} else if x > indent {
695+
l.split_at(x - indent).1.to_owned()
696+
} else {
697+
" ".repeat(indent - x) + l
698+
}
699+
})
700+
.collect::<Vec<String>>()
701+
.join("\n")
706702
}
707703

708704
/// Gets the parent expression, if any –- this is useful to constrain a lint.
@@ -1474,26 +1470,26 @@ macro_rules! unwrap_cargo_metadata {
14741470

14751471
#[cfg(test)]
14761472
mod test {
1477-
use super::{trim_multiline, without_block_comments};
1473+
use super::{reindent_multiline, without_block_comments};
14781474

14791475
#[test]
1480-
fn test_trim_multiline_single_line() {
1481-
assert_eq!("", trim_multiline("".into(), false, None));
1482-
assert_eq!("...", trim_multiline("...".into(), false, None));
1483-
assert_eq!("...", trim_multiline(" ...".into(), false, None));
1484-
assert_eq!("...", trim_multiline("\t...".into(), false, None));
1485-
assert_eq!("...", trim_multiline("\t\t...".into(), false, None));
1476+
fn test_reindent_multiline_single_line() {
1477+
assert_eq!("", reindent_multiline("".into(), false, None));
1478+
assert_eq!("...", reindent_multiline("...".into(), false, None));
1479+
assert_eq!("...", reindent_multiline(" ...".into(), false, None));
1480+
assert_eq!("...", reindent_multiline("\t...".into(), false, None));
1481+
assert_eq!("...", reindent_multiline("\t\t...".into(), false, None));
14861482
}
14871483

14881484
#[test]
14891485
#[rustfmt::skip]
1490-
fn test_trim_multiline_block() {
1486+
fn test_reindent_multiline_block() {
14911487
assert_eq!("\
14921488
if x {
14931489
y
14941490
} else {
14951491
z
1496-
}", trim_multiline(" if x {
1492+
}", reindent_multiline(" if x {
14971493
y
14981494
} else {
14991495
z
@@ -1503,7 +1499,7 @@ mod test {
15031499
\ty
15041500
} else {
15051501
\tz
1506-
}", trim_multiline(" if x {
1502+
}", reindent_multiline(" if x {
15071503
\ty
15081504
} else {
15091505
\tz
@@ -1512,21 +1508,37 @@ mod test {
15121508

15131509
#[test]
15141510
#[rustfmt::skip]
1515-
fn test_trim_multiline_empty_line() {
1511+
fn test_reindent_multiline_empty_line() {
15161512
assert_eq!("\
15171513
if x {
15181514
y
15191515
15201516
} else {
15211517
z
1522-
}", trim_multiline(" if x {
1518+
}", reindent_multiline(" if x {
15231519
y
15241520
15251521
} else {
15261522
z
15271523
}".into(), false, None));
15281524
}
15291525

1526+
#[test]
1527+
#[rustfmt::skip]
1528+
fn test_reindent_multiline_lines_deeper() {
1529+
assert_eq!("\
1530+
if x {
1531+
y
1532+
} else {
1533+
z
1534+
}", reindent_multiline("\
1535+
if x {
1536+
y
1537+
} else {
1538+
z
1539+
}".into(), true, Some(8)));
1540+
}
1541+
15301542
#[test]
15311543
fn test_without_block_comments_lines_without_block_comments() {
15321544
let result = without_block_comments(vec!["/*", "", "*/"]);

tests/ui/unit_arg.rs

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
#![warn(clippy::unit_arg)]
2-
#![allow(clippy::no_effect, unused_must_use, unused_variables)]
2+
#![allow(
3+
clippy::no_effect,
4+
unused_must_use,
5+
unused_variables,
6+
clippy::unused_unit,
7+
clippy::or_fun_call
8+
)]
39

410
use std::fmt::Debug;
511

@@ -47,6 +53,11 @@ fn bad() {
4753
foo(3);
4854
},
4955
);
56+
// here Some(foo(2)) isn't the top level statement expression, wrap the suggestion in a block
57+
None.or(Some(foo(2)));
58+
// in this case, the suggestion can be inlined, no need for a surrounding block
59+
// foo(()); foo(()) instead of { foo(()); foo(()) }
60+
foo(foo(()))
5061
}
5162

5263
fn ok() {

0 commit comments

Comments
 (0)