about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2023-11-20 12:30:03 +0000
committerbors <bors@rust-lang.org>2023-11-20 12:30:03 +0000
commit11a2eb03fa55d38e6d55079ba48573d335d0fbb6 (patch)
tree6acbe5a25effde2cb68a27b33eb71805858a9cca
parent41140e3cb8642abb12404520ac95c4ac82ead73f (diff)
parent34d9e88a4739a33d0d570947691c78d6980c48a9 (diff)
downloadrust-11a2eb03fa55d38e6d55079ba48573d335d0fbb6.tar.gz
rust-11a2eb03fa55d38e6d55079ba48573d335d0fbb6.zip
Auto merge of #11453 - xFrednet:11208-path-join-correct, r=blyxyas
New lint `clippy::join_absolute_paths`

Hey `@ofeeg,` this PR is a copy of all the changes you did in https://github.com/rust-lang/rust-clippy/commit/47171d3c6366dd08476b2ab1e1661950d13a4218 from PR https://github.com/rust-lang/rust-clippy/pull/11208. I wasn't sure how to fix the git history. Hope you're okay with me figuring this out in this separate PR

---

changelog: New lint [`join_absolute_paths`]
[#11453](https://github.com/rust-lang/rust-clippy/pull/11453)

Closes: https://github.com/rust-lang/rust-clippy/issues/10655

r? `@Centri3` Since you also gave feedback on the other PR. I hope that I copied everything correctly, but a third pair of eyes would be appreciated :D
-rw-r--r--CHANGELOG.md1
-rw-r--r--clippy_lints/src/declared_lints.rs1
-rw-r--r--clippy_lints/src/methods/join_absolute_paths.rs52
-rw-r--r--clippy_lints/src/methods/mod.rs44
-rw-r--r--tests/ui/join_absolute_paths.rs30
-rw-r--r--tests/ui/join_absolute_paths.stderr68
6 files changed, 196 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1760d19b385..46f5ff168f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5190,6 +5190,7 @@ Released 2018-09-13
 [`iter_with_drain`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_with_drain
 [`iter_without_into_iter`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_without_into_iter
 [`iterator_step_by_zero`]: https://rust-lang.github.io/rust-clippy/master/index.html#iterator_step_by_zero
+[`join_absolute_paths`]: https://rust-lang.github.io/rust-clippy/master/index.html#join_absolute_paths
 [`just_underscores_and_digits`]: https://rust-lang.github.io/rust-clippy/master/index.html#just_underscores_and_digits
 [`large_const_arrays`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_const_arrays
 [`large_digit_groups`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_digit_groups
diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs
index 55dedc0a658..7d4ab9fde1f 100644
--- a/clippy_lints/src/declared_lints.rs
+++ b/clippy_lints/src/declared_lints.rs
@@ -377,6 +377,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
     crate::methods::ITER_SKIP_NEXT_INFO,
     crate::methods::ITER_SKIP_ZERO_INFO,
     crate::methods::ITER_WITH_DRAIN_INFO,
+    crate::methods::JOIN_ABSOLUTE_PATHS_INFO,
     crate::methods::MANUAL_FILTER_MAP_INFO,
     crate::methods::MANUAL_FIND_MAP_INFO,
     crate::methods::MANUAL_NEXT_BACK_INFO,
diff --git a/clippy_lints/src/methods/join_absolute_paths.rs b/clippy_lints/src/methods/join_absolute_paths.rs
new file mode 100644
index 00000000000..02f28779cf6
--- /dev/null
+++ b/clippy_lints/src/methods/join_absolute_paths.rs
@@ -0,0 +1,52 @@
+use clippy_utils::diagnostics::span_lint_and_then;
+use clippy_utils::expr_or_init;
+use clippy_utils::source::snippet_opt;
+use clippy_utils::ty::is_type_diagnostic_item;
+use rustc_ast::ast::LitKind;
+use rustc_errors::Applicability;
+use rustc_hir::{Expr, ExprKind};
+use rustc_lint::LateContext;
+use rustc_span::symbol::sym;
+use rustc_span::Span;
+
+use super::JOIN_ABSOLUTE_PATHS;
+
+pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, recv: &'tcx Expr<'tcx>, join_arg: &'tcx Expr<'tcx>, expr_span: Span) {
+    let ty = cx.typeck_results().expr_ty(recv).peel_refs();
+    if (is_type_diagnostic_item(cx, ty, sym::Path) || is_type_diagnostic_item(cx, ty, sym::PathBuf))
+        && let ExprKind::Lit(spanned) = expr_or_init(cx, join_arg).kind
+        && let LitKind::Str(symbol, _) = spanned.node
+        && let sym_str = symbol.as_str()
+        && sym_str.starts_with(['/', '\\'])
+    {
+        span_lint_and_then(
+            cx,
+            JOIN_ABSOLUTE_PATHS,
+            join_arg.span,
+            "argument to `Path::join` starts with a path separator",
+            |diag| {
+                let arg_str = snippet_opt(cx, spanned.span).unwrap_or_else(|| "..".to_string());
+
+                let no_separator = if sym_str.starts_with('/') {
+                    arg_str.replacen('/', "", 1)
+                } else {
+                    arg_str.replacen('\\', "", 1)
+                };
+
+                diag.note("joining a path starting with separator will replace the path instead")
+                    .span_suggestion(
+                        spanned.span,
+                        "if this is unintentional, try removing the starting separator",
+                        no_separator,
+                        Applicability::Unspecified,
+                    )
+                    .span_suggestion(
+                        expr_span,
+                        "if this is intentional, try using `Path::new` instead",
+                        format!("PathBuf::from({arg_str})"),
+                        Applicability::Unspecified,
+                    );
+            },
+        );
+    }
+}
diff --git a/clippy_lints/src/methods/mod.rs b/clippy_lints/src/methods/mod.rs
index 3166a33fc2f..9ea2f6448b6 100644
--- a/clippy_lints/src/methods/mod.rs
+++ b/clippy_lints/src/methods/mod.rs
@@ -49,6 +49,7 @@ mod iter_skip_next;
 mod iter_skip_zero;
 mod iter_with_drain;
 mod iterator_step_by_zero;
+mod join_absolute_paths;
 mod manual_next_back;
 mod manual_ok_or;
 mod manual_saturating_arithmetic;
@@ -3684,6 +3685,46 @@ declare_clippy_lint! {
     "calling the `try_from` and `try_into` trait methods when `From`/`Into` is implemented"
 }
 
+declare_clippy_lint! {
+    /// ### What it does
+    /// Checks for calls to `Path::join` that start with a path separator (`\\` or `/`).
+    ///
+    /// ### Why is this bad?
+    /// If the argument to `Path::join` starts with a separator, it will overwrite
+    /// the original path. If this is intentional, prefer using `Path::new` instead.
+    ///
+    /// Note the behavior is platform dependent. A leading `\\` will be accepted
+    /// on unix systems as part of the file name
+    ///
+    /// See [`Path::join`](https://doc.rust-lang.org/std/path/struct.Path.html#method.join)
+    ///
+    /// ### Example
+    /// ```rust
+    /// # use std::path::{Path, PathBuf};
+    /// let path = Path::new("/bin");
+    /// let joined_path = path.join("/sh");
+    /// assert_eq!(joined_path, PathBuf::from("/sh"));
+    /// ```
+    ///
+    /// Use instead;
+    /// ```rust
+    /// # use std::path::{Path, PathBuf};
+    /// let path = Path::new("/bin");
+    ///
+    /// // If this was unintentional, remove the leading separator
+    /// let joined_path = path.join("sh");
+    /// assert_eq!(joined_path, PathBuf::from("/bin/sh"));
+    ///
+    /// // If this was intentional, create a new path instead
+    /// let new = Path::new("/sh");
+    /// assert_eq!(new, PathBuf::from("/sh"));
+    /// ```
+    #[clippy::version = "1.76.0"]
+    pub JOIN_ABSOLUTE_PATHS,
+    suspicious,
+    "calls to `Path::join` which will overwrite the original path"
+}
+
 pub struct Methods {
     avoid_breaking_exported_api: bool,
     msrv: Msrv,
@@ -3833,6 +3874,7 @@ impl_lint_pass!(Methods => [
     REDUNDANT_AS_STR,
     WAKER_CLONE_WAKE,
     UNNECESSARY_FALLIBLE_CONVERSIONS,
+    JOIN_ABSOLUTE_PATHS,
 ]);
 
 /// Extracts a method call name, args, and `Span` of the method name.
@@ -4235,6 +4277,8 @@ impl Methods {
                 ("join", [join_arg]) => {
                     if let Some(("collect", _, _, span, _)) = method_call(recv) {
                         unnecessary_join::check(cx, expr, recv, join_arg, span);
+                    } else {
+                        join_absolute_paths::check(cx, recv, join_arg, expr.span);
                     }
                 },
                 ("last", []) => {
diff --git a/tests/ui/join_absolute_paths.rs b/tests/ui/join_absolute_paths.rs
new file mode 100644
index 00000000000..efa77a0492e
--- /dev/null
+++ b/tests/ui/join_absolute_paths.rs
@@ -0,0 +1,30 @@
+//@no-rustfix
+
+#![allow(clippy::needless_raw_string_hashes)]
+#![warn(clippy::join_absolute_paths)]
+
+use std::path::{Path, PathBuf};
+
+fn main() {
+    let path = Path::new("/bin");
+    path.join("/sh");
+    //~^ ERROR: argument to `Path::join` starts with a path separator
+
+    let path = Path::new("C:\\Users");
+    path.join("\\user");
+    //~^ ERROR: argument to `Path::join` starts with a path separator
+
+    let path = PathBuf::from("/bin");
+    path.join("/sh");
+    //~^ ERROR: argument to `Path::join` starts with a path separator
+
+    let path = PathBuf::from("/bin");
+    path.join(r#"/sh"#);
+    //~^ ERROR: argument to `Path::join` starts with a path separator
+
+    let path: &[&str] = &["/bin"];
+    path.join("/sh");
+
+    let path = Path::new("/bin");
+    path.join("sh");
+}
diff --git a/tests/ui/join_absolute_paths.stderr b/tests/ui/join_absolute_paths.stderr
new file mode 100644
index 00000000000..0c2f89d9978
--- /dev/null
+++ b/tests/ui/join_absolute_paths.stderr
@@ -0,0 +1,68 @@
+error: argument to `Path::join` starts with a path separator
+  --> $DIR/join_absolute_paths.rs:10:15
+   |
+LL |     path.join("/sh");
+   |               ^^^^^
+   |
+   = note: joining a path starting with separator will replace the path instead
+   = note: `-D clippy::join-absolute-paths` implied by `-D warnings`
+   = help: to override `-D warnings` add `#[allow(clippy::join_absolute_paths)]`
+help: if this is unintentional, try removing the starting separator
+   |
+LL |     path.join("sh");
+   |               ~~~~
+help: if this is intentional, try using `Path::new` instead
+   |
+LL |     PathBuf::from("/sh");
+   |     ~~~~~~~~~~~~~~~~~~~~
+
+error: argument to `Path::join` starts with a path separator
+  --> $DIR/join_absolute_paths.rs:14:15
+   |
+LL |     path.join("\\user");
+   |               ^^^^^^^^
+   |
+   = note: joining a path starting with separator will replace the path instead
+help: if this is unintentional, try removing the starting separator
+   |
+LL |     path.join("\user");
+   |               ~~~~~~~
+help: if this is intentional, try using `Path::new` instead
+   |
+LL |     PathBuf::from("\\user");
+   |     ~~~~~~~~~~~~~~~~~~~~~~~
+
+error: argument to `Path::join` starts with a path separator
+  --> $DIR/join_absolute_paths.rs:18:15
+   |
+LL |     path.join("/sh");
+   |               ^^^^^
+   |
+   = note: joining a path starting with separator will replace the path instead
+help: if this is unintentional, try removing the starting separator
+   |
+LL |     path.join("sh");
+   |               ~~~~
+help: if this is intentional, try using `Path::new` instead
+   |
+LL |     PathBuf::from("/sh");
+   |     ~~~~~~~~~~~~~~~~~~~~
+
+error: argument to `Path::join` starts with a path separator
+  --> $DIR/join_absolute_paths.rs:22:15
+   |
+LL |     path.join(r#"/sh"#);
+   |               ^^^^^^^^
+   |
+   = note: joining a path starting with separator will replace the path instead
+help: if this is unintentional, try removing the starting separator
+   |
+LL |     path.join(r#"sh"#);
+   |               ~~~~~~~
+help: if this is intentional, try using `Path::new` instead
+   |
+LL |     PathBuf::from(r#"/sh"#);
+   |     ~~~~~~~~~~~~~~~~~~~~~~~
+
+error: aborting due to 4 previous errors
+