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 50 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e9deaee
cargo dev new_lint
ada4a Apr 22, 2025
2aef34a
add test
ada4a Apr 22, 2025
349c4f2
add some docs
ada4a Apr 22, 2025
3dd75d6
try to impl LateLintPass for NeedlessPathNew
ada4a Apr 30, 2025
9de7d0c
initialize `check` with the output of `#[author]`
ada4a May 24, 2025
b5cd1bf
borrow some stuff from mut_reference
ada4a May 25, 2025
53cfabe
implements_asref_path
ada4a May 25, 2025
cb6f951
fix some stuff using cargo check
ada4a May 25, 2025
50bf8cd
match on `Path::new(x)` call
ada4a May 25, 2025
5aa9374
is_path_new
ada4a May 25, 2025
0497b7c
instantiate `Path` as `GenericArg` to `AsRef`
ada4a May 25, 2025
a608def
check that the parameter wants an `impl AsRef<Path>`
ada4a May 25, 2025
fe5f3a9
add more tests
ada4a May 25, 2025
642fabe
give a suggestion
ada4a May 25, 2025
7808442
update lint message
ada4a May 25, 2025
ad6d9d7
update help message
ada4a May 25, 2025
6eecad2
more verbose (and better?) lint message
ada4a May 25, 2025
637ac49
"instantiate" to avoid `skip_binder`
ada4a May 25, 2025
ef679fb
create `{asref_def_id,path_ty}` once, and reuse in the closure
ada4a May 25, 2025
74710dc
update lint declaration
ada4a May 25, 2025
8d67ac7
rewrite motivation for the `args.len() == 1` check
ada4a May 25, 2025
022b55c
extract `cx.tcx` into a variable
ada4a May 26, 2025
ed33316
refactor: don't create the intermediate `TyKind::Adt`
ada4a May 26, 2025
75f088d
remove the `args.len()` check after all
ada4a May 26, 2025
313f522
test: method calls
ada4a Jun 3, 2025
f92a1f8
test: two params with the same generic
ada4a Jun 3, 2025
5a11d10
misc: pull `if` into match arm
ada4a Jun 3, 2025
89c1dbd
test: function stored in a variable
ada4a Jun 3, 2025
ac2a6a9
rm unused params
ada4a Jun 3, 2025
a0e1b01
failing test: `ExprKind::Call` which is not a function call
ada4a Jun 4, 2025
8ffb7fa
use `Ty::is_fn`
ada4a Jun 20, 2025
9eab15c
get `ParamEnv` and caller counds
ada4a Jun 23, 2025
ec10f1f
only get bounds once
ada4a Jun 23, 2025
15f5c60
expect there to be a single `arg`
ada4a Jun 23, 2025
8f49a4f
bail out on non-empty `Binder`
ada4a Jun 23, 2025
a8db514
get `path_ty` in a cleaner way
ada4a Jun 23, 2025
814a513
don't change parameters referring to generics used elsewhere
ada4a Jun 23, 2025
c85dd81
WIP: test: separate file for separate case
ada4a Jun 23, 2025
0d558df
WIP: comment-out `dbg!(bounds)`
ada4a Jun 23, 2025
21f684a
WIP: don't change parameters referring to generic args used in return…
ada4a Jun 23, 2025
d91aea3
fix: don't lint the method receiver
ada4a Jun 25, 2025
49e79ba
s/fn_sig/sig
ada4a Jun 25, 2025
c28be81
inline `check_arguments`
ada4a Jun 25, 2025
e934a72
use the `FnSig.inputs_and_output` field directly
ada4a Jun 25, 2025
fbbd92a
create `path_ty`, `asref_def_id` when constructing the lint pass
ada4a Jun 27, 2025
ca84f08
rm `bounds` and `generic_args_we_can_change` for now
ada4a Jun 27, 2025
422201c
also accept constructors of tuple structs and enum variants
ada4a Jun 27, 2025
d98d1ff
switch to the new way of aligning arguments to argument types
ada4a Jun 27, 2025
ae9c2a4
WIP: `is_used_anywhere_else`
ada4a Jul 14, 2025
b8a1e85
WIP: `has_required_preds`
ada4a Jul 14, 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 @@ -6186,6 +6186,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 @@ -551,6 +551,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 @@ -269,6 +269,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(|tcx| Box::new(needless_path_new::NeedlessPathNew::new(tcx)));
// add lints here, do not remove this comment, it's used in `new_lint`
}
146 changes: 146 additions & 0 deletions clippy_lints/src/needless_path_new.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::path_res;
use clippy_utils::source::snippet;
use clippy_utils::ty::implements_trait;
use rustc_errors::Applicability;
use rustc_hir::def::{CtorKind, DefKind, Res};
use rustc_hir::def_id::DefId;
use rustc_hir::{Expr, ExprKind, QPath};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::{self, GenericPredicates, List, ParamTy, Ty, TyCtxt};
use rustc_session::impl_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.89.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"
}

impl_lint_pass!(NeedlessPathNew<'_> => [NEEDLESS_PATH_NEW]);

pub struct NeedlessPathNew<'tcx> {
path_ty: Option<Ty<'tcx>>,
asref_def_id: Option<DefId>,
}

impl<'tcx> NeedlessPathNew<'tcx> {
pub fn new(tcx: TyCtxt<'tcx>) -> Self {
Self {
path_ty: (tcx.get_diagnostic_item(sym::Path))
.map(|path_def_id| Ty::new_adt(tcx, tcx.adt_def(path_def_id), List::empty())),
asref_def_id: tcx.get_diagnostic_item(sym::AsRef),
}
}
}

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

let Some(path_ty) = self.path_ty else {
return;
};

let Some(asref_def_id) = self.asref_def_id else {
return;
};

let (fn_did, args) = match e.kind {
ExprKind::Call(callee, args)
if let Res::Def(DefKind::Fn | DefKind::AssocFn | DefKind::Ctor(_, CtorKind::Fn), did) =
path_res(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 implements_asref_path = |arg| implements_trait(cx, arg, asref_def_id, &[path_ty.into()]);

let is_used_anywhere_else = |param_ty: &ParamTy, other_sig_tys: &[Ty<'_>]| {
other_sig_tys.iter().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
}
})
})
};

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(|(i, (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())
&& has_required_preds(arg_param_ty /* , cx.tcx.predicates_of(arg_ty) */)
{
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

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

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

fn takes_path(_: &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) {}

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 at the same time
takes_two_impl_paths_with_the_same_generic(
"foo", //~ needless_path_new
"bar", //~ 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");
}
65 changes: 65 additions & 0 deletions tests/ui/needless_path_new.rs
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>>(_: P, _: P) {}

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"));
}
41 changes: 41 additions & 0 deletions tests/ui/needless_path_new.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
error: the expression enclosed in `Path::new` implements `AsRef<Path>`
--> tests/ui/needless_path_new.rs:24: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:27: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:28: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:35: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:40: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:41:9
|
LL | Path::new("bar"),
| ^^^^^^^^^^^^^^^^ help: remove the enclosing `Path::new`: `"bar"`

error: aborting due to 6 previous errors

Loading