Skip to content

Commit

Permalink
Auto merge of #7806 - Serial-ATA:lint-match-case-mismatch, r=llogiq
Browse files Browse the repository at this point in the history
Add match_str_case_mismatch lint

changelog: Added a new lint [`match_str_case_mismatch`]

Fixes #7440
  • Loading branch information
bors committed Oct 12, 2021
2 parents 5c97b27 + 0c99de0 commit d3905af
Show file tree
Hide file tree
Showing 8 changed files with 311 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2837,6 +2837,7 @@ Released 2018-09-13
[`match_result_ok`]: https://rust-lang.github.io/rust-clippy/master/index.html#match_result_ok
[`match_same_arms`]: https://rust-lang.github.io/rust-clippy/master/index.html#match_same_arms
[`match_single_binding`]: https://rust-lang.github.io/rust-clippy/master/index.html#match_single_binding
[`match_str_case_mismatch`]: https://rust-lang.github.io/rust-clippy/master/index.html#match_str_case_mismatch
[`match_wild_err_arm`]: https://rust-lang.github.io/rust-clippy/master/index.html#match_wild_err_arm
[`match_wildcard_for_single_variants`]: https://rust-lang.github.io/rust-clippy/master/index.html#match_wildcard_for_single_variants
[`maybe_infinite_iter`]: https://rust-lang.github.io/rust-clippy/master/index.html#maybe_infinite_iter
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/lib.register_all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
LintId::of(map_unit_fn::OPTION_MAP_UNIT_FN),
LintId::of(map_unit_fn::RESULT_MAP_UNIT_FN),
LintId::of(match_result_ok::MATCH_RESULT_OK),
LintId::of(match_str_case_mismatch::MATCH_STR_CASE_MISMATCH),
LintId::of(matches::INFALLIBLE_DESTRUCTURING_MATCH),
LintId::of(matches::MATCH_AS_REF),
LintId::of(matches::MATCH_LIKE_MATCHES_MACRO),
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/lib.register_correctness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ store.register_group(true, "clippy::correctness", Some("clippy_correctness"), ve
LintId::of(loops::ITER_NEXT_LOOP),
LintId::of(loops::NEVER_LOOP),
LintId::of(loops::WHILE_IMMUTABLE_CONDITION),
LintId::of(match_str_case_mismatch::MATCH_STR_CASE_MISMATCH),
LintId::of(mem_discriminant::MEM_DISCRIMINANT_NON_ENUM),
LintId::of(mem_replace::MEM_REPLACE_WITH_UNINIT),
LintId::of(methods::CLONE_DOUBLE_REF),
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/lib.register_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ store.register_lints(&[
map_unit_fn::RESULT_MAP_UNIT_FN,
match_on_vec_items::MATCH_ON_VEC_ITEMS,
match_result_ok::MATCH_RESULT_OK,
match_str_case_mismatch::MATCH_STR_CASE_MISMATCH,
matches::INFALLIBLE_DESTRUCTURING_MATCH,
matches::MATCH_AS_REF,
matches::MATCH_BOOL,
Expand Down
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ mod map_err_ignore;
mod map_unit_fn;
mod match_on_vec_items;
mod match_result_ok;
mod match_str_case_mismatch;
mod matches;
mod mem_discriminant;
mod mem_forget;
Expand Down Expand Up @@ -771,6 +772,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
let enable_raw_pointer_heuristic_for_send = conf.enable_raw_pointer_heuristic_for_send;
store.register_late_pass(move || Box::new(non_send_fields_in_send_ty::NonSendFieldInSendTy::new(enable_raw_pointer_heuristic_for_send)));
store.register_late_pass(move || Box::new(undocumented_unsafe_blocks::UndocumentedUnsafeBlocks::default()));
store.register_late_pass(|| Box::new(match_str_case_mismatch::MatchStrCaseMismatch));
}

#[rustfmt::skip]
Expand Down
171 changes: 171 additions & 0 deletions clippy_lints/src/match_str_case_mismatch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::ty::is_type_diagnostic_item;
use rustc_ast::ast::LitKind;
use rustc_errors::Applicability;
use rustc_hir::intravisit::{walk_expr, NestedVisitorMap, Visitor};
use rustc_hir::{Arm, Expr, ExprKind, MatchSource, PatKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::hir::map::Map;
use rustc_middle::lint::in_external_macro;
use rustc_middle::ty;
use rustc_session::{declare_lint_pass, declare_tool_lint};
use rustc_span::symbol::SymbolStr;
use rustc_span::{sym, Span};

declare_clippy_lint! {
/// ### What it does
/// Checks for `match` expressions modifying the case of a string with non-compliant arms
///
/// ### Why is this bad?
/// The arm is unreachable, which is likely a mistake
///
/// ### Example
/// ```rust
/// # let text = "Foo";
///
/// match &*text.to_ascii_lowercase() {
/// "foo" => {},
/// "Bar" => {},
/// _ => {},
/// }
/// ```
/// Use instead:
/// ```rust
/// # let text = "Foo";
///
/// match &*text.to_ascii_lowercase() {
/// "foo" => {},
/// "bar" => {},
/// _ => {},
/// }
/// ```
pub MATCH_STR_CASE_MISMATCH,
correctness,
"creation of a case altering match expression with non-compliant arms"
}

declare_lint_pass!(MatchStrCaseMismatch => [MATCH_STR_CASE_MISMATCH]);

#[derive(Debug)]
enum CaseMethod {
LowerCase,
AsciiLowerCase,
UpperCase,
AsciiUppercase,
}

impl LateLintPass<'_> for MatchStrCaseMismatch {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
if_chain! {
if !in_external_macro(cx.tcx.sess, expr.span);
if let ExprKind::Match(match_expr, arms, MatchSource::Normal) = expr.kind;
if let ty::Ref(_, ty, _) = cx.typeck_results().expr_ty(match_expr).kind();
if let ty::Str = ty.kind();
then {
let mut visitor = MatchExprVisitor {
cx,
case_method: None,
};

visitor.visit_expr(match_expr);

if let Some(case_method) = visitor.case_method {
if let Some((bad_case_span, bad_case_str)) = verify_case(&case_method, arms) {
lint(cx, &case_method, bad_case_span, &bad_case_str);
}
}
}
}
}
}

struct MatchExprVisitor<'a, 'tcx> {
cx: &'a LateContext<'tcx>,
case_method: Option<CaseMethod>,
}

impl<'a, 'tcx> Visitor<'tcx> for MatchExprVisitor<'a, 'tcx> {
type Map = Map<'tcx>;

fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
NestedVisitorMap::None
}

