|
| 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 | +} |
0 commit comments