Skip to content

Commit 3bca6fd

Browse files
committed
skip warning when generic evolved;
suggest explicit type when its inferred in closure
1 parent 7e650b7 commit 3bca6fd

File tree

4 files changed

+171
-17
lines changed

4 files changed

+171
-17
lines changed

clippy_lints/src/manual_is_ascii_check.rs

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
use clippy_config::msrvs::{self, Msrv};
2-
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::diagnostics::span_lint_and_then;
33
use clippy_utils::macros::root_macro_call;
44
use clippy_utils::sugg::Sugg;
5-
use clippy_utils::{higher, in_constant};
5+
use clippy_utils::{higher, in_constant, path_to_local, peel_ref_operators};
66
use rustc_ast::ast::RangeLimits;
7-
use rustc_ast::LitKind::{Byte, Char};
7+
use rustc_ast::LitKind::{self, Byte, Char};
8+
use rustc_data_structures::fx::FxHashMap;
89
use rustc_errors::Applicability;
9-
use rustc_hir::{BorrowKind, Expr, ExprKind, PatKind, RangeEnd};
10+
use rustc_hir::{BodyId, Closure, Expr, ExprKind, FnDecl, HirId, PatKind, RangeEnd, TyKind};
1011
use rustc_lint::{LateContext, LateLintPass};
12+
use rustc_middle::ty;
1113
use rustc_session::impl_lint_pass;
1214
use rustc_span::def_id::DefId;
1315
use rustc_span::{sym, Span};
@@ -59,12 +61,16 @@ impl_lint_pass!(ManualIsAsciiCheck => [MANUAL_IS_ASCII_CHECK]);
5961

6062
pub struct ManualIsAsciiCheck {
6163
msrv: Msrv,
64+
closure_params: FxHashMap<HirId, Span>,
6265
}
6366

6467
impl ManualIsAsciiCheck {
6568
#[must_use]
6669
pub fn new(msrv: Msrv) -> Self {
67-
Self { msrv }
70+
Self {
71+
msrv,
72+
closure_params: FxHashMap::default(),
73+
}
6874
}
6975
}
7076

@@ -97,12 +103,16 @@ impl<'tcx> LateLintPass<'tcx> for ManualIsAsciiCheck {
97103
return;
98104
}
99105

106+
if let ExprKind::Closure(Closure { fn_decl, body, .. }) = expr.kind {
107+
collect_params_with_inferred_ty(cx, *body, fn_decl, &mut self.closure_params);
108+
}
109+
100110
if let Some(macro_call) = root_macro_call(expr.span)
101111
&& is_matches_macro(cx, macro_call.def_id)
102112
{
103113
if let ExprKind::Match(recv, [arm, ..], _) = expr.kind {
104114
let range = check_pat(&arm.pat.kind);
105-
check_is_ascii(cx, macro_call.span, recv, &range);
115+
check_is_ascii(cx, macro_call.span, recv, &range, None);
106116
}
107117
} else if let ExprKind::MethodCall(path, receiver, [arg], ..) = expr.kind
108118
&& path.ident.name == sym!(contains)
@@ -112,19 +122,61 @@ impl<'tcx> LateLintPass<'tcx> for ManualIsAsciiCheck {
112122
limits: RangeLimits::Closed,
113123
}) = higher::Range::hir(receiver)
114124
{
115-
let range = check_range(start, end);
116-
if let ExprKind::AddrOf(BorrowKind::Ref, _, e) = arg.kind {
117-
check_is_ascii(cx, expr.span, e, &range);
118-
} else {
119-
check_is_ascii(cx, expr.span, arg, &range);
125+
let arg_ty = cx.typeck_results().expr_ty(arg).peel_refs();
126+
if matches!(arg_ty.kind(), ty::Param(_)) {
127+
return;
120128
}
129+
130+
let arg = peel_ref_operators(cx, arg);
131+
let ty_sugg = get_ty_sugg(arg, &self.closure_params, start);
132+
let range = check_range(start, end);
133+
check_is_ascii(cx, expr.span, peel_ref_operators(cx, arg), &range, ty_sugg);
121134
}
122135
}
123136

