Skip to content

Commit db89f1d

Browse files
committed
Be more restrictive and use more clippy utils on lint manual_pattern_char_comparison.
Move `str_literal_to_char_literal` util function to source.rs
1 parent d1b9036 commit db89f1d

File tree

7 files changed

+163
-118
lines changed

7 files changed

+163
-118
lines changed

clippy_lints/src/methods/single_char_insert_string.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use clippy_utils::diagnostics::span_lint_and_sugg;
2-
use clippy_utils::source::snippet_with_applicability;
3-
use clippy_utils::str_utils::get_hint_if_single_char_arg;
2+
use clippy_utils::source::{snippet_with_applicability, str_literal_to_char_literal};
43
use rustc_errors::Applicability;
54
use rustc_hir as hir;
65
use rustc_lint::LateContext;
@@ -10,7 +9,7 @@ use super::SINGLE_CHAR_ADD_STR;
109
/// lint for length-1 `str`s as argument for `insert_str`
1110
pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, receiver: &hir::Expr<'_>, args: &[hir::Expr<'_>]) {
1211
let mut applicability = Applicability::MachineApplicable;
13-
if let Some(extension_string) = get_hint_if_single_char_arg(cx, &args[1], &mut applicability, false) {
12+
if let Some(extension_string) = str_literal_to_char_literal(cx, &args[1], &mut applicability, false) {
1413
let base_string_snippet =
1514
snippet_with_applicability(cx, receiver.span.source_callsite(), "_", &mut applicability);
1615
let pos_arg = snippet_with_applicability(cx, args[0].span, "..", &mut applicability);

clippy_lints/src/methods/single_char_push_string.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use clippy_utils::diagnostics::span_lint_and_sugg;
2-
use clippy_utils::source::snippet_with_applicability;
3-
use clippy_utils::str_utils::get_hint_if_single_char_arg;
2+
use clippy_utils::source::{snippet_with_applicability, str_literal_to_char_literal};
43
use rustc_errors::Applicability;
54
use rustc_hir as hir;
65
use rustc_lint::LateContext;
@@ -10,7 +9,7 @@ use super::SINGLE_CHAR_ADD_STR;
109
/// lint for length-1 `str`s as argument for `push_str`
1110
pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, receiver: &hir::Expr<'_>, args: &[hir::Expr<'_>]) {
1211
let mut applicability = Applicability::MachineApplicable;
13-
if let Some(extension_string) = get_hint_if_single_char_arg(cx, &args[0], &mut applicability, false) {
12+
if let Some(extension_string) = str_literal_to_char_literal(cx, &args[0], &mut applicability, false) {
1413
let base_string_snippet =
1514
snippet_with_applicability(cx, receiver.span.source_callsite(), "..", &mut applicability);
1615
let sugg = format!("{base_string_snippet}.push({extension_string})");

clippy_lints/src/string_patterns.rs

Lines changed: 78 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ use std::cmp::Ordering;
22
use std::ops::ControlFlow;
33

44
use clippy_utils::diagnostics::span_lint_and_sugg;
5-
use clippy_utils::source::snippet;
6-
use clippy_utils::str_utils::get_hint_if_single_char_arg;
5+
use clippy_utils::macros::matching_root_macro_call;
6+
use clippy_utils::path_to_local_id;
7+
use clippy_utils::source::str_literal_to_char_literal;
78
use clippy_utils::visitors::{for_each_expr, Descend};
89
use rustc_ast::{BinOpKind, LitKind};
910
use rustc_errors::Applicability;
1011
use rustc_hir::{Expr, ExprKind, PatKind, QPath};
1112
use rustc_lint::{LateContext, LateLintPass};
1213
use rustc_middle::ty;
1314
use rustc_session::declare_lint_pass;
15+
use rustc_span::sym;
1416

1517
declare_clippy_lint! {
1618
/// ### What it does
@@ -93,95 +95,110 @@ const PATTERN_METHODS: [(&str, usize); 22] = [
9395
("replacen", 0),
9496
];
9597

96-
fn check_single_char_pattern_lint(cx: &LateContext<'_>, arg: &Expr<'_>, applicability: &mut Applicability) {
97-
if let Some(hint) = get_hint_if_single_char_arg(cx, arg, applicability, true) {
98+
fn check_single_char_pattern_lint(cx: &LateContext<'_>, arg: &Expr<'_>) {
99+
let mut applicability = Applicability::MachineApplicable;
100+
if let Some(hint) = str_literal_to_char_literal(cx, arg, &mut applicability, true) {
98101
span_lint_and_sugg(
99102
cx,
100103
SINGLE_CHAR_PATTERN,
101104
arg.span,
102105
"single-character string constant used as pattern",
103106
"consider using a `char`",
104107
hint,
105-
*applicability,
108+
applicability,
106109
);
107110
}
108111
}
109112

110-
fn check_manual_pattern_char_comparison(cx: &LateContext<'_>, arg: &Expr<'_>, applicability: Applicability) {
111-
if let ExprKind::Closure(closure) = arg.kind
113+
fn get_char_value(expr: &Expr<'_>) -> Option<String> {
114+
match expr.kind {
115+
ExprKind::Lit(lit) => match lit.node {
116+
LitKind::Char(c) => Some(format!("'{}'", c.escape_default())),
117+
_ => None,
118+
},
119+
ExprKind::Path(QPath::Resolved(_, path)) => {
120+
if path.segments.len() == 1 {
121+
let segment = &path.segments[0];
122+
if segment.args.is_none() {
123+
Some(segment.ident.name.to_string())
124+
} else {
125+
None
126+
}
127+
} else {
128+
None
129+
}
130+
},
131+
_ => None,
132+
}
133+
}
134+
135+
fn check_manual_pattern_char_comparison(cx: &LateContext<'_>, method_arg: &Expr<'_>) {
136+
let applicability = Applicability::MachineApplicable;
137+
if let ExprKind::Closure(closure) = method_arg.kind
112138
&& let body = cx.tcx.hir().body(closure.body)
113-
&& let Some(param_name) = body.params[0].pat.simple_ident()
139+
&& let PatKind::Binding(_, binding, ..) = body.params[0].pat.kind
114140
{
115-
let ex = body.value;
116141
let mut set_chars: Vec<String> = Vec::new();
117142

118143
// We want to retrieve all the comparisons done.
119144
// They are ordered in a nested way and so we need to traverse the AST to collect them all.
120-
for_each_expr(ex, |sub_expr| -> ControlFlow<(), Descend> {
145+
if for_each_expr(body.value, |sub_expr| -> ControlFlow<(), Descend> {
121146
match sub_expr.kind {
122-
ExprKind::Binary(op, left, right) => {
123-
// Avoid matching code that contains complex comparisons such as functions calls etc..
124-
// (ex: c.is_whitespace())
125-
if matches!(
126-
left.kind,
127-
ExprKind::Binary(_, _, _) | ExprKind::Match(_, _, _) | ExprKind::Lit(_) | ExprKind::Path(_)
128-
) && matches!(
129-
right.kind,
130-
ExprKind::Binary(_, _, _) | ExprKind::Match(_, _, _) | ExprKind::Lit(_) | ExprKind::Path(_)
131-
) {
132-
if matches!(op.node, BinOpKind::Eq | BinOpKind::Or) {
133-
ControlFlow::Continue(Descend::Yes)
134-
} else {
135-
ControlFlow::Continue(Descend::No)
136-
}
147+
ExprKind::Binary(op, left, right) if op.node == BinOpKind::Eq => {
148+
if path_to_local_id(left, binding)
149+
&& cx.typeck_results().expr_ty_adjusted(right).kind() == &ty::Char
150+
&& let Some(c) = get_char_value(right)
151+
{
152+
set_chars.push(c);
153+
ControlFlow::Continue(Descend::No)
154+
} else if path_to_local_id(right, binding)
155+
&& cx.typeck_results().expr_ty_adjusted(left).kind() == &ty::Char
156+
&& let Some(c) = get_char_value(left)
157+
{
158+
set_chars.push(c);
159+
ControlFlow::Continue(Descend::No)
137160
} else {
138161
ControlFlow::Break(())
139162
}
140163
},
141-
ExprKind::Match(match_value, arms, _) => {
142-
if snippet(cx, match_value.span, "") != param_name.name.as_str() || arms.len() != 2 {
164+
ExprKind::Binary(op, _, _) if op.node == BinOpKind::Or => ControlFlow::Continue(Descend::Yes),
165+
ExprKind::Match(match_value, [arm, _], _) => {
166+
if matching_root_macro_call(cx, sub_expr.span, sym::matches_macro).is_none() {
143167
return ControlFlow::Break(());
144168
}
145-
if let PatKind::Or(pats) = arms[0].pat.kind {
146-
for pat in pats {
147-
if let PatKind::Lit(expr) = pat.kind
148-
&& let ExprKind::Lit(lit) = expr.kind
149-
&& let LitKind::Char(c) = lit.node
150-
{
151-
set_chars.push(format!("'{}'", c.escape_default()));
152-
}
153-
}
154-
} else if let PatKind::Lit(expr) = arms[0].pat.kind
155-
&& let ExprKind::Lit(lit) = expr.kind
156-
&& let LitKind::Char(c) = lit.node
157-
{
158-
set_chars.push(format!("'{}'", c.escape_default()));
169+
if arm.guard.is_some() {
170+
return ControlFlow::Break(());
159171
}
160-
ControlFlow::Continue(Descend::No)
161-
},
162-
ExprKind::Lit(lit) => {
163-
if let LitKind::Char(c) = lit.node {
164-
set_chars.push(format!("'{}'", c.escape_default()));
172+
if !path_to_local_id(match_value, binding) {
173+
return ControlFlow::Break(());
165174
}
166-
ControlFlow::Continue(Descend::No)
167-
},
168-
ExprKind::Path(QPath::Resolved(_, path)) => {
169-
if path.segments.len() == 1 {
170-
let segment = &path.segments[0];
171-
if segment.ident != param_name {
172-
set_chars.push(segment.ident.to_string());
173-
}
175+
if arm.pat.walk_short(|pat| match pat.kind {
176+
PatKind::Lit(expr) if let ExprKind::Lit(lit) = expr.kind => {
177+
if let LitKind::Char(c) = lit.node {
178+
set_chars.push(format!("'{}'", c.escape_default()));
179+
}
180+
true
181+
},
182+
PatKind::Or(_) => true,
183+
_ => false,
184+
}) {
185+
ControlFlow::Continue(Descend::No)
186+
} else {
187+
ControlFlow::Break(())
174188
}
175-
ControlFlow::Continue(Descend::No)
176189
},
177-
_ => ControlFlow::Continue(Descend::No),
190+
_ => ControlFlow::Break(()),
178191
}
179-
});
192+
})
193+
.is_some()
194+
{
195+
return;
196+
}
180197
match set_chars.len().cmp(&1) {
181198
Ordering::Equal => span_lint_and_sugg(
182199
cx,
183200
MANUAL_PATTERN_CHAR_COMPARISON,
184-
arg.span,
201+
method_arg.span,
185202
"this manual char comparison can be written more succinctly",
186203
"consider using a `char`",
187204
set_chars[0].clone(),
@@ -190,7 +207,7 @@ fn check_manual_pattern_char_comparison(cx: &LateContext<'_>, arg: &Expr<'_>, ap
190207
Ordering::Greater => span_lint_and_sugg(
191208
cx,
192209
MANUAL_PATTERN_CHAR_COMPARISON,
193-
arg.span,
210+
method_arg.span,
194211
"this manual char comparison can be written more succinctly",
195212
"consider using an array of `char`",
196213
format!("[{}]", set_chars.join(", ")),
@@ -214,10 +231,9 @@ impl<'tcx> LateLintPass<'tcx> for StringPatterns {
214231
&& args.len() > pos
215232
{
216233
let arg = &args[pos];
217-
let mut applicability = Applicability::MachineApplicable;
218-
check_single_char_pattern_lint(cx, arg, &mut applicability);
234+
check_single_char_pattern_lint(cx, arg);
219235

220-
check_manual_pattern_char_comparison(cx, arg, applicability);
236+
check_manual_pattern_char_comparison(cx, arg);
221237
}
222238
}
223239
}

clippy_utils/src/source.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
#![allow(clippy::module_name_repetitions)]
44

5+
use rustc_ast::{LitKind, StrStyle};
56
use rustc_data_structures::sync::Lrc;
67
use rustc_errors::Applicability;
78
use rustc_hir::{BlockCheckMode, Expr, ExprKind, UnsafeSource};
@@ -500,6 +501,50 @@ pub fn expand_past_previous_comma(cx: &LateContext<'_>, span: Span) -> Span {
500501
extended.with_lo(extended.lo() - BytePos(1))
501502
}
502503

504+
/// Converts `expr` to a `char` literal if it's a `str` literal containing a single
505+
/// character (or a single byte with `ascii_only`)
506+
pub fn str_literal_to_char_literal(
507+
cx: &LateContext<'_>,
508+
expr: &Expr<'_>,
509+
applicability: &mut Applicability,
510+
ascii_only: bool,
511+
) -> Option<String> {
512+
if let ExprKind::Lit(lit) = &expr.kind
513+
&& let LitKind::Str(r, style) = lit.node
514+
&& let string = r.as_str()
515+
&& let len = if ascii_only {
516+
string.len()
517+
} else {
518+
string.chars().count()
519+
}
520+
&& len == 1
521+
{
522+
let snip = snippet_with_applicability(cx, expr.span, string, applicability);
523+
let ch = if let StrStyle::Raw(nhash) = style {
524+
let nhash = nhash as usize;
525+
// for raw string: r##"a"##
526+
&snip[(nhash + 2)..(snip.len() - 1 - nhash)]
527+
} else {
528+
// for regular string: "a"
529+
&snip[1..(snip.len() - 1)]
530+
};
531+
532+
let hint = format!(
533+
"'{}'",
534+
match ch {
535+
"'" => "\\'",
536+
r"\" => "\\\\",
537+
"\\\"" => "\"", // no need to escape `"` in `'"'`
538+
_ => ch,
539+
}
540+
);
541+
542+
Some(hint)
543+
} else {
544+
None
545+
}
546+
}
547+
503548
#[cfg(test)]
504549
mod test {
505550
use super::{reindent_multiline, without_block_comments};

clippy_utils/src/str_utils.rs

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
use rustc_ast::{LitKind, StrStyle};
2-
use rustc_errors::Applicability;
3-
use rustc_hir::{Expr, ExprKind};
4-
use rustc_lint::LateContext;
5-
6-
use crate::source::snippet_with_applicability;
7-
81
/// Dealing with sting indices can be hard, this struct ensures that both the
92
/// character and byte index are provided for correct indexing.
103
#[derive(Debug, Default, PartialEq, Eq)]
@@ -296,49 +289,6 @@ pub fn to_camel_case(item_name: &str) -> String {
296289
s
297290
}
298291

299-
/// Return an hint if the `arg` is a string with a single character
300-
pub fn get_hint_if_single_char_arg(
301-
cx: &LateContext<'_>,
302-
arg: &Expr<'_>,
303-
applicability: &mut Applicability,
304-
ascii_only: bool,
305-
) -> Option<String> {
306-
if let ExprKind::Lit(lit) = &arg.kind
307-
&& let LitKind::Str(r, style) = lit.node
308-
&& let string = r.as_str()
309-
&& let len = if ascii_only {
310-
string.len()
311-
} else {
312-
string.chars().count()
313-
}
314-
&& len == 1
315-
{
316-
let snip = snippet_with_applicability(cx, arg.span, string, applicability);
317-
let ch = if let StrStyle::Raw(nhash) = style {
318-
let nhash = nhash as usize;
319-
// for raw string: r##"a"##
320-
&snip[(nhash + 2)..(snip.len() - 1 - nhash)]
321-
} else {
322-
// for regular string: "a"
323-
&snip[1..(snip.len() - 1)]
324-
};
325-
326-
let hint = format!(
327-
"'{}'",
328-
match ch {
329-
"'" => "\\'",
330-
r"\" => "\\\\",
331-
"\\\"" => "\"", // no need to escape `"` in `'"'`
332-
_ => ch,
333-
}
334-
);
335-
336-
Some(hint)
337-
} else {
338-
None
339-
}
340-
}
341-
342292
#[cfg(test)]
343293
mod test {
344294
use super::*;

tests/ui/manual_pattern_char_comparison.fixed

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,22 @@ fn main() {
2121

2222
let not_str = NotStr;
2323
not_str.find(|c: char| c == 'X');
24+
25+
"".find(|c| c == 'a' || c > 'z');
26+
27+
let x = true;
28+
"".find(|c| c == 'a' || x || c == 'b');
29+
30+
let d = 'd';
31+
"".find(|c| c == 'a' || d == 'b');
32+
33+
"".find(|c| match c {
34+
'a' | 'b' => true,
35+
_ => c.is_ascii(),
36+
});
37+
38+
"".find(|c| matches!(c, 'a' | 'b' if false));
39+
40+
"".find(|c| matches!(c, 'a' | '1'..'4'));
41+
"".find(|c| c == 'a' || matches!(c, '1'..'4'));
2442
}

0 commit comments

Comments
 (0)