Skip to content

Commit 9e0ce14

Browse files
committed
Add match_str_case_mismatch lint
1 parent 7272366 commit 9e0ce14

8 files changed

+323
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2837,6 +2837,7 @@ Released 2018-09-13
28372837
[`match_result_ok`]: https://rust-lang.github.io/rust-clippy/master/index.html#match_result_ok
28382838
[`match_same_arms`]: https://rust-lang.github.io/rust-clippy/master/index.html#match_same_arms
28392839
[`match_single_binding`]: https://rust-lang.github.io/rust-clippy/master/index.html#match_single_binding
2840+
[`match_str_case_mismatch`]: https://rust-lang.github.io/rust-clippy/master/index.html#match_str_case_mismatch
28402841
[`match_wild_err_arm`]: https://rust-lang.github.io/rust-clippy/master/index.html#match_wild_err_arm
28412842
[`match_wildcard_for_single_variants`]: https://rust-lang.github.io/rust-clippy/master/index.html#match_wildcard_for_single_variants
28422843
[`maybe_infinite_iter`]: https://rust-lang.github.io/rust-clippy/master/index.html#maybe_infinite_iter

clippy_lints/src/lib.register_all.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
118118
LintId::of(map_unit_fn::OPTION_MAP_UNIT_FN),
119119
LintId::of(map_unit_fn::RESULT_MAP_UNIT_FN),
120120
LintId::of(match_result_ok::MATCH_RESULT_OK),
121+
LintId::of(match_str_case_mismatch::MATCH_STR_CASE_MISMATCH),
121122
LintId::of(matches::INFALLIBLE_DESTRUCTURING_MATCH),
122123
LintId::of(matches::MATCH_AS_REF),
123124
LintId::of(matches::MATCH_LIKE_MATCHES_MACRO),

