| use std::{fmt, ops}; |
| |
| use clippy_config::Conf; |
| use clippy_utils::diagnostics::span_lint_and_then; |
| use clippy_utils::fn_has_unsatisfiable_preds; |
| use clippy_utils::source::SpanRangeExt; |
| 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 { |
| pub fn new(conf: &'static Conf) -> Self { |
| Self { |
| maximum_allowed_size: conf.stack_size_threshold, |
| } |
| } |
| } |
| |
| 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 typing_env = mir.typing_env(cx.tcx); |
| |
| let sizes_of_locals = || { |
| mir.local_decls.iter().filter_map(|local| { |
| let layout = cx.tcx.layout_of(typing_env.as_query_input(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) = local_span.get_source_text(cx) |
| && 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", |
| ); |
| }, |
| ); |
| } |
| } |
| } |