rust/clippy_lints/src/large_stack_frames.rs
2024-04-01 15:04:45 +02:00

221 lines
8.6 KiB
Rust

use std::{fmt, ops};
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::fn_has_unsatisfiable_preds;
use clippy_utils::source::snippet_opt;
use rustc_hir::def_id::LocalDefId;
use rustc_hir::intravisit::FnKind;
use rustc_hir::{Body, FnDecl};
use rustc_lexer::is_ident;
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::impl_lint_pass;
use rustc_span::Span;
declare_clippy_lint! {
/// ### What it does
/// Checks for functions that use a lot of stack space.
///
/// This often happens when constructing a large type, such as an array with a lot of elements,
/// or constructing *many* smaller-but-still-large structs, or copying around a lot of large types.
///
/// This lint is a more general version of [`large_stack_arrays`](https://rust-lang.github.io/rust-clippy/master/#large_stack_arrays)
/// that is intended to look at functions as a whole instead of only individual array expressions inside of a function.
///
/// ### Why is this bad?
/// The stack region of memory is very limited in size (usually *much* smaller than the heap) and attempting to
/// use too much will result in a stack overflow and crash the program.
/// To avoid this, you should consider allocating large types on the heap instead (e.g. by boxing them).
///
/// Keep in mind that the code path to construction of large types does not even need to be reachable;
/// it purely needs to *exist* inside of the function to contribute to the stack size.
/// For example, this causes a stack overflow even though the branch is unreachable:
/// ```rust,ignore
/// fn main() {
/// if false {
/// let x = [0u8; 10000000]; // 10 MB stack array
/// black_box(&x);
/// }
/// }
/// ```
///
/// ### Known issues
/// False positives. The stack size that clippy sees is an estimated value and can be vastly different
/// from the actual stack usage after optimizations passes have run (especially true in release mode).
/// Modern compilers are very smart and are able to optimize away a lot of unnecessary stack allocations.
/// In debug mode however, it is usually more accurate.
///
/// This lint works by summing up the size of all variables that the user typed, variables that were
/// implicitly introduced by the compiler for temporaries, function arguments and the return value,
/// and comparing them against a (configurable, but high-by-default).
///
/// ### Example
/// This function creates four 500 KB arrays on the stack. Quite big but just small enough to not trigger `large_stack_arrays`.
/// However, looking at the function as a whole, it's clear that this uses a lot of stack space.
/// ```no_run
/// struct QuiteLargeType([u8; 500_000]);
/// fn foo() {
/// // ... some function that uses a lot of stack space ...
/// let _x1 = QuiteLargeType([0; 500_000]);
/// let _x2 = QuiteLargeType([0; 500_000]);
/// let _x3 = QuiteLargeType([0; 500_000]);
/// let _x4 = QuiteLargeType([0; 500_000]);
/// }
/// ```
///
/// Instead of doing this, allocate the arrays on the heap.
/// This currently requires going through a `Vec` first and then converting it to a `Box`:
/// ```no_run
/// struct NotSoLargeType(Box<[u8]>);
///
/// fn foo() {
/// let _x1 = NotSoLargeType(vec![0; 500_000].into_boxed_slice());
/// // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Now heap allocated.
/// // The size of `NotSoLargeType` is 16 bytes.
/// // ...
/// }
/// ```
#[clippy::version = "1.72.0"]
pub LARGE_STACK_FRAMES,
nursery,
"checks for functions that allocate a lot of stack space"
}
pub struct LargeStackFrames {
maximum_allowed_size: u64,
}
impl LargeStackFrames {
#[must_use]
pub fn new(size: u64) -> Self {
Self {
maximum_allowed_size: size,
}
}
}
impl_lint_pass!(LargeStackFrames => [LARGE_STACK_FRAMES]);
#[derive(Copy, Clone)]
enum Space {
Used(u64),
Overflow,
}
impl Space {
pub fn exceeds_limit(self, limit: u64) -> bool {
match self {
Self::Used(used) => used > limit,
Self::Overflow => true,
}
}
}
impl fmt::Display for Space {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Space::Used(1) => write!(f, "1 byte"),
Space::Used(n) => write!(f, "{n} bytes"),
Space::Overflow => write!(f, "over 2⁶⁴-1 bytes"),
}
}
}
impl ops::Add<u64> for Space {
type Output = Self;
fn add(self, rhs: u64) -> Self {
match self {
Self::Used(lhs) => match lhs.checked_add(rhs) {
Some(sum) => Self::Used(sum),
None => Self::Overflow,
},
Self::Overflow => self,
}
}
}
impl<'tcx> LateLintPass<'tcx> for LargeStackFrames {
fn check_fn(
&mut self,
cx: &LateContext<'tcx>,
fn_kind: FnKind<'tcx>,
_: &'tcx FnDecl<'tcx>,
_: &'tcx Body<'tcx>,
entire_fn_span: Span,
local_def_id: LocalDefId,
) {
let def_id = local_def_id.to_def_id();
// Building MIR for `fn`s with unsatisfiable preds results in ICE.
if fn_has_unsatisfiable_preds(cx, def_id) {
return;
}
let mir = cx.tcx.optimized_mir(def_id);
let param_env = cx.tcx.param_env(def_id);
let sizes_of_locals = || {
mir.local_decls.iter().filter_map(|local| {
let layout = cx.tcx.layout_of(param_env.and(local.ty)).ok()?;
Some((local, layout.size.bytes()))
})
};
let frame_size = sizes_of_locals().fold(Space::Used(0), |sum, (_, size)| sum + size);
let limit = self.maximum_allowed_size;
if frame_size.exceeds_limit(limit) {
// Point at just the function name if possible, because lints that span
// the entire body and don't have to are less legible.
let fn_span = match fn_kind {
FnKind::ItemFn(ident, _, _) | FnKind::Method(ident, _) => ident.span,
FnKind::Closure => entire_fn_span,
};
span_lint_and_then(
cx,
LARGE_STACK_FRAMES,
fn_span,
format!("this function may allocate {frame_size} on the stack"),
|diag| {
// Point out the largest individual contribution to this size, because
// it is the most likely to be unintentionally large.
if let Some((local, size)) = sizes_of_locals().max_by_key(|&(_, size)| size) {
let local_span: Span = local.source_info.span;
let size = Space::Used(size); // pluralizes for us
let ty = local.ty;
// TODO: Is there a cleaner, robust way to ask this question?
// The obvious `LocalDecl::is_user_variable()` panics on "unwrapping cross-crate data",
// and that doesn't get us the true name in scope rather than the span text either.
if let Some(name) = snippet_opt(cx, local_span)
&& is_ident(&name)
{
// If the local is an ordinary named variable,
// print its name rather than relying solely on the span.
diag.span_label(
local_span,
format!("`{name}` is the largest part, at {size} for type `{ty}`"),
);
} else {
diag.span_label(
local_span,
format!("this is the largest part, at {size} for type `{ty}`"),
);
}
}
// Explain why we are linting this and not other functions.
diag.note(format!(
"{frame_size} is larger than Clippy's configured `stack-size-threshold` of {limit}"
));
// Explain why the user should care, briefly.
diag.note_once(
"allocating large amounts of stack space can overflow the stack \
and cause the program to abort",
);
},
);
}
}
}