]>
Commit | Line | Data |
---|---|---|
f20569fa XL |
1 | use crate::utils::span_lint_and_help; |
2 | use if_chain::if_chain; | |
3 | use rustc_ast::ast::LitKind; | |
4 | use rustc_hir::{Expr, ExprKind, PathSegment}; | |
5 | use rustc_lint::{LateContext, LateLintPass}; | |
6 | use rustc_middle::ty; | |
7 | use rustc_session::{declare_lint_pass, declare_tool_lint}; | |
8 | use rustc_span::{source_map::Spanned, symbol::sym, Span}; | |
9 | ||
10 | declare_clippy_lint! { | |
11 | /// **What it does:** | |
12 | /// Checks for calls to `ends_with` with possible file extensions | |
13 | /// and suggests to use a case-insensitive approach instead. | |
14 | /// | |
15 | /// **Why is this bad?** | |
16 | /// `ends_with` is case-sensitive and may not detect files with a valid extension. | |
17 | /// | |
18 | /// **Known problems:** None. | |
19 | /// | |
20 | /// **Example:** | |
21 | /// | |
22 | /// ```rust | |
23 | /// fn is_rust_file(filename: &str) -> bool { | |
24 | /// filename.ends_with(".rs") | |
25 | /// } | |
26 | /// ``` | |
27 | /// Use instead: | |
28 | /// ```rust | |
29 | /// fn is_rust_file(filename: &str) -> bool { | |
30 | /// filename.rsplit('.').next().map(|ext| ext.eq_ignore_ascii_case("rs")) == Some(true) | |
31 | /// } | |
32 | /// ``` | |
33 | pub CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS, | |
34 | pedantic, | |
35 | "Checks for calls to ends_with with case-sensitive file extensions" | |
36 | } | |
37 | ||
38 | declare_lint_pass!(CaseSensitiveFileExtensionComparisons => [CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS]); | |
39 | ||
40 | fn check_case_sensitive_file_extension_comparison(ctx: &LateContext<'_>, expr: &Expr<'_>) -> Option<Span> { | |
41 | if_chain! { | |
42 | if let ExprKind::MethodCall(PathSegment { ident, .. }, _, [obj, extension, ..], span) = expr.kind; | |
43 | if ident.as_str() == "ends_with"; | |
44 | if let ExprKind::Lit(Spanned { node: LitKind::Str(ext_literal, ..), ..}) = extension.kind; | |
45 | if (2..=6).contains(&ext_literal.as_str().len()); | |
46 | if ext_literal.as_str().starts_with('.'); | |
47 | if ext_literal.as_str().chars().skip(1).all(|c| c.is_uppercase() || c.is_digit(10)) | |
48 | || ext_literal.as_str().chars().skip(1).all(|c| c.is_lowercase() || c.is_digit(10)); | |
49 | then { | |
50 | let mut ty = ctx.typeck_results().expr_ty(obj); | |
51 | ty = match ty.kind() { | |
52 | ty::Ref(_, ty, ..) => ty, | |
53 | _ => ty | |
54 | }; | |
55 | ||
56 | match ty.kind() { | |
57 | ty::Str => { | |
58 | return Some(span); | |
59 | }, | |
60 | ty::Adt(&ty::AdtDef { did, .. }, _) => { | |
61 | if ctx.tcx.is_diagnostic_item(sym::string_type, did) { | |
62 | return Some(span); | |
63 | } | |
64 | }, | |
65 | _ => { return None; } | |
66 | } | |
67 | } | |
68 | } | |
69 | None | |
70 | } | |
71 | ||
72 | impl LateLintPass<'tcx> for CaseSensitiveFileExtensionComparisons { | |
73 | fn check_expr(&mut self, ctx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { | |
74 | if let Some(span) = check_case_sensitive_file_extension_comparison(ctx, expr) { | |
75 | span_lint_and_help( | |
76 | ctx, | |
77 | CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS, | |
78 | span, | |
79 | "case-sensitive file extension comparison", | |
80 | None, | |
81 | "consider using a case-insensitive comparison instead", | |
82 | ); | |
83 | } | |
84 | } | |
85 | } |