@@ -74,6 +74,7 @@ mod option_map_unwrap_or;
74
74
mod or_fun_call;
75
75
mod or_then_unwrap;
76
76
mod path_buf_push_overwrite;
77
+ mod path_ends_with_ext;
77
78
mod range_zip_with_len;
78
79
mod read_line_without_trim;
79
80
mod readonly_write_lock;
@@ -120,6 +121,8 @@ use clippy_utils::msrvs::{self, Msrv};
120
121
use clippy_utils:: ty:: { contains_ty_adt_constructor_opaque, implements_trait, is_copy, is_type_diagnostic_item} ;
121
122
use clippy_utils:: { contains_return, is_bool, is_trait_method, iter_input_pats, peel_blocks, return_ty} ;
122
123
use if_chain:: if_chain;
124
+ pub use path_ends_with_ext:: DEFAULT_ALLOWED_DOTFILES ;
125
+ use rustc_data_structures:: fx:: FxHashSet ;
123
126
use rustc_hir as hir;
124
127
use rustc_hir:: { Expr , ExprKind , Node , Stmt , StmtKind , TraitItem , TraitItemKind } ;
125
128
use rustc_hir_analysis:: hir_ty_to_ty;
@@ -3563,11 +3566,51 @@ declare_clippy_lint! {
3563
3566
"calls to `.take()` or `.skip()` that are out of bounds"
3564
3567
}
3565
3568
3569
+ declare_clippy_lint ! {
3570
+ /// ### What it does
3571
+ /// Looks for calls to `Path::ends_with` calls where the argument looks like a file extension.
3572
+ ///
3573
+ /// By default, Clippy has a short list of known filenames that start with a dot
3574
+ /// but aren't necessarily file extensions (e.g. the `.git` folder), which are allowed by default.
3575
+ /// The `allowed-dotfiles` configuration can be used to allow additional
3576
+ /// file extensions that Clippy should not lint.
3577
+ ///
3578
+ /// ### Why is this bad?
3579
+ /// This doesn't actually compare file extensions. Rather, `ends_with` compares the given argument
3580
+ /// to the last **component** of the path and checks if it matches exactly.
3581
+ ///
3582
+ /// ### Known issues
3583
+ /// File extensions are often at most three characters long, so this only lints in those cases
3584
+ /// in an attempt to avoid false positives.
3585
+ /// Any extension names longer than that are assumed to likely be real path components and are
3586
+ /// therefore ignored.
3587
+ ///
3588
+ /// ### Example
3589
+ /// ```rust
3590
+ /// # use std::path::Path;
3591
+ /// fn is_markdown(path: &Path) -> bool {
3592
+ /// path.ends_with(".md")
3593
+ /// }
3594
+ /// ```
3595
+ /// Use instead:
3596
+ /// ```rust
3597
+ /// # use std::path::Path;
3598
+ /// fn is_markdown(path: &Path) -> bool {
3599
+ /// path.extension().is_some_and(|ext| ext == "md")
3600
+ /// }
3601
+ /// ```
3602
+ #[ clippy:: version = "1.74.0" ]
3603
+ pub PATH_ENDS_WITH_EXT ,
3604
+ suspicious,
3605
+ "attempting to compare file extensions using `Path::ends_with`"
3606
+ }
3607
+
3566
3608
pub struct Methods {
3567
3609
avoid_breaking_exported_api : bool ,
3568
3610
msrv : Msrv ,
3569
3611
allow_expect_in_tests : bool ,
3570
3612
allow_unwrap_in_tests : bool ,
3613
+ allowed_dotfiles : FxHashSet < String > ,
3571
3614
}
3572
3615
3573
3616
impl Methods {
@@ -3577,12 +3620,14 @@ impl Methods {
3577
3620
msrv : Msrv ,
3578
3621
allow_expect_in_tests : bool ,
3579
3622
allow_unwrap_in_tests : bool ,
3623
+ allowed_dotfiles : FxHashSet < String > ,
3580
3624
) -> Self {
3581
3625
Self {
3582
3626
avoid_breaking_exported_api,
3583
3627
msrv,
3584
3628
allow_expect_in_tests,
3585
3629
allow_unwrap_in_tests,
3630
+ allowed_dotfiles,
3586
3631
}
3587
3632
}
3588
3633
}
@@ -3703,6 +3748,7 @@ impl_lint_pass!(Methods => [
3703
3748
FILTER_MAP_BOOL_THEN ,
3704
3749
READONLY_WRITE_LOCK ,
3705
3750
ITER_OUT_OF_BOUNDS ,
3751
+ PATH_ENDS_WITH_EXT ,
3706
3752
] ) ;
3707
3753
3708
3754
/// Extracts a method call name, args, and `Span` of the method name.
@@ -3978,6 +4024,7 @@ impl Methods {
3978
4024
if let ExprKind :: MethodCall ( .., span) = expr. kind {
3979
4025
case_sensitive_file_extension_comparisons:: check ( cx, expr, span, recv, arg) ;
3980
4026
}
4027
+ path_ends_with_ext:: check ( cx, recv, arg, expr, & self . msrv , & self . allowed_dotfiles ) ;
3981
4028
} ,
3982
4029
( "expect" , [ _] ) => {
3983
4030
match method_call ( recv) {
0 commit comments