clippy_lints/src/lib.register_correctness.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ store.register_group(true, "clippy::correctness", Some("clippy_correctness"), ve
3636
LintId::of(loops::ITER_NEXT_LOOP),
3737
LintId::of(loops::NEVER_LOOP),
3838
LintId::of(loops::WHILE_IMMUTABLE_CONDITION),
39+
LintId::of(match_str_case_mismatch::MATCH_STR_CASE_MISMATCH),
3940
LintId::of(mem_discriminant::MEM_DISCRIMINANT_NON_ENUM),
4041
LintId::of(mem_replace::MEM_REPLACE_WITH_UNINIT),
4142
LintId::of(methods::CLONE_DOUBLE_REF),

clippy_lints/src/lib.register_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ store.register_lints(&[
226226
map_unit_fn::RESULT_MAP_UNIT_FN,
227227
match_on_vec_items::MATCH_ON_VEC_ITEMS,
228228
match_result_ok::MATCH_RESULT_OK,
229+
match_str_case_mismatch::MATCH_STR_CASE_MISMATCH,
229230
matches::INFALLIBLE_DESTRUCTURING_MATCH,
230231
matches::MATCH_AS_REF,
231232
matches::MATCH_BOOL,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ mod map_err_ignore;
265265
mod map_unit_fn;
266266
mod match_on_vec_items;
267267
mod match_result_ok;
268+
mod match_str_case_mismatch;
268269
mod matches;
269270
mod mem_discriminant;
270271
mod mem_forget;
@@ -771,6 +772,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
771772
let enable_raw_pointer_heuristic_for_send = conf.enable_raw_pointer_heuristic_for_send;
772773
store.register_late_pass(move || Box::new(non_send_fields_in_send_ty::NonSendFieldInSendTy::new(enable_raw_pointer_heuristic_for_send)));
773774
store.register_late_pass(move || Box::new(undocumented_unsafe_blocks::UndocumentedUnsafeBlocks::default()));
775+
store.register_late_pass(|| Box::new(match_str_case_mismatch::MatchStrCaseMismatch));
774776
}
775777

776778
#[rustfmt::skip]
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
use clippy_utils::diagnostics::span_lint_and_help;
2+
use clippy_utils::ty::is_type_diagnostic_item;
3+
use rustc_ast::ast::LitKind;
4+
use rustc_hir::intravisit::{walk_expr, NestedVisitorMap, Visitor};
5+
use rustc_hir::{Arm, Expr, ExprKind, MatchSource, PatKind};
6+
use rustc_lint::{LateContext, LateLintPass};
7+
use rustc_middle::hir::map::Map;
8+
use rustc_middle::lint::in_external_macro;
9+
use rustc_middle::ty;
10+
use rustc_session::{declare_lint_pass, declare_tool_lint};
11+
use rustc_span::{sym, Span};
12+
13+
declare_clippy_lint! {
14+
/// ### What it does
15+
/// Checks for `match` expressions modifying the case of a string with non-compliant arms
16+
///
17+
/// ### Why is this bad?
18+
/// The arm is unreachable, which is likely a mistake
19+
///
20+
/// ### Example
21+
/// ```rust
22+
/// match &*text.to_ascii_lowercase() {
23+
/// "foo" => {},
24+
/// "Bar" => {},
25+
/// _ => {},
26+
/// }
27+
/// ```
28+
/// Use instead:
29+
/// ```rust
30+
/// match &*text.to_ascii_lowercase() {
31+
/// "foo" => {},
32+
/// "bar" => {},
33+
/// _ => {},
34+
/// }
35+
/// ```
36+
pub MATCH_STR_CASE_MISMATCH,
37+
correctness,
38+
"creation of a case altering match expression with non-compliant arms"
39+
}
40+
41+
declare_lint_pass!(MatchStrCaseMismatch => [MATCH_STR_CASE_MISMATCH]);
42+
43+
#[derive(Debug)]
44+
enum CaseMethod {
45+
LowerCase,
46+
AsciiLowerCase,
47+
UpperCase,
48+
AsciiUppercase,
49+
}
50+
51+
impl LateLintPass<'_> for MatchStrCaseMismatch {
52+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
53+
if_chain! {
54+
if !in_external_macro(cx.tcx.sess, expr.span);
55+
if let ExprKind::Match(match_expr, arms, MatchSource::Normal) = expr.kind;
56+
if let ty::Ref(_, ty, _) = cx.typeck_results().expr_ty(match_expr).kind();
57+
if let ty::Str = ty.kind();
58+
then {
59+
let mut visitor = MatchExprVisitor {
60+
cx,
61+
case_method: None,
62+
};
63+
64+
visitor.visit_expr(match_expr);
65+
66+
if let Some(case_method) = visitor.case_method {
67+
if let Some(bad_case) = verify_case(&case_method, arms) {
68+
lint(cx, expr.span, &case_method, bad_case);
69+
}
70+
}
71+
}
72+
}
73+
}
74+
}
75+
76+
struct MatchExprVisitor<'a, 'tcx> {
77+
cx: &'a LateContext<'tcx>,
78+
case_method: Option<CaseMethod>,
79+
}
80+
81+
impl<'a, 'tcx> Visitor<'tcx> for MatchExprVisitor<'a, 'tcx> {
82+
type Map = Map<'tcx>;
83+
84+
fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
85+
NestedVisitorMap::None
86+
}
87+
88+
fn visit_expr(&mut self, ex: &'tcx Expr<'_>) {
89+
match ex.kind {
90+
ExprKind::MethodCall(segment, _, [receiver], _)
91+
if self.case_altered(&*segment.ident.as_str(), receiver) => {},
92+
_ => walk_expr(self, ex),
93+
}
94+
}
95+
}
96+
97+
impl<'a, 'tcx> MatchExprVisitor<'a, 'tcx> {
98+
fn case_altered(&mut self, segment_ident: &str, receiver: &Expr<'_>) -> bool {
99+
if let Some(case_method) = get_case_method(segment_ident) {
100+
let ty = self.cx.typeck_results().expr_ty(receiver).peel_refs();
101+
102+
if is_type_diagnostic_item(self.cx, ty, sym::String) || ty.kind() == &ty::Str {
103+
self.case_method = Some(case_method);
104+
return true;
105+
}
106+
}
107+
108+
false
109+
}
110+
}
111+
112+
fn get_case_method(segment_ident_str: &str) -> Option<CaseMethod> {
113+
match segment_ident_str {
114+
"to_lowercase" => Some(CaseMethod::LowerCase),
115+
"to_ascii_lowercase" => Some(CaseMethod::AsciiLowerCase),
116+
"to_uppercase" => Some(CaseMethod::UpperCase),
117+
"to_ascii_uppercase" => Some(CaseMethod::AsciiUppercase),
118+
_ => None,
119+
}
120+
}
121+
122+
fn verify_case(case_method: &CaseMethod, arms: &'_ [Arm<'_>]) -> Option<Span> {
123+
let mut bad_case = None;
124+
125+
let case_check = match case_method {
126+
CaseMethod::LowerCase => |input: &str| -> bool { input.chars().all(char::is_lowercase) },
127+
CaseMethod::AsciiLowerCase => |input: &str| -> bool { input.chars().all(|c| matches!(c, 'a'..='z')) },
128+
CaseMethod::UpperCase => |input: &str| -> bool { input.chars().all(char::is_uppercase) },
129+
CaseMethod::AsciiUppercase => |input: &str| -> bool { input.chars().all(|c| matches!(c, 'A'..='Z')) },
130+
};
131+
132+
for arm in arms {
133+
if_chain! {
134+
if let PatKind::Lit(Expr {
135+
kind: ExprKind::Lit(lit),
136+
..
137+
}) = arm.pat.kind;
138+
if let LitKind::Str(symbol, _) = lit.node;
139+
if !case_check(&symbol.as_str());
140+
then {
141+
bad_case = Some(lit.span);
142+
break;
143+
}
144+
}
145+
}
146+
147+
bad_case
148+
}
149+
150+
fn lint(cx: &LateContext<'_>, expr_span: Span, case_method: &CaseMethod, bad_case_span: Span) {
151+
let method_str = match case_method {
152+
CaseMethod::LowerCase => "to_lower_case",
153+
CaseMethod::AsciiLowerCase => "to_ascii_lowercase",
154+
CaseMethod::UpperCase => "to_uppercase",
155+
CaseMethod::AsciiUppercase => "to_ascii_uppercase",
156+
};
157+
158+
span_lint_and_help(
159+
cx,
160+
MATCH_STR_CASE_MISMATCH,
161+
expr_span,
162+
"this `match` expression alters case, but has non-compliant arms",
163+
Some(bad_case_span),
164+
&*format!("consider changing the case of this arm to respect `{}`", method_str),
165+
);
166+
}

