Skip to content

Commit d7681e0

Browse files
committed
Add case_sensitive_file_extensions lint
Closes rust-lang#6425 Looks for ends_with methods calls with case sensitive extensions.
1 parent 25e9acb commit d7681e0

6 files changed

+182
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1963,6 +1963,7 @@ Released 2018-09-13
19631963
[`mutable_key_type`]: https://rust-lang.github.io/rust-clippy/master/index.html#mutable_key_type
19641964
[`mutex_atomic`]: https://rust-lang.github.io/rust-clippy/master/index.html#mutex_atomic
19651965
[`mutex_integer`]: https://rust-lang.github.io/rust-clippy/master/index.html#mutex_integer
1966+
[`case_sensitive_file_extensions`]: https://rust-lang.github.io/rust-clippy/master/index.html#case_sensitive_file_extensions
19661967
[`naive_bytecount`]: https://rust-lang.github.io/rust-clippy/master/index.html#naive_bytecount
19671968
[`needless_arbitrary_self_type`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_arbitrary_self_type
19681969
[`needless_bool`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_bool

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_EXTENSIONS,
37+
pedantic,
38+
"default lint description"
39+
}
40+
41+
declare_lint_pass!(CaseSensitiveFileExtensions => [CASE_SENSITIVE_FILE_EXTENSIONS]);
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 CaseSensitiveFileExtensions {
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_EXTENSIONS,
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_extensions;
173174
mod checked_conversions;
174175
mod cognitive_complexity;
175176
mod collapsible_if;
@@ -792,6 +793,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
792793
&mutable_debug_assertion::DEBUG_ASSERT_WITH_MUT_CALL,
793794
&mutex_atomic::MUTEX_ATOMIC,
794795
&mutex_atomic::MUTEX_INTEGER,
796+
&case_sensitive_file_extensions::CASE_SENSITIVE_FILE_EXTENSIONS,
795797
&needless_arbitrary_self_type::NEEDLESS_ARBITRARY_SELF_TYPE,
796798
&needless_bool::BOOL_COMPARISON,
797799
&needless_bool::NEEDLESS_BOOL,
@@ -1215,6 +1217,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
12151217
store.register_late_pass(|| box strings::StrToString);
12161218
store.register_late_pass(|| box strings::StringToString);
12171219
store.register_late_pass(|| box zero_sized_map_values::ZeroSizedMapValues);
1220+
store.register_late_pass(|| box case_sensitive_file_extensions::CaseSensitiveFileExtensions);
12181221

12191222
store.register_group(true, "clippy::restriction", Some("clippy_restriction"), vec![
12201223
LintId::of(&arithmetic::FLOAT_ARITHMETIC),
@@ -1955,6 +1958,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
19551958
LintId::of(&missing_const_for_fn::MISSING_CONST_FOR_FN),
19561959
LintId::of(&mutable_debug_assertion::DEBUG_ASSERT_WITH_MUT_CALL),
19571960
LintId::of(&mutex_atomic::MUTEX_INTEGER),
1961+
LintId::of(&case_sensitive_file_extensions::CASE_SENSITIVE_FILE_EXTENSIONS),
19581962
LintId::of(&needless_borrow::NEEDLESS_BORROW),
19591963
LintId::of(&path_buf_push_overwrite::PATH_BUF_PUSH_OVERWRITE),
19601964
LintId::of(&redundant_pub_crate::REDUNDANT_PUB_CRATE),
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#![warn(clippy::case_sensitive_file_extensions)]
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_extensions.rs:12:14
3+
|
4+
LL | filename.ends_with(".rs")
5+
| ^^^^^^^^^^^^^^^^
6+
|
7+
= note: `-D clippy::case-sensitive-file-extensions` 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_extensions.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_extensions.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_extensions.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_extensions.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)