Skip to content

Commit 61f3d9d

Browse files
committed
Add case_sensitive_file_extensions lint
Closes #6425 Looks for ends_with methods calls with case sensitive extensions.
1 parent a6b72d3 commit 61f3d9d

6 files changed

+182
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1878,6 +1878,7 @@ Released 2018-09-13
18781878
[`boxed_local`]: https://rust-lang.github.io/rust-clippy/master/index.html#boxed_local
18791879
[`builtin_type_shadow`]: https://rust-lang.github.io/rust-clippy/master/index.html#builtin_type_shadow
18801880
[`cargo_common_metadata`]: https://rust-lang.github.io/rust-clippy/master/index.html#cargo_common_metadata
1881+
[`case_sensitive_file_extension_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#case_sensitive_file_extension_comparisons
18811882
[`cast_lossless`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_lossless
18821883
[`cast_possible_truncation`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_possible_truncation
18831884
[`cast_possible_wrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_possible_wrap

clippy_lints/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ rustc-semver="1.1.0"
3434
url = { version = "2.1.0", features = ["serde"] }
3535
quote = "1"
3636
syn = { version = "1", features = ["full"] }
37+
regex = "1.4"
38+
lazy_static = "1.4"
3739

3840
[features]
3941
deny-warnings = []
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use crate::utils::paths::STRING;
2+
use crate::utils::{match_def_path, span_lint_and_help};
3+
use if_chain::if_chain;
4+
use lazy_static::lazy_static;
5+
use regex::Regex;
6+
use rustc_ast::ast::LitKind;
7+
use rustc_hir::{Expr, ExprKind, PathSegment};
8+
use rustc_lint::{LateContext, LateLintPass};
9+
use rustc_middle::ty;
10+
use rustc_session::{declare_lint_pass, declare_tool_lint};
11+
use rustc_span::{source_map::Spanned, Span};
12+
13+
declare_clippy_lint! {
14+
/// **What it does:**
15+
/// Checks for calls to `ends_with` with possible file extensions
16+
/// and suggests to use a case-insensitive approach instead.
17+
///
18+
/// **Why is this bad?**
19+
/// `ends_with` is case-sensitive and may not detect files with a valid extension.
20+
///
21+
/// **Known problems:** None.
22+
///
23+
/// **Example:**
24+
///
25+
/// ```rust
26+
/// fn is_rust_file(filename: &str) -> bool {
27+
/// filename.ends_with(".rs")
28+
/// }
29+
/// ```
30+
/// Use instead:
31+
/// ```rust
32+
/// fn is_rust_file(filename: &str) -> bool {
33+
/// filename.rsplit('.').next().map(|ext| ext.eq_ignore_ascii_case("rs")) == Some(true)
34+
/// }
35+
/// ```
36+
pub CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS,
37+
pedantic,
38+
"default lint description"
39+
}
40+
41+
declare_lint_pass!(CaseSensitiveFileExtensionComparisons => [CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS]);
42+
43+
fn check_case_sensitive_file_extension_comparison(ctx: &LateContext<'_>, expr: &Expr<'_>) -> Option<Span> {
44+
lazy_static! {
45+
static ref RE: Regex = Regex::new(r"^\.([a-z0-9]{1,5}|[A-Z0-9]{1,5})$").unwrap();
46+
}
47+
if_chain! {
48+
if let ExprKind::MethodCall(PathSegment { ident, .. }, _, [obj, extension, ..], span) = expr.kind;
49+
if ident.as_str() == "ends_with";
50+
if let ExprKind::Lit(Spanned { node: LitKind::Str(ext_literal, ..), ..}) = extension.kind;
51+
if RE.is_match(&ext_literal.as_str());
52+
then {
53+
let mut ty = ctx.typeck_results().expr_ty(obj);
54+
ty = match ty.kind() {
55+
ty::Ref(_, ty, ..) => ty,
56+
_ => ty
57+
};
58+
59+
match ty.kind() {
60+
ty::Str => {
61+
return Some(span);
62+
},
63+
ty::Adt(&ty::AdtDef { did, .. }, _) => {
64+
if match_def_path(ctx, did, &STRING) {
65+
return Some(span);
66+
}
67+
},
68+
_ => { return None; }
69+
}
70+
}
71+
}
72+
None
73+
}
74+
75+
impl LateLintPass<'tcx> for CaseSensitiveFileExtensionComparisons {
76+
fn check_expr(&mut self, ctx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
77+
if let Some(span) = check_case_sensitive_file_extension_comparison(ctx, expr) {
78+
span_lint_and_help(
79+
ctx,
80+
CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS,
81+
span,
82+
"case-sensitive file extension comparison",
83+
None,
84+
"consider using a case-insensitive comparison instead",
85+
);
86+
}
87+
}
88+
}

