Skip to content

WIP: New lint: needless_path_new #14895

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 46 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
aefaebd
cargo dev new_lint
ada4a Apr 22, 2025
f1bda8c
add test
ada4a Apr 22, 2025
d9f98e3
add some docs
ada4a Apr 22, 2025
292d480
try to impl LateLintPass for NeedlessPathNew
ada4a Apr 30, 2025
4fc9079
initialize `check` with the output of `#[author]`
ada4a May 24, 2025
acf0f20
borrow some stuff from mut_reference
ada4a May 25, 2025
43e407e
misc: pull `if` into match arm
ada4a Jun 3, 2025
a5e9a56
implements_asref_path
ada4a May 25, 2025
6d7307f
fix some stuff using cargo check
ada4a May 25, 2025
89ab8b8
is_path_new - check for `Path::new(x)` call
ada4a Jul 24, 2025
b462fa7
instantiate `Path` as `GenericArg` to `AsRef`
ada4a May 25, 2025
988a5ff
check that the parameter wants an `impl AsRef<Path>`
ada4a May 25, 2025
ca1e42c
add more tests
ada4a May 25, 2025
8f5ac9b
give a suggestion
ada4a May 25, 2025
6fbe5b0
update lint message
ada4a May 25, 2025
167a4dd
update help message
ada4a May 25, 2025
dbb3440
more verbose (and better?) lint message
ada4a May 25, 2025
3a22134
"instantiate" to avoid `skip_binder`
ada4a May 25, 2025
59035d5
create `{asref_def_id,path_ty}` once, and reuse in the closure
ada4a May 25, 2025
7bead6d
extract `cx.tcx` into a variable
ada4a May 26, 2025
e1a6c10
refactor: don't create the intermediate `TyKind::Adt`
ada4a May 26, 2025
9c26da6
test: method calls
ada4a Jun 3, 2025
a78affd
test: two params with the same generic
ada4a Jun 3, 2025
b2834be
rm unused params
ada4a Jun 3, 2025
9dcd0f4
failing test: `ExprKind::Call` which is not a function call
ada4a Jun 4, 2025
f318c0d
use `Ty::is_fn`
ada4a Jun 20, 2025
1be03c3
get `ParamEnv` and caller bounds
ada4a Jun 23, 2025
98ea783
bail out on non-empty `Binder`
ada4a Jun 23, 2025
47d31e8
get `path_ty` in a cleaner way
ada4a Jun 23, 2025
7cf4db5
don't change parameters referring to generics used elsewhere
ada4a Jun 23, 2025
863741d
WIP: test: separate file for separate case
ada4a Jun 23, 2025
e2c0cd7
WIP: comment-out `dbg!(bounds)`
ada4a Jun 23, 2025
e76e45a
WIP: don't change parameters referring to generic args used in return…
ada4a Jun 23, 2025
19c17ba
fix: don't lint the method receiver
ada4a Jun 25, 2025
1700cc9
s/fn_sig/sig
ada4a Jun 25, 2025
c2430f5
inline `check_arguments`
ada4a Jun 25, 2025
1ad05f6
use the `FnSig.inputs_and_output` field directly
ada4a Jun 25, 2025
2391c1a
create `path_ty`, `asref_def_id` when constructing the lint pass
ada4a Jun 27, 2025
e017c5c
rm `bounds` and `generic_args_we_can_change` for now
ada4a Jun 27, 2025
2637a30
also accept constructors of tuple structs and enum variants
ada4a Jun 27, 2025
8fa24da
switch to the new way of aligning arguments to argument types
ada4a Jun 27, 2025
7f7d939
`is_used_anywhere_else`
ada4a Jul 14, 2025
a5553af
WIP: `has_required_preds`
ada4a Jul 14, 2025
3c0d183
rm `implements_asref_path` and everything required for it
ada4a Jul 24, 2025
22b1d40
test: function stored in a variable
ada4a Jun 3, 2025
a5dd45b
wip
ada4a Jul 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6284,6 +6284,7 @@ Released 2018-09-13
[`needless_parens_on_range_literals`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_parens_on_range_literals
[`needless_pass_by_ref_mut`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_ref_mut
[`needless_pass_by_value`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_value
[`needless_path_new`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_path_new
[`needless_pub_self`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_pub_self
[`needless_question_mark`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_question_mark
[`needless_range_loop`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_range_loop
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::needless_parens_on_range_literals::NEEDLESS_PARENS_ON_RANGE_LITERALS_INFO,
crate::needless_pass_by_ref_mut::NEEDLESS_PASS_BY_REF_MUT_INFO,
crate::needless_pass_by_value::NEEDLESS_PASS_BY_VALUE_INFO,
crate::needless_path_new::NEEDLESS_PATH_NEW_INFO,
crate::needless_question_mark::NEEDLESS_QUESTION_MARK_INFO,
crate::needless_update::NEEDLESS_UPDATE_INFO,
crate::neg_cmp_op_on_partial_ord::NEG_CMP_OP_ON_PARTIAL_ORD_INFO,
Expand Down
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ mod needless_maybe_sized;
mod needless_parens_on_range_literals;
mod needless_pass_by_ref_mut;
mod needless_pass_by_value;
mod needless_path_new;
mod needless_question_mark;
mod needless_update;
mod neg_cmp_op_on_partial_ord;
Expand Down Expand Up @@ -830,5 +831,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_late_pass(|_| Box::new(cloned_ref_to_slice_refs::ClonedRefToSliceRefs::new(conf)));
store.register_late_pass(|_| Box::new(infallible_try_from::InfallibleTryFrom));
store.register_late_pass(|_| Box::new(coerce_container_to_any::CoerceContainerToAny));
store.register_late_pass(|_| Box::new(needless_path_new::NeedlessPathNew));
// add lints here, do not remove this comment, it's used in `new_lint`
}
129 changes: 129 additions & 0 deletions clippy_lints/src/needless_path_new.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::snippet;
use clippy_utils::{expr_or_init, path_res};
use rustc_errors::Applicability;
use rustc_hir::def::{CtorKind, DefKind, Res};
use rustc_hir::{Expr, ExprKind, QPath};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::{self, GenericPredicates, ParamTy, Ty};
use rustc_session::declare_lint_pass;
use rustc_span::sym;
use std::iter;

declare_clippy_lint! {
/// ### What it does
/// Detects expressions being enclosed in `Path::new` when passed to a function that accepts
/// `impl AsRef<Path>`, when the enclosed expression could be used.
///
/// ### Why is this bad?
/// It is unnecessarily verbose
///
/// ### Example
/// ```no_run
/// # use std::{fs, path::Path};
/// fs::write(Path::new("foo.txt"), "foo");
/// ```
/// Use instead:
/// ```no_run
/// # use std::{fs, path::Path};
/// fs::write("foo.txt", "foo");
/// ```
#[clippy::version = "1.90.0"]
pub NEEDLESS_PATH_NEW,
nursery,
"an argument passed to a function that accepts `impl AsRef<Path>` \
being enclosed in `Path::new` when the argument implements the trait"
}

declare_lint_pass!(NeedlessPathNew => [NEEDLESS_PATH_NEW]);

fn is_used_anywhere_else<'a>(param_ty: &'_ ParamTy, mut other_sig_tys: impl Iterator<Item = Ty<'a>>) -> bool {
other_sig_tys.any(|sig_ty| {
sig_ty.walk().any(|generic_arg| {
if let Some(ty) = generic_arg.as_type()
&& let ty::Param(pt) = ty.kind()
&& pt == param_ty
{
true
} else {
false
}
})
})
}

impl<'tcx> LateLintPass<'tcx> for NeedlessPathNew {
fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'tcx>) {
let tcx = cx.tcx;

let (fn_did, args) = match e.kind {
ExprKind::Call(callee, args)
if let Res::Def(DefKind::Fn | DefKind::AssocFn | DefKind::Ctor(_, CtorKind::Fn), did) =
// re: `expr_or_init`: `callee` might be a variable storing a fn ptr, for example,
// so we need to get to the actual initializer
path_res(cx, expr_or_init(cx, callee)) =>
{
(did, args)
},
ExprKind::MethodCall(_, _, args, _)
if let Some(did) = cx.typeck_results().type_dependent_def_id(e.hir_id) =>
{
(did, args)
},
_ => return,
};

let sig = tcx.fn_sig(fn_did).skip_binder().skip_binder();

// whether `func` is `Path::new`
let is_path_new = |func: &Expr<'_>| {
if let ExprKind::Path(ref qpath) = func.kind
&& let QPath::TypeRelative(ty, path) = qpath
&& let Some(did) = path_res(cx, *ty).opt_def_id()
&& tcx.is_diagnostic_item(sym::Path, did)
&& path.ident.name == sym::new
{
true
} else {
false
}
};

let has_required_preds = |_param_ty: &ParamTy, _preds: GenericPredicates<'_>| -> bool { true };

// as far as I understand, `ExprKind::MethodCall` doesn't include the receiver in `args`,
// but does in `sig.inputs()` -- so we iterate over both in `rev`erse in order to line
// them up starting from the _end_
//
// and for `ExprKind::Call` this is basically a no-op
iter::zip(sig.inputs().iter().rev(), args.iter().rev())
.enumerate()
.for_each(|(arg_idx, (arg_ty, arg))| {
// we want `argument` to be `Path::new(x)`
if let ExprKind::Call(path_new, [x]) = arg.kind
&& is_path_new(path_new)
&& let ty::Param(arg_param_ty) = arg_ty.kind()
&& !is_used_anywhere_else(
arg_param_ty,
sig.inputs()
.iter()
// `arg_idx` is based on the reversed order, so we need to reverse as well
.rev()
.enumerate()
.filter_map(|(i, input)| (i != arg_idx).then_some(*input)),
)
&& has_required_preds(arg_param_ty, cx.tcx.predicates_of(fn_did))
{
span_lint_and_sugg(
cx,
NEEDLESS_PATH_NEW,
arg.span,
"the expression enclosed in `Path::new` implements `AsRef<Path>`",
"remove the enclosing `Path::new`",
format!("{}", snippet(cx, x.span, "..")),
Applicability::MachineApplicable,
);
}
})
}
}
16 changes: 16 additions & 0 deletions tests/ui/needless_path_ne2.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#![warn(clippy::needless_path_new)]

use std::fs;
use std::path::Path;

// fn foo() -> Option<&'static Path> {
// // Some(...) is `ExprKind::Call`, but we don't consider it
// Some(Path::new("foo.txt"))
// }
//
fn main() {
// let _: Option<&Path> = Some(Path::new("foo"));
fn foo() -> Option<impl AsRef<Path>> {
Some("bar.txt") //~ needless_path_new
}
}
16 changes: 16 additions & 0 deletions tests/ui/needless_path_ne2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#![warn(clippy::needless_path_new)]

use std::fs;
use std::path::Path;

fn foo() -> Option<&'static Path> {
// Some(...) is `ExprKind::Call`, but we don't consider it
Some(Path::new("foo.txt"))
}

fn main() {
let _: Option<&Path> = Some(Path::new("foo"));
fn foo() -> Option<impl AsRef<Path>> {
Some(Path::new("bar.txt")) //~ needless_path_new
}
}
11 changes: 11 additions & 0 deletions tests/ui/needless_path_ne2.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
error: the expression enclosed in `Path::new` implements `AsRef<Path>`
--> tests/ui/needless_path_new_some.rs:14:14
|
LL | Some(Path::new("bar.txt"))
| ^^^^^^^^^^^^^^^^^^^^ help: remove the enclosing `Path::new`: `"bar.txt"`
|
= note: `-D clippy::needless-path-new` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::needless_path_new)]`

error: aborting due to 1 previous error

65 changes: 65 additions & 0 deletions tests/ui/needless_path_new.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#![warn(clippy::needless_path_new)]

use std::fs;
use std::path::Path;

fn takes_path(_: &Path) {}

fn takes_impl_path(_: impl AsRef<Path>) {}

fn takes_path_and_impl_path(_: &Path, _: impl AsRef<Path>) {}

fn takes_two_impl_paths_with_the_same_generic<P: AsRef<Path>>(_: P, _: P) {}
fn takes_two_impl_paths_with_different_generics<P: AsRef<Path>, Q: AsRef<Path>>(_: P, _: Q) {}

struct Foo;

impl Foo {
fn takes_path(_: &Path) {}
fn takes_self_and_path(&self, _: &Path) {}
fn takes_path_and_impl_path(_: &Path, _: impl AsRef<Path>) {}
fn takes_self_and_path_and_impl_path(&self, _: &Path, _: impl AsRef<Path>) {}
}

fn main() {
let f = Foo;

fs::write("foo.txt", "foo"); //~ needless_path_new

fs::copy(
"foo", //~ needless_path_new
"bar", //~ needless_path_new
);

Foo::takes_path(Path::new("foo"));

f.takes_self_and_path_and_impl_path(
Path::new("foo"),
"bar", //~ needless_path_new
);

// we can and should change both independently
takes_two_impl_paths_with_different_generics(
"foo", //~ needless_path_new
"bar", //~ needless_path_new
);

let a = takes_impl_path;

a("foo.txt"); //~ needless_path_new

// no warning
takes_path(Path::new("foo"));

// the paramater that _could_ be passed directly, was
// the parameter that could't, wasn't
takes_path_and_impl_path(Path::new("foo"), "bar");

// same but as a method
Foo::takes_path_and_impl_path(Path::new("foo"), "bar");
f.takes_self_and_path_and_impl_path(Path::new("foo"), "bar");

// we are conservative and don't suggest changing a parameter
// if it contains a generic type used elsewhere in the function
takes_two_impl_paths_with_the_same_generic(Path::new("foo"), Path::new("bar"));
}
73 changes: 73 additions & 0 deletions tests/ui/needless_path_new.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#![warn(clippy::needless_path_new)]

use std::fs;
use std::path::Path;

fn takes_path(_: &Path) {}

fn takes_impl_path(_: impl AsRef<Path>) {}

fn takes_path_and_impl_path(_: &Path, _: impl AsRef<Path>) {}

fn takes_two_impl_paths_with_the_same_generic<P: AsRef<Path>>(_: P, _: P) {}
fn takes_two_impl_paths_with_different_generics<P: AsRef<Path>, Q: AsRef<Path>>(_: P, _: Q) {}

struct Foo;

impl Foo {
fn takes_path(_: &Path) {}
fn takes_self_and_path(&self, _: &Path) {}
fn takes_path_and_impl_path(_: &Path, _: impl AsRef<Path>) {}
fn takes_self_and_path_and_impl_path(&self, _: &Path, _: impl AsRef<Path>) {}
}

fn main() {
let f = Foo;

fs::write(Path::new("foo.txt"), "foo"); //~ needless_path_new

fs::copy(
Path::new("foo"), //~ needless_path_new
Path::new("bar"), //~ needless_path_new
);

Foo::takes_path(Path::new("foo"));

f.takes_self_and_path_and_impl_path(
Path::new("foo"),
Path::new("bar"), //~ needless_path_new
);

// we can and should change both independently
takes_two_impl_paths_with_different_generics(
Path::new("foo"), //~ needless_path_new
Path::new("bar"), //~ needless_path_new
);

let a = takes_impl_path;

a(Path::new("foo.txt")); //~ needless_path_new

// no warning
takes_path(Path::new("foo"));

// the paramater that _could_ be passed directly, was
// the parameter that could't, wasn't
takes_path_and_impl_path(Path::new("foo"), "bar");

// same but as a method
Foo::takes_path_and_impl_path(Path::new("foo"), "bar");
f.takes_self_and_path_and_impl_path(Path::new("foo"), "bar");

// we are conservative and don't suggest changing a parameter
// if it contains a generic type used elsewhere in the function
takes_two_impl_paths_with_the_same_generic(Path::new("foo"), Path::new("bar"));

// the type ascription specifies `Path`, not just `impl AsRef<Path>`
let _: Option<&Path> = Some(Path::new("foo")); //~ needless_path_new

// the return type requires `Path`, not just `impl AsRef<Path>`
fn foo() -> Option<&'static Path> {
Some(Path::new("foo.txt"))
}
}
47 changes: 47 additions & 0 deletions tests/ui/needless_path_new.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
error: the expression enclosed in `Path::new` implements `AsRef<Path>`
--> tests/ui/needless_path_new.rs:27:15
|
LL | fs::write(Path::new("foo.txt"), "foo");
| ^^^^^^^^^^^^^^^^^^^^ help: remove the enclosing `Path::new`: `"foo.txt"`
|
= note: `-D clippy::needless-path-new` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::needless_path_new)]`

