221 lines
8.6 KiB
Rust
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",
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|