fn visit_expr(&mut self, ex: &'tcx Expr<'_>) {
match ex.kind {
ExprKind::MethodCall(segment, _, [receiver], _)
if self.case_altered(&*segment.ident.as_str(), receiver) => {},
_ => walk_expr(self, ex),
}
}
}

impl<'a, 'tcx> MatchExprVisitor<'a, 'tcx> {
fn case_altered(&mut self, segment_ident: &str, receiver: &Expr<'_>) -> bool {
if let Some(case_method) = get_case_method(segment_ident) {
let ty = self.cx.typeck_results().expr_ty(receiver).peel_refs();

if is_type_diagnostic_item(self.cx, ty, sym::String) || ty.kind() == &ty::Str {
self.case_method = Some(case_method);
return true;
}
}

false
}
}

fn get_case_method(segment_ident_str: &str) -> Option<CaseMethod> {
match segment_ident_str {
"to_lowercase" => Some(CaseMethod::LowerCase),
"to_ascii_lowercase" => Some(CaseMethod::AsciiLowerCase),
"to_uppercase" => Some(CaseMethod::UpperCase),
"to_ascii_uppercase" => Some(CaseMethod::AsciiUppercase),
_ => None,
}
}

fn verify_case<'a>(case_method: &'a CaseMethod, arms: &'a [Arm<'_>]) -> Option<(Span, SymbolStr)> {
let case_check = match case_method {
CaseMethod::LowerCase => |input: &str| -> bool { input.chars().all(char::is_lowercase) },
CaseMethod::AsciiLowerCase => |input: &str| -> bool { input.chars().all(|c| matches!(c, 'a'..='z')) },
CaseMethod::UpperCase => |input: &str| -> bool { input.chars().all(char::is_uppercase) },
CaseMethod::AsciiUppercase => |input: &str| -> bool { input.chars().all(|c| matches!(c, 'A'..='Z')) },
};

for arm in arms {
if_chain! {
if let PatKind::Lit(Expr {
kind: ExprKind::Lit(lit),
..
}) = arm.pat.kind;
if let LitKind::Str(symbol, _) = lit.node;
let input = symbol.as_str();
if !case_check(&input);
then {
return Some((lit.span, input));
}
}
}

None
}