clippy_lints/src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ mod blocks_in_if_conditions;
170170
mod booleans;
171171
mod bytecount;
172172
mod cargo_common_metadata;
173+
mod case_sensitive_file_extension_comparisons;
173174
mod checked_conversions;
174175
mod cognitive_complexity;
175176
mod collapsible_if;
@@ -556,6 +557,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
556557
&booleans::NONMINIMAL_BOOL,
557558
&bytecount::NAIVE_BYTECOUNT,
558559
&cargo_common_metadata::CARGO_COMMON_METADATA,
560+
&case_sensitive_file_extension_comparisons::CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS,
559561
&checked_conversions::CHECKED_CONVERSIONS,
560562
&cognitive_complexity::COGNITIVE_COMPLEXITY,
561563
&collapsible_if::COLLAPSIBLE_ELSE_IF,
@@ -1224,6 +1226,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
12241226
store.register_late_pass(|| box zero_sized_map_values::ZeroSizedMapValues);
12251227
store.register_late_pass(|| box vec_init_then_push::VecInitThenPush::default());
12261228
store.register_late_pass(move || box types::PtrAsPtr::new(msrv));
1229+
store.register_late_pass(|| box case_sensitive_file_extension_comparisons::CaseSensitiveFileExtensionComparisons);
12271230

12281231
store.register_group(true, "clippy::restriction", Some("clippy_restriction"), vec![
12291232
LintId::of(&arithmetic::FLOAT_ARITHMETIC),
@@ -1281,6 +1284,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
12811284
LintId::of(&await_holding_invalid::AWAIT_HOLDING_LOCK),
12821285
LintId::of(&await_holding_invalid::AWAIT_HOLDING_REFCELL_REF),
12831286
LintId::of(&bit_mask::VERBOSE_BIT_MASK),
1287+
LintId::of(&case_sensitive_file_extension_comparisons::CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS),
12841288
LintId::of(&checked_conversions::CHECKED_CONVERSIONS),
12851289
LintId::of(&copies::SAME_FUNCTIONS_IN_IF_CONDITION),
12861290
LintId::of(&copy_iterator::COPY_ITERATOR),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#![warn(clippy::case_sensitive_file_extension_comparisons)]
2+
3+
use std::string::String;
4+
5+
struct TestStruct {}
6+
7+
impl TestStruct {
8+
fn ends_with(self, arg: &str) {}
9+
}
10+
11+
fn is_rust_file(filename: &str) -> bool {
12+
filename.ends_with(".rs")
13+
}
14+
15+
fn main() {
16+
// std::string::String and &str should trigger the lint failure with .ext12
17+
let _ = String::from("").ends_with(".ext12");
18+
let _ = "str".ends_with(".ext12");
19+
20+
// The test struct should not trigger the lint failure with .ext12
21+
TestStruct {}.ends_with(".ext12");
22+
23+
// std::string::String and &str should trigger the lint failure with .EXT12
24+
let _ = String::from("").ends_with(".EXT12");
25+
let _ = "str".ends_with(".EXT12");
26+
27+
// The test struct should not trigger the lint failure with .EXT12
28+
TestStruct {}.ends_with(".EXT12");
29+
30+
// Should not trigger the lint failure with .eXT12
31+
let _ = String::from("").ends_with(".eXT12");
32+
let _ = "str".ends_with(".eXT12");
33+
TestStruct {}.ends_with(".eXT12");
34+
35+
// Should not trigger the lint failure with .EXT123 (too long)
36+
let _ = String::from("").ends_with(".EXT123");
37+
let _ = "str".ends_with(".EXT123");
38+
TestStruct {}.ends_with(".EXT123");
39+
40+
// Shouldn't fail if it doesn't start with a dot
41+
let _ = String::from("").ends_with("a.ext");
42+
let _ = "str".ends_with("a.extA");
43+
TestStruct {}.ends_with("a.ext");
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
error: case-sensitive file extension comparison
2+
--> $DIR/case_sensitive_file_extension_comparisons.rs:12:14
3+
|
4+
LL | filename.ends_with(".rs")
5+
| ^^^^^^^^^^^^^^^^
6+
|
7+
= note: `-D clippy::case-sensitive-file-extension-comparisons` implied by `-D warnings`
8+
= help: consider using a case-insensitive comparison instead
9+
10+
error: case-sensitive file extension comparison
11+
--> $DIR/case_sensitive_file_extension_comparisons.rs:17:30
12+
|
13+
LL | let _ = String::from("").ends_with(".ext12");
14+
| ^^^^^^^^^^^^^^^^^^^
15+
|
16+
= help: consider using a case-insensitive comparison instead
17+
18+
error: case-sensitive file extension comparison
19+
--> $DIR/case_sensitive_file_extension_comparisons.rs:18:19
20+
|
21+
LL | let _ = "str".ends_with(".ext12");
22+
| ^^^^^^^^^^^^^^^^^^^
23+
|
24+
= help: consider using a case-insensitive comparison instead
25+
26+
error: case-sensitive file extension comparison
27+
--> $DIR/case_sensitive_file_extension_comparisons.rs:24:30
28+
|
29+
LL | let _ = String::from("").ends_with(".EXT12");
30+
| ^^^^^^^^^^^^^^^^^^^
31+
|
32+
= help: consider using a case-insensitive comparison instead
33+
34+
error: case-sensitive file extension comparison
35+
--> $DIR/case_sensitive_file_extension_comparisons.rs:25:19
36+
|
37+
LL | let _ = "str".ends_with(".EXT12");
38+
| ^^^^^^^^^^^^^^^^^^^
39+
|
40+
= help: consider using a case-insensitive comparison instead
41+
42+
error: aborting due to 5 previous errors
43+

0 commit comments

Comments
 (0)