tests/ui/match_str_case_mismatch.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#![warn(clippy::match_str_case_mismatch)]
2+
3+
// Valid
4+
5+
fn as_str_match() {
6+
let var = "BAR";
7+
8+
match var.to_ascii_lowercase().as_str() {
9+
"foo" => {},
10+
"bar" => {},
11+
_ => {},
12+
}
13+
}
14+
15+
fn addrof_unary_match() {
16+
let var = "BAR";
17+
18+
match &*var.to_ascii_lowercase() {
19+
"foo" => {},
20+
"bar" => {},
21+
_ => {},
22+
}
23+
}
24+
25+
fn alternating_chain() {
26+
let var = "BAR";
27+
28+
match &*var
29+
.to_ascii_lowercase()
30+
.to_uppercase()
31+
.to_lowercase()
32+
.to_ascii_uppercase()
33+
{
34+
"FOO" => {},
35+
"BAR" => {},
36+
_ => {},
37+
}
38+
}
39+
40+
fn unrelated_method() {
41+
struct Item {
42+
a: String,
43+
}
44+
45+
impl Item {
46+
#[allow(clippy::wrong_self_convention)]
47+
fn to_lowercase(self) -> String {
48+
self.a
49+
}
50+
}
51+
52+
let item = Item { a: String::from("BAR") };
53+
54+
match &*item.to_lowercase() {
55+
"FOO" => {},
56+
"BAR" => {},
57+
_ => {},
58+
}
59+
}
60+
61+
// Invalid
62+
63+
fn as_str_match_mismatch() {
64+
let var = "BAR";
65+
66+
match var.to_ascii_lowercase().as_str() {
67+
"foo" => {},
68+
"Bar" => {},
69+
_ => {},
70+
}
71+
}
72+
73+
fn addrof_unary_match_mismatch() {
74+
let var = "BAR";
75+
76+
match &*var.to_ascii_lowercase() {
77+
"foo" => {},
78+
"Bar" => {},
79+
_ => {},
80+
}
81+
}
82+
83+
fn alternating_chain_mismatch() {
84+
let var = "BAR";
85+
86+
match &*var
87+
.to_ascii_lowercase()
88+
.to_uppercase()
89+
.to_lowercase()
90+
.to_ascii_uppercase()
91+
{
92+
"FOO" => {},
93+
"bAR" => {},
94+
_ => {},
95+
}
96+
}
97+
98+
fn main() {}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
error: this `match` expression alters case, but has non-compliant arms
2+
--> $DIR/match_str_case_mismatch.rs:66:5
3+
|
4+
LL | / match var.to_ascii_lowercase().as_str() {
5+
LL | | "foo" => {},
6+
LL | | "Bar" => {},
7+
LL | | _ => {},
8+
LL | | }
9+
| |_____^
10+
|
11+
= note: `-D clippy::match-str-case-mismatch` implied by `-D warnings`
12+
help: consider changing the case of this arm to respect `to_ascii_lowercase`
13+
--> $DIR/match_str_case_mismatch.rs:68:9
14+
|
15+
LL | "Bar" => {},
16+
| ^^^^^
17+
18+
error: this `match` expression alters case, but has non-compliant arms
19+
--> $DIR/match_str_case_mismatch.rs:76:5
20+
|
21+
LL | / match &*var.to_ascii_lowercase() {
22+
LL | | "foo" => {},
23+
LL | | "Bar" => {},
24+
LL | | _ => {},
25+
LL | | }
26+
| |_____^
27+
|
28+
help: consider changing the case of this arm to respect `to_ascii_lowercase`
29+
--> $DIR/match_str_case_mismatch.rs:78:9
30+
|
31+
LL | "Bar" => {},
32+
| ^^^^^
33+
34+
error: this `match` expression alters case, but has non-compliant arms
35+
--> $DIR/match_str_case_mismatch.rs:86:5
36+
|
37+
LL | / match &*var
38+
LL | | .to_ascii_lowercase()
39+
LL | | .to_uppercase()
40+
LL | | .to_lowercase()
41+
... |
42+
LL | | _ => {},
43+
LL | | }
44+
| |_____^
45+
|
46+
help: consider changing the case of this arm to respect `to_ascii_uppercase`
47+
--> $DIR/match_str_case_mismatch.rs:93:9
48+
|
49+
LL | "bAR" => {},
50+
| ^^^^^
51+
52+
error: aborting due to 3 previous errors
53+

0 commit comments

Comments
 (0)