Auto merge of #11656 - y21:unnecessary_string_from_utf8, r=Jarcho

[`unnecessary_to_owned`]: catch `to_owned` on byte slice to create temporary `&str`

Closes #11648

Detects the pattern `&String::from_utf8(bytes.to_vec()).unwrap()` and suggests `core::str::from_utf8(bytes).unwrap()`, which avoids the unnecessary intermediate allocation.

I decided to put this in the existing `unnecessary_to_owned` lint (rather than creating a new lint) for a few reasons:
- we get to use some of its logic (for example, recognizing any of the functions in the `to_owned` family, e.g. `to_vec`)
- the actual inefficient operation that can be avoided here is the call to `.to_vec()`, so this is in a way similar to the other cases caught by `unnecessary_to_owned`, just through a bunch of type conversions
- we can make this more "generic" later and catch other cases, so imo it's best not to tie this lint specifically to the `String` type

changelog: [`unnecessary_to_owned`]: catch `&String::from_utf8(bytes.to_vec()).unwrap()` and suggest `core::str::from_utf8(bytes).unwrap()`
This commit is contained in:
bors 2024-07-05 02:04:06 +00:00
commit 885f97e2ce
5 changed files with 154 additions and 15 deletions

View file

@ -1,13 +1,15 @@
use super::implicit_clone::is_clone_like;
use super::unnecessary_iter_cloned::{self, is_into_iter};
use clippy_config::msrvs::{self, Msrv};
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::snippet_opt;
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
use clippy_utils::source::{snippet, snippet_opt};
use clippy_utils::ty::{
get_iterator_item_ty, implements_trait, is_copy, is_type_diagnostic_item, is_type_lang_item, peel_mid_ty_refs,
};
use clippy_utils::visitors::find_all_ret_expressions;
use clippy_utils::{fn_def_id, get_parent_expr, is_diag_item_method, is_diag_trait_item, return_ty};
use clippy_utils::{
fn_def_id, get_parent_expr, is_diag_item_method, is_diag_trait_item, match_def_path, paths, return_ty,
};
use rustc_errors::Applicability;
use rustc_hir::def::{DefKind, Res};
use rustc_hir::def_id::DefId;
@ -52,6 +54,9 @@ pub fn check<'tcx>(
if check_into_iter_call_arg(cx, expr, method_name, receiver, msrv) {
return;
}
if check_string_from_utf8(cx, expr, receiver) {
return;
}
check_other_call_arg(cx, expr, method_name, receiver);
}
} else {
@ -240,6 +245,65 @@ fn check_into_iter_call_arg(
false
}
/// Checks for `&String::from_utf8(bytes.{to_vec,to_owned,...}()).unwrap()` coercing to `&str`,
/// which can be written as just `std::str::from_utf8(bytes).unwrap()`.
fn check_string_from_utf8<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, receiver: &'tcx Expr<'tcx>) -> bool {
if let Some((call, arg)) = skip_addr_of_ancestors(cx, expr)
&& !arg.span.from_expansion()
&& let ExprKind::Call(callee, _) = call.kind
&& fn_def_id(cx, call).is_some_and(|did| match_def_path(cx, did, &paths::STRING_FROM_UTF8))
&& let Some(unwrap_call) = get_parent_expr(cx, call)
&& let ExprKind::MethodCall(unwrap_method_name, ..) = unwrap_call.kind
&& matches!(unwrap_method_name.ident.name, sym::unwrap | sym::expect)
&& let Some(ref_string) = get_parent_expr(cx, unwrap_call)
&& let ExprKind::AddrOf(BorrowKind::Ref, Mutability::Not, _) = ref_string.kind
&& let adjusted_ty = cx.typeck_results().expr_ty_adjusted(ref_string)
// `&...` creates a `&String`, so only actually lint if this coerces to a `&str`
&& matches!(adjusted_ty.kind(), ty::Ref(_, ty, _) if ty.is_str())
{
span_lint_and_then(
cx,
UNNECESSARY_TO_OWNED,
ref_string.span,
"allocating a new `String` only to create a temporary `&str` from it",
|diag| {
let arg_suggestion = format!(
"{borrow}{recv_snippet}",
recv_snippet = snippet(cx, receiver.span.source_callsite(), ".."),
borrow = if cx.typeck_results().expr_ty(receiver).is_ref() {
""
} else {
// If not already a reference, prefix with a borrow so that it can coerce to one
"&"
}
);
diag.multipart_suggestion(
"convert from `&[u8]` to `&str` directly",
vec![
// `&String::from_utf8(bytes.to_vec()).unwrap()`
// ^^^^^^^^^^^^^^^^^
(callee.span, "core::str::from_utf8".into()),
// `&String::from_utf8(bytes.to_vec()).unwrap()`
// ^
(
ref_string.span.shrink_to_lo().to(unwrap_call.span.shrink_to_lo()),
String::new(),
),
// `&String::from_utf8(bytes.to_vec()).unwrap()`
// ^^^^^^^^^^^^^^
(arg.span, arg_suggestion),
],
Applicability::MachineApplicable,
);
},
);
true
} else {
false
}
}
/// Checks whether `expr` is an argument in an `into_iter` call and, if so, determines whether its
/// call of a `to_owned`-like function is unnecessary.
fn check_split_call_arg(cx: &LateContext<'_>, expr: &Expr<'_>, method_name: Symbol, receiver: &Expr<'_>) -> bool {