blob: 28c339ce2b7d34448531205540afce751635b6db [file] [log] [blame] [edit]
use std::collections::BTreeMap;
use std::collections::btree_map::Entry;
use std::mem;
use std::ops::ControlFlow;
use clippy_config::Conf;
use clippy_utils::consts::{ConstEvalCtxt, Constant};
use clippy_utils::diagnostics::span_lint_hir_and_then;
use clippy_utils::msrvs::{self, Msrv};
use clippy_utils::source::SpanRangeExt;
use clippy_utils::ty::is_copy;
use clippy_utils::visitors::for_each_local_use_after_expr;
use clippy_utils::{VEC_METHODS_SHADOWING_SLICE_METHODS, get_parent_expr, higher, is_in_test, span_contains_comment};
use rustc_errors::Applicability;
use rustc_hir::{BorrowKind, Expr, ExprKind, HirId, LetStmt, Mutability, Node, Pat, PatKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty;
use rustc_middle::ty::layout::LayoutOf;
use rustc_session::impl_lint_pass;
use rustc_span::{DesugaringKind, Span};
pub struct UselessVec {
too_large_for_stack: u64,
msrv: Msrv,
/// Maps from a `vec![]` source callsite invocation span to the "state" (i.e., whether we can
/// emit a warning there or not).
///
/// The purpose of this is to buffer lints up until `check_crate_post` so that we can cancel a
/// lint while visiting, because a `vec![]` invocation span can appear multiple times when
/// it is passed as a macro argument, once in a context that doesn't require a `Vec<_>` and
/// another time that does. Consider:
/// ```
/// macro_rules! m {
/// ($v:expr) => {
/// let a = $v;
/// $v.push(3);
/// }
/// }
/// m!(vec![1, 2]);
/// ```
/// The macro invocation expands to two `vec![1, 2]` invocations. If we eagerly suggest changing
/// the first `vec![1, 2]` (which is shared with the other expn) to an array which indeed would
/// work, we get a false positive warning on the `$v.push(3)` which really requires `$v` to
/// be a vector.
span_to_state: BTreeMap<Span, VecState>,
allow_in_test: bool,
}
impl UselessVec {
pub fn new(conf: &'static Conf) -> Self {
Self {
too_large_for_stack: conf.too_large_for_stack,
msrv: conf.msrv,
span_to_state: BTreeMap::new(),
allow_in_test: conf.allow_useless_vec_in_tests,
}
}
}
declare_clippy_lint! {
/// ### What it does
/// Checks for usage of `vec![..]` when using `[..]` would
/// be possible.
///
/// ### Why is this bad?
/// This is less efficient.
///
/// ### Example
/// ```no_run
/// fn foo(_x: &[u8]) {}
///
/// foo(&vec![1, 2]);
/// ```
///
/// Use instead:
/// ```no_run
/// # fn foo(_x: &[u8]) {}
/// foo(&[1, 2]);
/// ```
#[clippy::version = "pre 1.29.0"]
pub USELESS_VEC,
perf,
"useless `vec!`"
}
impl_lint_pass!(UselessVec => [USELESS_VEC]);
/// The "state" of a `vec![]` invocation, indicating whether it can or cannot be changed.
enum VecState {
Change {
suggest_ty: SuggestedType,
vec_snippet: String,
expr_hir_id: HirId,
},
NoChange,
}
enum VecToArray {
/// Expression does not need to be a `Vec<_>` and its type can be changed to an array (or
/// slice).
Possible,
/// Expression must be a `Vec<_>`. Type cannot change.
Impossible,
}
impl UselessVec {
/// Checks if the surrounding environment requires this expression to actually be of type
/// `Vec<_>`, or if it can be changed to `&[]`/`[]` without causing type errors.
fn expr_usage_requires_vec(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) -> VecToArray {
match cx.tcx.parent_hir_node(expr.hir_id) {
// search for `let foo = vec![_]` expressions where all uses of `foo`
// adjust to slices or call a method that exist on slices (e.g. len)
Node::LetStmt(LetStmt {
ty: None,
pat:
Pat {
kind: PatKind::Binding(_, id, ..),
..
},
..
}) => {
let only_slice_uses = for_each_local_use_after_expr(cx, *id, expr.hir_id, |expr| {
// allow indexing into a vec and some set of allowed method calls that exist on slices, too
if let Some(parent) = get_parent_expr(cx, expr)
&& (adjusts_to_slice(cx, expr)
|| match parent.kind {
ExprKind::Index(..) => true,
ExprKind::MethodCall(path, _, [], _) => {
// If the given expression is a method call to a `Vec` method that also exists on
// slices, it means that this expression does not actually require a `Vec` and could
// just work with an array.
VEC_METHODS_SHADOWING_SLICE_METHODS.contains(&path.ident.name)
},
_ => false,
})
{
ControlFlow::Continue(())
} else {
ControlFlow::Break(())
}
})
.is_continue();
if only_slice_uses {
VecToArray::Possible
} else {
VecToArray::Impossible
}
},
// if the local pattern has a specified type, do not lint.
Node::LetStmt(LetStmt { ty: Some(_), .. }) if higher::VecArgs::hir(cx, expr).is_some() => {
VecToArray::Impossible
},
// search for `for _ in vec![...]`
Node::Expr(expr)
if expr.span.is_desugaring(DesugaringKind::ForLoop)
&& self.msrv.meets(cx, msrvs::ARRAY_INTO_ITERATOR) =>
{
VecToArray::Possible
},
// search for `&vec![_]` or `vec![_]` expressions where the adjusted type is `&[_]`
_ => {
if adjusts_to_slice(cx, expr) {
VecToArray::Possible
} else {
VecToArray::Impossible
}
},
}
}
}
impl<'tcx> LateLintPass<'tcx> for UselessVec {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
if let Some(vec_args) = higher::VecArgs::hir(cx, expr.peel_borrows())
// The `vec![]` or `&vec![]` invocation span.
&& let vec_span = expr.span.parent_callsite().unwrap_or(expr.span)
&& !vec_span.from_expansion()
{
if self.allow_in_test && is_in_test(cx.tcx, expr.hir_id) {
return;
}
match self.expr_usage_requires_vec(cx, expr) {
VecToArray::Possible => {
let suggest_ty = suggest_type(expr);
// Size and `Copy` checks don't depend on the enclosing usage of the expression
// and don't need to be inserted into the state map.
let vec_snippet = match vec_args {
higher::VecArgs::Repeat(expr, len) => {
if is_copy(cx, cx.typeck_results().expr_ty(expr))
&& let Some(Constant::Int(length)) = ConstEvalCtxt::new(cx).eval(len)
&& let Ok(length) = u64::try_from(length)
&& size_of(cx, expr)
.checked_mul(length)
.is_some_and(|size| size <= self.too_large_for_stack)
{
suggest_ty.snippet(
cx,
Some(expr.span.source_callsite()),
Some(len.span.source_callsite()),
)
} else {
return;
}
},
higher::VecArgs::Vec(args) => {
if let Ok(length) = u64::try_from(args.len())
&& size_of(cx, expr)
.checked_mul(length)
.is_some_and(|size| size <= self.too_large_for_stack)
{
suggest_ty.snippet(
cx,
args.first().zip(args.last()).map(|(first, last)| {
first.span.source_callsite().to(last.span.source_callsite())
}),
None,
)
} else {
return;
}
},
};
if let Entry::Vacant(entry) = self.span_to_state.entry(vec_span) {
entry.insert(VecState::Change {
suggest_ty,
vec_snippet,
expr_hir_id: expr.hir_id,
});
}
},
VecToArray::Impossible => {
self.span_to_state.insert(vec_span, VecState::NoChange);
},
}
}
}
fn check_crate_post(&mut self, cx: &LateContext<'tcx>) {
for (span, state) in mem::take(&mut self.span_to_state) {
if let VecState::Change {
suggest_ty,
vec_snippet,
expr_hir_id,
} = state
{
span_lint_hir_and_then(cx, USELESS_VEC, expr_hir_id, span, "useless use of `vec!`", |diag| {
let help_msg = format!("you can use {} directly", suggest_ty.desc());
// If the `vec!` macro contains comment, better not make the suggestion machine applicable as it
// would remove them.
let applicability = if span_contains_comment(cx.tcx.sess.source_map(), span) {
Applicability::Unspecified
} else {
Applicability::MachineApplicable
};
diag.span_suggestion(span, help_msg, vec_snippet, applicability);
});
}
}
}
}
#[derive(Copy, Clone)]
pub(crate) enum SuggestedType {
/// Suggest using a slice `&[..]` / `&mut [..]`
SliceRef(Mutability),
/// Suggest using an array: `[..]`
Array,
}
impl SuggestedType {
fn desc(self) -> &'static str {
match self {
Self::SliceRef(_) => "a slice",
Self::Array => "an array",
}
}
fn snippet(self, cx: &LateContext<'_>, args_span: Option<Span>, len_span: Option<Span>) -> String {
// Invariant of the lint as implemented: all spans are from the root context (and as a result,
// always trivially crate-local).
assert!(args_span.is_none_or(|s| !s.from_expansion()));
assert!(len_span.is_none_or(|s| !s.from_expansion()));
let maybe_args = args_span.map(|sp| sp.get_source_text(cx).expect("spans are always crate-local"));
let maybe_args = maybe_args.as_deref().unwrap_or_default();
let maybe_len = len_span
.map(|sp| sp.get_source_text(cx).expect("spans are always crate-local"))
.map(|st| format!("; {st}"))
.unwrap_or_default();
match self {
Self::SliceRef(Mutability::Mut) => format!("&mut [{maybe_args}{maybe_len}]"),
Self::SliceRef(Mutability::Not) => format!("&[{maybe_args}{maybe_len}]"),
Self::Array => format!("[{maybe_args}{maybe_len}]"),
}
}
}
fn size_of(cx: &LateContext<'_>, expr: &Expr<'_>) -> u64 {
let ty = cx.typeck_results().expr_ty_adjusted(expr);
cx.layout_of(ty).map_or(0, |l| l.size.bytes())
}
fn adjusts_to_slice(cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
matches!(cx.typeck_results().expr_ty_adjusted(e).kind(), ty::Ref(_, ty, _) if ty.is_slice())
}
fn suggest_type(expr: &Expr<'_>) -> SuggestedType {
if let ExprKind::AddrOf(BorrowKind::Ref, mutability, _) = expr.kind {
// `expr` is `&vec![_]`, so suggest `&[_]` (or `&mut[_]` resp.)
SuggestedType::SliceRef(mutability)
} else {
// `expr` is the `vec![_]` expansion, so suggest `[_]`
SuggestedType::Array
}
}