fn lint(cx: &LateContext<'_>, case_method: &CaseMethod, bad_case_span: Span, bad_case_str: &str) {
let (method_str, suggestion) = match case_method {
CaseMethod::LowerCase => ("to_lower_case", bad_case_str.to_lowercase()),
CaseMethod::AsciiLowerCase => ("to_ascii_lowercase", bad_case_str.to_ascii_lowercase()),
CaseMethod::UpperCase => ("to_uppercase", bad_case_str.to_uppercase()),
CaseMethod::AsciiUppercase => ("to_ascii_uppercase", bad_case_str.to_ascii_uppercase()),
};

span_lint_and_sugg(
cx,
MATCH_STR_CASE_MISMATCH,
bad_case_span,
"this `match` arm has a differing case than its expression",
&*format!("consider changing the case of this arm to respect `{}`", method_str),
format!("\"{}\"", suggestion),
Applicability::MachineApplicable,
);
}
98 changes: 98 additions & 0 deletions tests/ui/match_str_case_mismatch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#![warn(clippy::match_str_case_mismatch)]

// Valid

fn as_str_match() {
let var = "BAR";

match var.to_ascii_lowercase().as_str() {
"foo" => {},
"bar" => {},
_ => {},
}
}

fn addrof_unary_match() {
let var = "BAR";

match &*var.to_ascii_lowercase() {
"foo" => {},
"bar" => {},
_ => {},
}
}

fn alternating_chain() {
let var = "BAR";

match &*var
.to_ascii_lowercase()
.to_uppercase()
.to_lowercase()
.to_ascii_uppercase()
{
"FOO" => {},
"BAR" => {},
_ => {},
}
}

fn unrelated_method() {
struct Item {
a: String,
}

impl Item {
#[allow(clippy::wrong_self_convention)]
fn to_lowercase(self) -> String {
self.a
}
}

let item = Item { a: String::from("BAR") };

match &*item.to_lowercase() {
"FOO" => {},
"BAR" => {},
_ => {},
}
}

// Invalid

fn as_str_match_mismatch() {
let var = "BAR";

match var.to_ascii_lowercase().as_str() {
"foo" => {},
"Bar" => {},
_ => {},
}
}

fn addrof_unary_match_mismatch() {
let var = "BAR";

match &*var.to_ascii_lowercase() {
"foo" => {},
"Bar" => {},
_ => {},
}
}

fn alternating_chain_mismatch() {
let var = "BAR";

match &*var
.to_ascii_lowercase()
.to_uppercase()
.to_lowercase()
.to_ascii_uppercase()
{
"FOO" => {},
"bAR" => {},
_ => {},
}
}

fn main() {}
36 changes: 36 additions & 0 deletions tests/ui/match_str_case_mismatch.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
error: this `match` arm has a differing case than its expression
--> $DIR/match_str_case_mismatch.rs:68:9
|
LL | "Bar" => {},
| ^^^^^
|
= note: `-D clippy::match-str-case-mismatch` implied by `-D warnings`
help: consider changing the case of this arm to respect `to_ascii_lowercase`
|
LL | "bar" => {},
| ~~~~~

error: this `match` arm has a differing case than its expression
--> $DIR/match_str_case_mismatch.rs:78:9
|
LL | "Bar" => {},
| ^^^^^
|
help: consider changing the case of this arm to respect `to_ascii_lowercase`
|
LL | "bar" => {},
| ~~~~~

error: this `match` arm has a differing case than its expression
--> $DIR/match_str_case_mismatch.rs:93:9
|
LL | "bAR" => {},
| ^^^^^
|
help: consider changing the case of this arm to respect `to_ascii_uppercase`
|
LL | "BAR" => {},
| ~~~~~

error: aborting due to 3 previous errors

0 comments on commit d3905af

Please sign in to comment.