blob: b8d4e7c4651d201507de10e526d49b691d8fd262 [file] [log] [blame] [edit]
use clippy_utils::diagnostics::{span_lint_hir, span_lint_hir_and_then};
use clippy_utils::higher::{VecInitKind, get_vec_init_kind};
use clippy_utils::source::{indent_of, snippet};
use clippy_utils::{get_enclosing_block, sym};
use rustc_errors::Applicability;
use rustc_hir::def::Res;
use rustc_hir::intravisit::{Visitor, walk_expr};
use rustc_hir::{self as hir, Expr, ExprKind, HirId, LetStmt, PatKind, PathSegment, QPath, StmtKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass;
declare_clippy_lint! {
/// ### What it does
/// This lint catches reads into a zero-length `Vec`.
/// Especially in the case of a call to `with_capacity`, this lint warns that read
/// gets the number of bytes from the `Vec`'s length, not its capacity.
///
/// ### Why is this bad?
/// Reading zero bytes is almost certainly not the intended behavior.
///
/// ### Known problems
/// In theory, a very unusual read implementation could assign some semantic meaning
/// to zero-byte reads. But it seems exceptionally unlikely that code intending to do
/// a zero-byte read would allocate a `Vec` for it.
///
/// ### Example
/// ```no_run
/// use std::io;
/// fn foo<F: io::Read>(mut f: F) {
/// let mut data = Vec::with_capacity(100);
/// f.read(&mut data).unwrap();
/// }
/// ```
/// Use instead:
/// ```no_run
/// use std::io;
/// fn foo<F: io::Read>(mut f: F) {
/// let mut data = Vec::with_capacity(100);
/// data.resize(100, 0);
/// f.read(&mut data).unwrap();
/// }
/// ```
#[clippy::version = "1.63.0"]
pub READ_ZERO_BYTE_VEC,
nursery,
"checks for reads into a zero-length `Vec`"
}
declare_lint_pass!(ReadZeroByteVec => [READ_ZERO_BYTE_VEC]);
impl<'tcx> LateLintPass<'tcx> for ReadZeroByteVec {
fn check_block(&mut self, cx: &LateContext<'tcx>, block: &hir::Block<'tcx>) {
for stmt in block.stmts {
if stmt.span.from_expansion() {
return;
}
if let StmtKind::Let(local) = stmt.kind
&& let LetStmt {
pat, init: Some(init), ..
} = local
&& let PatKind::Binding(_, id, ident, _) = pat.kind
&& let Some(vec_init_kind) = get_vec_init_kind(cx, init)
{
let mut visitor = ReadVecVisitor {
local_id: id,
read_zero_expr: None,
has_resize: false,
};
let Some(enclosing_block) = get_enclosing_block(cx, id) else {
return;
};
visitor.visit_block(enclosing_block);
if let Some(expr) = visitor.read_zero_expr {
let applicability = Applicability::MaybeIncorrect;
match vec_init_kind {
VecInitKind::WithConstCapacity(len) => span_lint_hir_and_then(
cx,
READ_ZERO_BYTE_VEC,
expr.hir_id,
expr.span,
"reading zero byte data to `Vec`",
|diag| {
let span = first_stmt_containing_expr(cx, expr).map_or(expr.span, |stmt| stmt.span);
let indent = indent_of(cx, span).unwrap_or(0);
diag.span_suggestion(
span.shrink_to_lo(),
"try",
format!("{ident}.resize({len}, 0);\n{}", " ".repeat(indent)),
applicability,
);
},
),
VecInitKind::WithExprCapacity(hir_id) => {
let e = cx.tcx.hir_expect_expr(hir_id);
span_lint_hir_and_then(
cx,
READ_ZERO_BYTE_VEC,
expr.hir_id,
expr.span,
"reading zero byte data to `Vec`",
|diag| {
let span = first_stmt_containing_expr(cx, expr).map_or(expr.span, |stmt| stmt.span);
let indent = indent_of(cx, span).unwrap_or(0);
diag.span_suggestion(
span.shrink_to_lo(),
"try",
format!(
"{ident}.resize({}, 0);\n{}",
snippet(cx, e.span, ".."),
" ".repeat(indent)
),
applicability,
);
},
);
},
_ => {
span_lint_hir(
cx,
READ_ZERO_BYTE_VEC,
expr.hir_id,
expr.span,
"reading zero byte data to `Vec`",
);
},
}
}
}
}
}
}
fn first_stmt_containing_expr<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<&'tcx hir::Stmt<'tcx>> {
cx.tcx.hir_parent_iter(expr.hir_id).find_map(|(_, node)| {
if let hir::Node::Stmt(stmt) = node {
Some(stmt)
} else {
None
}
})
}
struct ReadVecVisitor<'tcx> {
local_id: HirId,
read_zero_expr: Option<&'tcx Expr<'tcx>>,
has_resize: bool,
}
impl<'tcx> Visitor<'tcx> for ReadVecVisitor<'tcx> {
fn visit_expr(&mut self, e: &'tcx Expr<'tcx>) {
if let ExprKind::MethodCall(path, receiver, args, _) = e.kind {
let PathSegment { ident, .. } = *path;
match ident.name {
sym::read | sym::read_exact => {
let [arg] = args else { return };
if let ExprKind::AddrOf(_, hir::Mutability::Mut, inner) = arg.kind
&& let ExprKind::Path(QPath::Resolved(None, inner_path)) = inner.kind
&& let [inner_seg] = inner_path.segments
&& let Res::Local(res_id) = inner_seg.res
&& self.local_id == res_id
{
self.read_zero_expr = Some(e);
return;
}
},
sym::resize => {
// If the Vec is resized, then it's a valid read
if let ExprKind::Path(QPath::Resolved(_, inner_path)) = receiver.kind
&& let Res::Local(res_id) = inner_path.res
&& self.local_id == res_id
{
self.has_resize = true;
return;
}
},
_ => {},
}
}
if !self.has_resize && self.read_zero_expr.is_none() {
walk_expr(self, e);
}
}
}