Skip to content

Commit 48df86f

Browse files
committed
fix: match_single_binding suggests wrongly inside binary expr
1 parent ba6485d commit 48df86f

File tree

6 files changed

+119
-77
lines changed

6 files changed

+119
-77
lines changed

clippy_lints/src/matches/match_single_binding.rs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ use std::ops::ControlFlow;
22

33
use clippy_utils::diagnostics::span_lint_and_sugg;
44
use clippy_utils::macros::HirNode;
5-
use clippy_utils::source::{
6-
RelativeIndent, indent_of, reindent_multiline_relative, snippet, snippet_block_with_context, snippet_with_context,
7-
};
8-
use clippy_utils::{is_refutable, peel_blocks};
5+
use clippy_utils::source::{indent_of, reindent_multiline, snippet, snippet_block_with_context, snippet_with_context};
6+
use clippy_utils::{is_expr_identity_of_pat, is_refutable, peel_blocks};
97
use rustc_data_structures::fx::FxHashSet;
108
use rustc_errors::Applicability;
119
use rustc_hir::def::Res;
@@ -86,6 +84,18 @@ pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], e
8684
snippet_with_context(cx, pat_span, ctxt, "..", &mut app).0
8785
),
8886
),
87+
None if is_expr_identity_of_pat(cx, arms[0].pat, ex, false) => {
88+
span_lint_and_sugg(
89+
cx,
90+
MATCH_SINGLE_BINDING,
91+
expr.span,
92+
"this match could be replaced by its body itself",
93+
"consider using the match body instead",
94+
snippet_body,
95+
Applicability::MachineApplicable,
96+
);
97+
return;
98+
},
8999
None => {
90100
let sugg = sugg_with_curlies(
91101
cx,
@@ -302,7 +312,7 @@ fn expr_in_nested_block(cx: &LateContext<'_>, match_expr: &Expr<'_>) -> bool {
302312
fn expr_must_have_curlies(cx: &LateContext<'_>, match_expr: &Expr<'_>) -> bool {
303313
let parent = cx.tcx.parent_hir_node(match_expr.hir_id);
304314
if let Node::Expr(Expr {
305-
kind: ExprKind::Closure { .. },
315+
kind: ExprKind::Closure(..) | ExprKind::Binary(..),
306316
..
307317
})
308318
| Node::AnonConst(..) = parent
@@ -319,15 +329,23 @@ fn expr_must_have_curlies(cx: &LateContext<'_>, match_expr: &Expr<'_>) -> bool {
319329
false
320330
}
321331

332+
fn indent_of_nth_line(snippet: &str, nth: usize) -> Option<usize> {
333+
snippet
334+
.lines()
335+
.nth(nth)
336+
.and_then(|s| s.find(|c: char| !c.is_whitespace()))
337+
}
338+
322339
fn reindent_snippet_if_in_block(snippet_body: &str, has_assignment: bool) -> String {
323340
if has_assignment || !snippet_body.starts_with('{') {
324-
return reindent_multiline_relative(snippet_body, true, RelativeIndent::Add(0));
341+
return reindent_multiline(snippet_body, true, indent_of_nth_line(snippet_body, 1));
325342
}
326343

327-
reindent_multiline_relative(
328-
snippet_body.trim_start_matches('{').trim_end_matches('}').trim(),
344+
let snippet_body = snippet_body.trim_start_matches('{').trim_end_matches('}').trim();
345+
reindent_multiline(
346+
snippet_body,
329347
false,
330-
RelativeIndent::Sub(4),
348+
indent_of_nth_line(snippet_body, 0).map(|indent| indent.saturating_sub(4)),
331349
)
332350
}
333351

clippy_utils/src/lib.rs

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1900,39 +1900,6 @@ pub fn is_must_use_func_call(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
19001900
///
19011901
/// Consider calling [`is_expr_untyped_identity_function`] or [`is_expr_identity_function`] instead.
19021902
fn is_body_identity_function(cx: &LateContext<'_>, func: &Body<'_>) -> bool {
1903-
fn check_pat(cx: &LateContext<'_>, pat: &Pat<'_>, expr: &Expr<'_>) -> bool {
1904-
if cx
1905-
.typeck_results()
1906-
.pat_binding_modes()
1907-
.get(pat.hir_id)
1908-
.is_some_and(|mode| matches!(mode.0, ByRef::Yes(_)))
1909-
{
1910-
// If the parameter is `(x, y)` of type `&(T, T)`, or `[x, y]` of type `&[T; 2]`, then
1911-
// due to match ergonomics, the inner patterns become references. Don't consider this
1912-
// the identity function as that changes types.
1913-
return false;
1914-
}
1915-
1916-
match (pat.kind, expr.kind) {
1917-
(PatKind::Binding(_, id, _, _), _) => {
1918-
path_to_local_id(expr, id) && cx.typeck_results().expr_adjustments(expr).is_empty()
1919-
},
1920-
(PatKind::Tuple(pats, dotdot), ExprKind::Tup(tup))
1921-
if dotdot.as_opt_usize().is_none() && pats.len() == tup.len() =>
1922-
{
1923-
pats.iter().zip(tup).all(|(pat, expr)| check_pat(cx, pat, expr))
1924-
},
1925-
(PatKind::Slice(before, slice, after), ExprKind::Array(arr))
1926-
if slice.is_none() && before.len() + after.len() == arr.len() =>
1927-
{
1928-
(before.iter().chain(after))
1929-
.zip(arr)
1930-
.all(|(pat, expr)| check_pat(cx, pat, expr))
1931-
},
1932-
_ => false,
1933-
}
1934-
}
1935-
19361903
let [param] = func.params else {
19371904
return false;
19381905
};
@@ -1965,11 +1932,56 @@ fn is_body_identity_function(cx: &LateContext<'_>, func: &Body<'_>) -> bool {
19651932
return false;
19661933
}
19671934
},
1968-
_ => return check_pat(cx, param.pat, expr),
1935+
_ => return is_expr_identity_of_pat(cx, param.pat, expr, true),
19691936
}
19701937
}
19711938
}
19721939

1940+
/// Checks if the given expression is an identity representation of the given pattern:
1941+
/// * `x` is the identity representation of `x`
1942+
/// * `(x, y)` is the identity representation of `(x, y)`
1943+
/// * `[x, y]` is the identity representation of `[x, y]`
1944+
///
1945+
/// Note that `by_hir` is used to determine bindings are checked by their `HirId` or by their name.
1946+
/// This can be useful when checking patterns in `let` bindings or `match` arms.
1947+
pub fn is_expr_identity_of_pat(cx: &LateContext<'_>, pat: &Pat<'_>, expr: &Expr<'_>, by_hir: bool) -> bool {
1948+
if cx
1949+
.typeck_results()
1950+
.pat_binding_modes()
1951+
.get(pat.hir_id)
1952+
.is_some_and(|mode| matches!(mode.0, ByRef::Yes(_)))
1953+
{
1954+
// If the parameter is `(x, y)` of type `&(T, T)`, or `[x, y]` of type `&[T; 2]`, then
1955+
// due to match ergonomics, the inner patterns become references. Don't consider this
1956+
// the identity function as that changes types.
1957+
return false;
1958+
}
1959+
1960+
match (pat.kind, expr.kind) {
1961+
(PatKind::Binding(_, id, _, _), _) if by_hir => {
1962+
path_to_local_id(expr, id) && cx.typeck_results().expr_adjustments(expr).is_empty()
1963+
},
1964+
(PatKind::Binding(_, _, ident, _), ExprKind::Path(QPath::Resolved(_, path))) => {
1965+
matches!(path.segments, [ segment] if segment.ident.name == ident.name)
1966+
},
1967+
(PatKind::Tuple(pats, dotdot), ExprKind::Tup(tup))
1968+
if dotdot.as_opt_usize().is_none() && pats.len() == tup.len() =>
1969+
{
1970+
pats.iter()
1971+
.zip(tup)
1972+
.all(|(pat, expr)| is_expr_identity_of_pat(cx, pat, expr, by_hir))
1973+
},
1974+
(PatKind::Slice(before, slice, after), ExprKind::Array(arr))
1975+
if slice.is_none() && before.len() + after.len() == arr.len() =>
1976+
{
1977+
(before.iter().chain(after))
1978+
.zip(arr)
1979+
.all(|(pat, expr)| is_expr_identity_of_pat(cx, pat, expr, by_hir))
1980+
},
1981+
_ => false,
1982+
}
1983+
}
1984+
19731985
/// This is the same as [`is_expr_identity_function`], but does not consider closures
19741986
/// with type annotations for its bindings (or similar) as identity functions:
19751987
/// * `|x: u8| x`

clippy_utils/src/source.rs

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use rustc_span::{
1818
};
1919
use std::borrow::Cow;
2020
use std::fmt;
21-
use std::ops::{Add, Deref, Index, Range};
21+
use std::ops::{Deref, Index, Range};
2222

2323
pub trait HasSession {
2424
fn sess(&self) -> &Session;
@@ -476,38 +476,6 @@ pub fn position_before_rarrow(s: &str) -> Option<usize> {
476476
})
477477
}
478478

479-
pub enum RelativeIndent {
480-
Add(usize),
481-
Sub(usize),
482-
}
483-
484-
impl Add<RelativeIndent> for usize {
485-
type Output = usize;
486-
487-
fn add(self, rhs: RelativeIndent) -> Self::Output {
488-
match rhs {
489-
RelativeIndent::Add(n) => self + n,
490-
RelativeIndent::Sub(n) => self.saturating_sub(n),
491-
}
492-
}
493-
}
494-
495-
/// Reindents a multiline string with possibility of ignoring the first line and relative
496-
/// indentation.
497-
pub fn reindent_multiline_relative(s: &str, ignore_first: bool, relative_indent: RelativeIndent) -> String {
498-
fn indent_of_string(s: &str) -> usize {
499-
s.find(|c: char| !c.is_whitespace()).unwrap_or(0)
500-
}
501-
502-
let mut indent = 0;
503-
if let Some(line) = s.lines().nth(usize::from(ignore_first)) {
504-
let line_indent = indent_of_string(line);
505-
indent = line_indent + relative_indent;
506-
}
507-
508-
reindent_multiline(s, ignore_first, Some(indent))
509-
}
510-
511479
/// Reindent a multiline string with possibility of ignoring the first line.
512480
pub fn reindent_multiline(s: &str, ignore_first: bool, indent: Option<usize>) -> String {
513481
let s_space = reindent_multiline_inner(s, ignore_first, indent, ' ');

tests/ui/match_single_binding.fixed

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,12 @@ mod issue15018 {
257257
}
258258
}
259259
}
260+
261+
#[allow(clippy::short_circuit_statement)]
262+
fn issue15269(a: usize, b: usize, c: usize) -> bool {
263+
a < b
264+
&& b < c;
265+
266+
a < b
267+
&& b < c
268+
}

tests/ui/match_single_binding.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,3 +328,18 @@ mod issue15018 {
328328
}
329329
}
330330
}
331+
332+
#[allow(clippy::short_circuit_statement)]
333+
fn issue15269(a: usize, b: usize, c: usize) -> bool {
334+
a < b
335+
&& match b {
336+
//~^ match_single_binding
337+
b => b < c,
338+
};
339+
340+
a < b
341+
&& match (a, b) {
342+
//~^ match_single_binding
343+
(a, b) => b < c,
344+
}
345+
}

tests/ui/match_single_binding.stderr

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,5 +505,25 @@ LL ~ let (x, y, z) = (a, b, c);
505505
LL + println!("{} {} {}", x, y, z);
506506
|
507507

508-
error: aborting due to 35 previous errors
508+
error: this match could be replaced by its body itself
509+
--> tests/ui/match_single_binding.rs:335:12
510+
|
511+
LL | && match b {
512+
| ____________^
513+
LL | |
514+
LL | | b => b < c,
515+
LL | | };
516+
| |_________^ help: consider using the match body instead: `b < c`
517+
518+
error: this match could be replaced by its body itself
519+
--> tests/ui/match_single_binding.rs:341:12
520+
|
521+
LL | && match (a, b) {
522+
| ____________^
523+
LL | |
524+
LL | | (a, b) => b < c,
525+
LL | | }
526+
| |_________^ help: consider using the match body instead: `b < c`
527+
528+
error: aborting due to 37 previous errors
509529

0 commit comments

Comments
 (0)