-
Notifications
You must be signed in to change notification settings - Fork 1.8k
new lint: [useless_pathbuf_conversion
]
#15640
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
base: master
Are you sure you want to change the base?
new lint: [useless_pathbuf_conversion
]
#15640
Conversation
Lintcheck changes for 5412590
This comment will be updated if you push new changes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you very much for working on this -- this is a lint I wanted to propose myself, alongside #14668. So here are some of the tips I've learned while working on that lint -- hope they'll be useful:)
{ | ||
// check that the function is `from` | ||
if cx.tcx.item_name(def_id) != sym::from { | ||
return; | ||
} | ||
|
||
// get the type of the function's return value | ||
let ty = cx.typeck_results().expr_ty(inner); | ||
|
||
if is_type_diagnostic_item(cx, ty, sym::PathBuf) | ||
&& let Some(arg) = args.first() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think pulling all of this into the let-chain wouldn't hurt
{ | |
// check that the function is `from` | |
if cx.tcx.item_name(def_id) != sym::from { | |
return; | |
} | |
// get the type of the function's return value | |
let ty = cx.typeck_results().expr_ty(inner); | |
if is_type_diagnostic_item(cx, ty, sym::PathBuf) | |
&& let Some(arg) = args.first() | |
// check that the function is `from` | |
&& cx.tcx.item_name(def_id) == sym::from { | |
// get the type of the function's return value | |
&& let ty = cx.typeck_results().expr_ty(inner) | |
&& is_type_diagnostic_item(cx, ty, sym::PathBuf) | |
&& let Some(arg) = args.first() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, that looks more accurate, fixed
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { | ||
// Only care about &PathBuf::from(...) | ||
if let ExprKind::AddrOf(_, _, inner) = &expr.kind | ||
&& let ExprKind::Call(func, args) = &inner.kind |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could ensure PathBuf::from
having only one argument already at this line:
&& let ExprKind::Call(func, args) = &inner.kind | |
&& let ExprKind::Call(func, [arg]) = &inner.kind |
This avoids the need for && let Some(arg) = args.first()
below
/// fn use_path(p: &std::path::Path) {} | ||
/// use_path(std::path::Path::new("abc")); | ||
/// ``` | ||
#[clippy::version = "1.91.0"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#[clippy::version = "1.91.0"] | |
#[clippy::version = "1.92.0"] |
#[clippy::version = "1.91.0"] | ||
pub USELESS_PATHBUF_CONVERSION, | ||
complexity, | ||
"creating a PathBuf only to take a reference, where Path::new would suffice" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"creating a PathBuf only to take a reference, where Path::new would suffice" | |
"creating a `PathBuf` only to take a reference, where `Path::new` would suffice" |
/// | ||
/// **Example:** | ||
/// ```rust | ||
/// fn use_path(p: &std::path::Path) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most functions working with Path
s will have the parameter specified as impl AsRef<Path>
, which this lint unfortunately wouldn't catch right now.
This is very similar to a lint I suggested myself: #14668, and I'm not sure what the interplay between them should be. That lint basically checks two things:
- that the implicit generic in
impl AsRef<Path>
isn't used elsewhere in the function - that the
x
inPath::new(x)
implements all the trait bounds of that generic (notablyAsRef
, but often alsoSized
), and therefore could be used directly
So this lint would probably need to copy the first check from that lint; and at this point one could argue that it could go the extra mile and also check the second thing, which would allow it to replace &PathBuf::from(x)
directly with x
-- otherwise, one would first create Path::new(x)
, and then need to repeat the entirety of the first check on that.
Now that I've written this out though, the first check isn't actually all that big, and also it's preferable to let the lints compose, so this is probably fine after all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for breaking it down. I agree, let the lints complement each other.
&& let ExprKind::Path(ref qpath) = func.kind | ||
&& let Some(def_id) = cx.qpath_res(qpath, func.hir_id).opt_def_id() | ||
{ | ||
// check that the function is `from` | ||
if cx.tcx.item_name(def_id) != sym::from { | ||
return; | ||
} | ||
|
||
// get the type of the function's return value | ||
let ty = cx.typeck_results().expr_ty(inner); | ||
|
||
if is_type_diagnostic_item(cx, ty, sym::PathBuf) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should be able to check that the call is PathBuf::from
with something like this:
&& let ExprKind::Path(ref qpath) = func.kind // pre-existing logic
&& let QPath::TypeRelative(pathbuf, from) = qpath
&& from.ident.name == sym::from
&& is_path_diagnostic_item(cx, pathbuf, sym::PathBuf)
This avoids the need to resolve/type-check everything for the second time1, since QPath
already contains the whole resolution
Footnotes
-
I'm pretty sure it's all cached in the compiler, but still ↩
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#[clippy::dump]
is quite useful for finding the right incantations like this. For this case, I used the following code:
fn main() {
#[clippy::dump]
std::path::PathBuf::from("x");
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice trick with clipy_dump, I didn`t know it, thanks.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Thanks for the comments, @ada4a . PR is updated now. |
1b9ec32
to
19d7324
Compare
@rustbot note remove Feature-freeze |
//~^ redundant_clone | ||
require_os_str(&OsString::from("x").to_os_string()); | ||
//~^ redundant_clone | ||
require_path(&std::path::PathBuf::from("x").to_path_buf()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
redundant_clone
fires here not because of the &PathBuf
, but because of the .to_path_buf()
(which is effectively a .clone()
), so this test case is important to retain. You should probably Actually, even that's not necessary, since allow
useless_pathbuf_conversion
in this file to avoid the extraneous warning.useless_pathbuf_conversion
no longer lints here
&& let QPath::TypeRelative(pathbuf, _) = qpath | ||
&& is_path_diagnostic_item(cx, *pathbuf, sym::PathBuf) | ||
{ | ||
let sugg = format!("Path::new({})", snippet(cx, arg.span, "..")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please prefer snippet_with_applicability
where possible. If you start with Applicabiliy::MachineApplicable
, but a snippet for a given span isn't available, then snippet_with_applicability
will reduce the applicability to Applicability::MaybeIncorrect
. The whole pattern usually looks like this:
let mut app = Applicability::MachineApplicable;
let sugg = snippet_with_applicability(cx, <span>, <default>, &mut app);
span_lint_and_sugg(.., sugg, app);
Note to self: this really ought to be in the documentation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And it might even be necessary to use snippet_with_context
-- the argument to PathBuf::from
might be a macro call, in which case snippet_with_applicability
would incorrectly expand it. You could use the following as a test case:
macro_rules! str {
() => {
"expands to a `&str`"
}
}
let _ = PathBuf::from(str!());
cx, | ||
USELESS_PATHBUF_CONVERSION, | ||
expr.span, | ||
"unnecessary `PathBuf::from` when a `&Path` is enough", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, this lint doesn't actually check whether a &Path
is enough -- for example, it would falsely warn in a case like:
fn takes_ref_pathbuf(_: &PathBuf) {}
fn main() {
takes_ref_pathbuf(&PathBuf::from("path"));
}
&& let Some(def_id) = cx.qpath_res(qpath, func.hir_id).opt_def_id() | ||
&& cx.tcx.item_name(def_id) == sym::from | ||
&& let QPath::TypeRelative(pathbuf, _) = qpath | ||
&& is_path_diagnostic_item(cx, *pathbuf, sym::PathBuf) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
&& let Some(def_id) = cx.qpath_res(qpath, func.hir_id).opt_def_id() | |
&& cx.tcx.item_name(def_id) == sym::from | |
&& let QPath::TypeRelative(pathbuf, _) = qpath | |
&& is_path_diagnostic_item(cx, *pathbuf, sym::PathBuf) | |
&& let QPath::TypeRelative(pathbuf, from) = qpath | |
&& is_path_diagnostic_item(cx, *pathbuf, sym::PathBuf) | |
&& from.ident.name == sym::from |
&& is_type_diagnostic_item(cx, *inner_ty, sym::Path) | ||
{ | ||
let mut app = Applicability::MachineApplicable; | ||
let arg_snippet = snippet_with_context(cx, arg.span, arg.span.ctxt(), "..", &mut app).0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, sorry for not explaining it fully. In snippet_with_context
, the span you use to get the ctxt
should be of a node that is the parent of the node for which you're creating the snippet. That was convoluted, so here's what that'd look like in your case:
You have &PathBuf::from(arg)
, and you want to get the snippet for arg
. Therefore, you get the ctxt
from the parent expression of arg
, which is PathBuf::from(arg)
, which you call inner
. With that, the right invocation would be:
let arg_snippet = snippet_with_context(cx, arg.span, arg.span.ctxt(), "..", &mut app).0; | |
let arg_snippet = snippet_with_context(cx, arg.span, inner.span.ctxt(), "..", &mut app).0; |
The best way I can explain the logic behind this is that you specifically want to avoid viewing arg
from "inside" -- fully expanded -- but rather from the "outside", and the most obvious thing that's looking at arg
from outside is its parent expression. I'm pretty sure a grandparent or something would work as well here, but the parent is the easiest thing to get to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem, thank you for the clarification. I have also added a note in a comment to snippet_with_context
declaration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have also added a note in a comment to snippet_with_context declaration.
The two paragraphs before your addition are trying to explain basically the same as what you added I think, so imo it would make sense to clarify them. One could imagine a more full-fledged example showing a code snippet, and what snippet_with_context
would return with different combinations of nodes. If you want to take a stab at that, feel free, otherwise I'll try to do that in a separate PR
This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
This comment has been minimized.
This comment has been minimized.
b41b15f
to
aaa4e16
Compare
This is mostly implemented already as use std::path::Path;
fn f(_: impl AsRef<Path>) {}
fn f2(x: &Path) {
f(x.to_path_buf())
} It doesn't currently recognize |
changelog: new lint: [
useless_pathbuf_conversion
]resolves
#15637