@@ -74,6 +74,7 @@ mod option_map_unwrap_or;
7474mod or_fun_call;
7575mod or_then_unwrap;
7676mod path_buf_push_overwrite;
77+ mod path_ends_with_ext;
7778mod range_zip_with_len;
7879mod read_line_without_trim;
7980mod readonly_write_lock;
@@ -120,6 +121,8 @@ use clippy_utils::msrvs::{self, Msrv};
120121use clippy_utils:: ty:: { contains_ty_adt_constructor_opaque, implements_trait, is_copy, is_type_diagnostic_item} ;
121122use clippy_utils:: { contains_return, is_bool, is_trait_method, iter_input_pats, peel_blocks, return_ty} ;
122123use if_chain:: if_chain;
124+ pub use path_ends_with_ext:: DEFAULT_ALLOWED_DOTFILES ;
125+ use rustc_data_structures:: fx:: FxHashSet ;
123126use rustc_hir as hir;
124127use rustc_hir:: { Expr , ExprKind , Node , Stmt , StmtKind , TraitItem , TraitItemKind } ;
125128use rustc_hir_analysis:: hir_ty_to_ty;
@@ -3563,11 +3566,51 @@ declare_clippy_lint! {
35633566 "calls to `.take()` or `.skip()` that are out of bounds"
35643567}
35653568
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+
35663608pub struct Methods {
35673609 avoid_breaking_exported_api : bool ,
35683610 msrv : Msrv ,
35693611 allow_expect_in_tests : bool ,
35703612 allow_unwrap_in_tests : bool ,
3613+ allowed_dotfiles : FxHashSet < String > ,
35713614}
35723615
35733616impl Methods {
@@ -3577,12 +3620,14 @@ impl Methods {
35773620 msrv : Msrv ,
35783621 allow_expect_in_tests : bool ,
35793622 allow_unwrap_in_tests : bool ,
3623+ allowed_dotfiles : FxHashSet < String > ,
35803624 ) -> Self {
35813625 Self {
35823626 avoid_breaking_exported_api,
35833627 msrv,
35843628 allow_expect_in_tests,
35853629 allow_unwrap_in_tests,
3630+ allowed_dotfiles,
35863631 }
35873632 }
35883633}
@@ -3703,6 +3748,7 @@ impl_lint_pass!(Methods => [
37033748 FILTER_MAP_BOOL_THEN ,
37043749 READONLY_WRITE_LOCK ,
37053750 ITER_OUT_OF_BOUNDS ,
3751+ PATH_ENDS_WITH_EXT ,
37063752] ) ;
37073753
37083754/// Extracts a method call name, args, and `Span` of the method name.
@@ -3978,6 +4024,7 @@ impl Methods {
39784024 if let ExprKind :: MethodCall ( .., span) = expr. kind {
39794025 case_sensitive_file_extension_comparisons:: check ( cx, expr, span, recv, arg) ;
39804026 }
4027+ path_ends_with_ext:: check ( cx, recv, arg, expr, & self . msrv , & self . allowed_dotfiles ) ;
39814028 } ,
39824029 ( "expect" , [ _] ) => {
39834030 match method_call ( recv) {
0 commit comments