137+
fn check_mod(&mut self, _: &LateContext<'tcx>, _: &'tcx rustc_hir::Mod<'tcx>, _: HirId) {
138+
self.closure_params = FxHashMap::default();
139+
}
140+
124141
extract_msrv_attr!(LateContext);
125142
}
126143

127-
fn check_is_ascii(cx: &LateContext<'_>, span: Span, recv: &Expr<'_>, range: &CharRange) {
144+
fn get_ty_sugg(arg: &Expr<'_>, map: &FxHashMap<HirId, Span>, range_expr: &Expr<'_>) -> Option<(Span, &'static str)> {
145+
let ExprKind::Lit(lit) = range_expr.kind else {
146+
return None;
147+
};
148+
let sugg_ty_span = path_to_local(arg).and_then(|id| map.get(&id)).copied()?;
149+
let sugg_ty_str = match lit.node {
150+
LitKind::Char(_) => "char",
151+
LitKind::Byte(_) => "u8",
152+
_ => return None,
153+
};
154+
155+
Some((sugg_ty_span, sugg_ty_str))
156+
}
157+
158+
/// Collect closure params' `HirId` and `Span` pairs into a map,
159+
/// if they have implicit (inferred) `ty`.
160+
fn collect_params_with_inferred_ty(
161+
cx: &LateContext<'_>,
162+
body_id: BodyId,
163+
fn_decl: &FnDecl<'_>,
164+
map: &mut FxHashMap<HirId, Span>,
165+
) {
166+
let params = cx.tcx.hir().body(body_id).params.iter();
167+
let fn_decl_input_tys = fn_decl.inputs.iter();
168+
for (id, span) in params.zip(fn_decl_input_tys).filter_map(|(param, ty)| {
169+
if let TyKind::Infer = ty.kind {
170+
Some((param.pat.hir_id, param.span))
171+
} else {
172+
None
173+
}
174+
}) {
175+
map.insert(id, span);
176+
}
177+
}
178+
179+
fn check_is_ascii(cx: &LateContext<'_>, span: Span, recv: &Expr<'_>, range: &CharRange, ty_sugg: Option<(Span, &str)>) {
128180
if let Some(sugg) = match range {
129181
CharRange::UpperChar => Some("is_ascii_uppercase"),
130182
CharRange::LowerChar => Some("is_ascii_lowercase"),
@@ -137,14 +189,22 @@ fn check_is_ascii(cx: &LateContext<'_>, span: Span, recv: &Expr<'_>, range: &Cha
137189
let mut app = Applicability::MachineApplicable;
138190
let recv = Sugg::hir_with_context(cx, recv, span.ctxt(), default_snip, &mut app).maybe_par();
139191

140-
span_lint_and_sugg(
192+
span_lint_and_then(
141193
cx,
142194
MANUAL_IS_ASCII_CHECK,
143195
span,
144196
"manual check for common ascii range",
145-
"try",
146-
format!("{recv}.{sugg}()"),
147-
app,
197+
|diag| {
198+
diag.span_suggestion(span, "try", format!("{recv}.{sugg}()"), app);
199+
if let Some((ty_span, ty_str)) = ty_sugg {
200+
diag.span_suggestion(
201+
ty_span,
202+
"also make sure to label the correct type",
203+
format!("{recv}: {ty_str}"),
204+
app,
205+
);
206+
}
207+
},
148208
);
149209
}
150210
}

tests/ui/manual_is_ascii_check.fixed

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,29 @@ fn msrv_1_47() {
5555
const FOO: bool = 'x'.is_ascii_digit();
5656
const BAR: bool = 'x'.is_ascii_hexdigit();
5757
}
58+
59+
#[allow(clippy::deref_addrof, clippy::needless_borrow)]
60+
fn with_refs() {
61+
let cool_letter = &&'g';
62+
cool_letter.is_ascii_digit();
63+
cool_letter.is_ascii_lowercase();
64+
}
65+
66+
fn generics() {
67+
fn a<U>(u: &U) -> bool
68+
where
69+
char: PartialOrd<U>,
70+
U: PartialOrd<char> + ?Sized,
71+
{
72+
('A'..='Z').contains(u)
73+
}
74+
75+
fn take_while<Item, F>(cond: F)
76+
where
77+
Item: Sized,
78+
F: Fn(Item) -> bool,
79+
{
80+
}
81+
take_while(|c: char| c.is_ascii_uppercase());
82+
take_while(|c: u8| c.is_ascii_uppercase());
83+
}

tests/ui/manual_is_ascii_check.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,29 @@ fn msrv_1_47() {
5555
const FOO: bool = matches!('x', '0'..='9');
5656
const BAR: bool = matches!('x', '0'..='9' | 'a'..='f' | 'A'..='F');
5757
}
58+
59+
#[allow(clippy::deref_addrof, clippy::needless_borrow)]
60+
fn with_refs() {
61+
let cool_letter = &&'g';
62+
('0'..='9').contains(&&cool_letter);
63+
('a'..='z').contains(*cool_letter);
64+
}
65+
66+
fn generics() {
67+
fn a<U>(u: &U) -> bool
68+
where
69+
char: PartialOrd<U>,
70+
U: PartialOrd<char> + ?Sized,
71+
{
72+
('A'..='Z').contains(u)
73+
}
74+
75+
fn take_while<Item, F>(cond: F)
76+
where
77+
Item: Sized,
78+
F: Fn(Item) -> bool,
79+
{
80+
}
81+
take_while(|c| ('A'..='Z').contains(&c));
82+
take_while(|c| (b'A'..=b'Z').contains(&c));
83+
}

tests/ui/manual_is_ascii_check.stderr

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,5 +133,47 @@ error: manual check for common ascii range
133133
LL | const BAR: bool = matches!('x', '0'..='9' | 'a'..='f' | 'A'..='F');
134134
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `'x'.is_ascii_hexdigit()`
135135

136-
error: aborting due to 22 previous errors
136+
error: manual check for common ascii range
137+
--> $DIR/manual_is_ascii_check.rs:62:5
138+
|
139+
LL | ('0'..='9').contains(&&cool_letter);
140+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `cool_letter.is_ascii_digit()`
141+
142+
error: manual check for common ascii range
143+
--> $DIR/manual_is_ascii_check.rs:63:5
144+
|
145+
LL | ('a'..='z').contains(*cool_letter);
146+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `cool_letter.is_ascii_lowercase()`
147+
148+
error: manual check for common ascii range
149+
--> $DIR/manual_is_ascii_check.rs:81:20
150+
|
151+
LL | take_while(|c| ('A'..='Z').contains(&c));
152+
| ^^^^^^^^^^^^^^^^^^^^^^^^
153+
|
154+
help: try
155+
|
156+
LL | take_while(|c| c.is_ascii_uppercase());
157+
| ~~~~~~~~~~~~~~~~~~~~~~
158+
help: also make sure to label the correct type
159+
|
160+
LL | take_while(|c: char| ('A'..='Z').contains(&c));
161+
| ~~~~~~~
162+
163+
error: manual check for common ascii range
164+
--> $DIR/manual_is_ascii_check.rs:82:20
165+
|
166+
LL | take_while(|c| (b'A'..=b'Z').contains(&c));
167+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
168+
|
169+
help: try
170+
|
171+
LL | take_while(|c| c.is_ascii_uppercase());
172+
| ~~~~~~~~~~~~~~~~~~~~~~
173+
help: also make sure to label the correct type
174+
|
175+
LL | take_while(|c: u8| (b'A'..=b'Z').contains(&c));
176+
| ~~~~~
177+
178+
error: aborting due to 26 previous errors
137179

0 commit comments

Comments
 (0)