| use std::{fmt, ops}; | |
| use clippy_config::Conf; | |
| use clippy_utils::diagnostics::span_lint_and_then; | |
| use clippy_utils::source::SpanExt as _; | |
| use clippy_utils::{fn_has_unsatisfiable_clauses, is_entrypoint_fn, is_in_test}; | |
| use rustc_errors::Diag; | |
| 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, impl_lint_pass}; | |
| use rustc_span::{Span, SyntaxContext}; | |
| 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/main/#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" | |
| } | |
| impl_lint_pass!(LargeStackFrames => [LARGE_STACK_FRAMES]); | |
| pub struct LargeStackFrames { | |
| maximum_allowed_size: u64, | |
| allow_large_stack_frames_in_tests: bool, | |
| } | |
| impl LargeStackFrames { | |
| pub fn new(conf: &'static Conf) -> Self { | |
| Self { | |
| maximum_allowed_size: conf.stack_size_threshold, | |
| allow_large_stack_frames_in_tests: conf.allow_large_stack_frames_in_tests, | |
| } | |
| } | |
| } | |
| #[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_clauses(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())) | |
| }) | |
| .collect::<Vec<_>>(); | |
| let frame_size = sizes_of_locals | |
| .iter() | |
| .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, fn_name) = match fn_kind { | |
| FnKind::ItemFn(ident, _, _) => (ident.span, format!("function `{}`", ident.name)), | |
| FnKind::Method(ident, _) => (ident.span, format!("method `{}`", ident.name)), | |
| FnKind::Closure => (entire_fn_span, "closure".to_string()), | |
| }; | |
| // Don't lint inside tests if configured to not do so. | |
| if self.allow_large_stack_frames_in_tests && is_in_test(cx.tcx, cx.tcx.local_def_id_to_hir_id(local_def_id)) | |
| { | |
| return; | |
| } | |
| let explain_lint = |diag: &mut Diag<'_>, ctxt: SyntaxContext| { | |
| // 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.iter().max_by_key(|&(_, size)| size) | |
| && let local_span = local.source_info.span | |
| && local_span.ctxt() == ctxt | |
| { | |
| 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_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", | |
| ); | |
| }; | |
| if fn_span.from_expansion() { | |
| // Don't lint on the main function generated by `--test` target | |
| if cx.tcx.sess.is_test_crate() && is_entrypoint_fn(cx, local_def_id.to_def_id()) { | |
| return; | |
| } | |
| let is_from_external_macro = fn_span.in_external_macro(cx.tcx.sess.source_map()); | |
| span_lint_and_then( | |
| cx, | |
| LARGE_STACK_FRAMES, | |
| fn_span.source_callsite(), | |
| format!( | |
| "{} generated by this macro may allocate a lot of stack space", | |
| if is_from_external_macro { | |
| cx.tcx.def_descr(local_def_id.into()) | |
| } else { | |
| fn_name.as_str() | |
| } | |
| ), | |
| |diag| { | |
| if is_from_external_macro { | |
| return; | |
| } | |
| diag.span_label( | |
| fn_span, | |
| format!( | |
| "this {} has a stack frame size of {frame_size}", | |
| cx.tcx.def_descr(local_def_id.into()) | |
| ), | |
| ); | |
| explain_lint(diag, fn_span.ctxt()); | |
| }, | |
| ); | |
| return; | |
| } | |
| span_lint_and_then( | |
| cx, | |
| LARGE_STACK_FRAMES, | |
| fn_span, | |
| format!("this function may allocate {frame_size} on the stack"), | |
| |diag| { | |
| explain_lint(diag, SyntaxContext::root()); | |
| }, | |
| ); | |
| } | |
| } | |
| } |