error: the expression enclosed in `Path::new` implements `AsRef<Path>`
--> tests/ui/needless_path_new.rs:31:9
|
LL | Path::new("bar"),
| ^^^^^^^^^^^^^^^^ help: remove the enclosing `Path::new`: `"bar"`

error: the expression enclosed in `Path::new` implements `AsRef<Path>`
--> tests/ui/needless_path_new.rs:30:9
|
LL | Path::new("foo"),
| ^^^^^^^^^^^^^^^^ help: remove the enclosing `Path::new`: `"foo"`

error: the expression enclosed in `Path::new` implements `AsRef<Path>`
--> tests/ui/needless_path_new.rs:38:9
|
LL | Path::new("bar"),
| ^^^^^^^^^^^^^^^^ help: remove the enclosing `Path::new`: `"bar"`

error: the expression enclosed in `Path::new` implements `AsRef<Path>`
--> tests/ui/needless_path_new.rs:44:9
|
LL | Path::new("bar"),
| ^^^^^^^^^^^^^^^^ help: remove the enclosing `Path::new`: `"bar"`

error: the expression enclosed in `Path::new` implements `AsRef<Path>`
--> tests/ui/needless_path_new.rs:43:9
|
LL | Path::new("foo"),
| ^^^^^^^^^^^^^^^^ help: remove the enclosing `Path::new`: `"foo"`

error: the expression enclosed in `Path::new` implements `AsRef<Path>`
--> tests/ui/needless_path_new.rs:49:7
|
LL | a(Path::new("foo.txt"));
| ^^^^^^^^^^^^^^^^^^^^ help: remove the enclosing `Path::new`: `"foo.txt"`

error: aborting due to 7 previous errors

Loading