diff --git a/CHANGELOG.md b/CHANGELOG.md index 66833c1810a..f54bfbfa472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4633,6 +4633,7 @@ Released 2018-09-13 [`large_const_arrays`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_const_arrays [`large_digit_groups`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_digit_groups [`large_enum_variant`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant +[`large_futures`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_futures [`large_include_file`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_include_file [`large_stack_arrays`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_stack_arrays [`large_types_passed_by_value`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_types_passed_by_value diff --git a/book/src/lint_configuration.md b/book/src/lint_configuration.md index dbd1a404150..4415a05251e 100644 --- a/book/src/lint_configuration.md +++ b/book/src/lint_configuration.md @@ -54,6 +54,7 @@ Please use that command to update the file and do not edit it by hand. | [allow-mixed-uninlined-format-args](#allow-mixed-uninlined-format-args) | `true` | | [suppress-restriction-lint-in-const](#suppress-restriction-lint-in-const) | `false` | | [missing-docs-in-crate-items](#missing-docs-in-crate-items) | `false` | +| [future-size-threshold](#future-size-threshold) | `16384` | ### arithmetic-side-effects-allowed Suppress checking of the passed type names in all types of operations. @@ -552,4 +553,12 @@ crate. For example, `pub(crate)` items. * [missing_docs_in_private_items](https://rust-lang.github.io/rust-clippy/master/index.html#missing_docs_in_private_items) +### future-size-threshold +The maximum byte size a `Future` can have, before it triggers the `clippy::large_futures` lint + +**Default Value:** `16384` (`u64`) + +* [large_futures](https://rust-lang.github.io/rust-clippy/master/index.html#large_futures) + + diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index e550b44e2da..f07c7f0534c 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -218,6 +218,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::iter_not_returning_iterator::ITER_NOT_RETURNING_ITERATOR_INFO, crate::large_const_arrays::LARGE_CONST_ARRAYS_INFO, crate::large_enum_variant::LARGE_ENUM_VARIANT_INFO, + crate::large_futures::LARGE_FUTURES_INFO, crate::large_include_file::LARGE_INCLUDE_FILE_INFO, crate::large_stack_arrays::LARGE_STACK_ARRAYS_INFO, crate::len_zero::COMPARISON_TO_EMPTY_INFO, diff --git a/clippy_lints/src/large_futures.rs b/clippy_lints/src/large_futures.rs new file mode 100644 index 00000000000..1b054481371 --- /dev/null +++ b/clippy_lints/src/large_futures.rs @@ -0,0 +1,87 @@ +use clippy_utils::source::snippet; +use clippy_utils::{diagnostics::span_lint_and_sugg, ty::implements_trait}; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, LangItem, MatchSource, QPath}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_target::abi::Size; + +declare_clippy_lint! { + /// ### What it does + /// It checks for the size of a `Future` created by `async fn` or `async {}`. + /// + /// ### Why is this bad? + /// Due to the current [unideal implemention](https://github.com/rust-lang/rust/issues/69826) of `Generator`, + /// large size of a `Future` may cause stack overflows. + /// + /// ### Example + /// ```rust + /// async fn wait(f: impl std::future::Future) {} + /// + /// async fn big_fut(arg: [u8; 1024]) {} + /// + /// pub async fn test() { + /// let fut = big_fut([0u8; 1024]); + /// wait(fut).await; + /// } + /// ``` + /// + /// `Box::pin` the big future instead. + /// + /// ```rust + /// async fn wait(f: impl std::future::Future) {} + /// + /// async fn big_fut(arg: [u8; 1024]) {} + /// + /// pub async fn test() { + /// let fut = Box::pin(big_fut([0u8; 1024])); + /// wait(fut).await; + /// } + /// ``` + #[clippy::version = "1.68.0"] + pub LARGE_FUTURES, + pedantic, + "large future may lead to unexpected stack overflows" +} + +#[derive(Copy, Clone)] +pub struct LargeFuture { + future_size_threshold: u64, +} + +impl LargeFuture { + pub fn new(future_size_threshold: u64) -> Self { + Self { future_size_threshold } + } +} + +impl_lint_pass!(LargeFuture => [LARGE_FUTURES]); + +impl<'tcx> LateLintPass<'tcx> for LargeFuture { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if matches!(expr.span.ctxt().outer_expn_data().kind, rustc_span::ExpnKind::Macro(..)) { + return; + } + if let ExprKind::Match(expr, _, MatchSource::AwaitDesugar) = expr.kind { + if let ExprKind::Call(func, [expr, ..]) = expr.kind + && let ExprKind::Path(QPath::LangItem(LangItem::IntoFutureIntoFuture, ..)) = func.kind + && let ty = cx.typeck_results().expr_ty(expr) + && let Some(future_trait_def_id) = cx.tcx.lang_items().future_trait() + && implements_trait(cx, ty, future_trait_def_id, &[]) + && let Ok(layout) = cx.tcx.layout_of(cx.param_env.and(ty)) + && let size = layout.layout.size() + && size >= Size::from_bytes(self.future_size_threshold) + { + span_lint_and_sugg( + cx, + LARGE_FUTURES, + expr.span, + &format!("large future with a size of {} bytes", size.bytes()), + "consider `Box::pin` on it", + format!("Box::pin({})", snippet(cx, expr.span, "..")), + Applicability::Unspecified, + ); + } + } + } +} diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index 9edac5227c6..75c53a09d79 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -163,6 +163,7 @@ mod items_after_statements; mod iter_not_returning_iterator; mod large_const_arrays; mod large_enum_variant; +mod large_futures; mod large_include_file; mod large_stack_arrays; mod len_zero; @@ -810,6 +811,8 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_late_pass(move |_| Box::new(dereference::Dereferencing::new(msrv()))); store.register_late_pass(|_| Box::new(option_if_let_else::OptionIfLetElse)); store.register_late_pass(|_| Box::new(future_not_send::FutureNotSend)); + let future_size_threshold = conf.future_size_threshold; + store.register_late_pass(move |_| Box::new(large_futures::LargeFuture::new(future_size_threshold))); store.register_late_pass(|_| Box::new(if_let_mutex::IfLetMutex)); store.register_late_pass(|_| Box::new(if_not_else::IfNotElse)); store.register_late_pass(|_| Box::new(equatable_if_let::PatternEquality)); diff --git a/clippy_lints/src/utils/conf.rs b/clippy_lints/src/utils/conf.rs index 5384ae01f92..e1c9dd10117 100644 --- a/clippy_lints/src/utils/conf.rs +++ b/clippy_lints/src/utils/conf.rs @@ -459,6 +459,10 @@ define_Conf! { /// Whether to **only** check for missing documentation in items visible within the current /// crate. For example, `pub(crate)` items. (missing_docs_in_crate_items: bool = false), + /// Lint: LARGE_FUTURES. + /// + /// The maximum byte size a `Future` can have, before it triggers the `clippy::large_futures` lint + (future_size_threshold: u64 = 16 * 1024), } /// Search for the configuration file. diff --git a/tests/ui-toml/large_futures/clippy.toml b/tests/ui-toml/large_futures/clippy.toml new file mode 100644 index 00000000000..61bb17fdf6b --- /dev/null +++ b/tests/ui-toml/large_futures/clippy.toml @@ -0,0 +1 @@ +future-size-threshold = 1024 diff --git a/tests/ui-toml/large_futures/large_futures.rs b/tests/ui-toml/large_futures/large_futures.rs new file mode 100644 index 00000000000..4158df8b5ff --- /dev/null +++ b/tests/ui-toml/large_futures/large_futures.rs @@ -0,0 +1,27 @@ +#![warn(clippy::large_futures)] + +fn main() {} + +pub async fn should_warn() { + let x = [0u8; 1024]; + async {}.await; + dbg!(x); +} + +pub async fn should_not_warn() { + let x = [0u8; 1020]; + async {}.await; + dbg!(x); +} + +pub async fn bar() { + should_warn().await; + + async { + let x = [0u8; 1024]; + dbg!(x); + } + .await; + + should_not_warn().await; +} diff --git a/tests/ui-toml/large_futures/large_futures.stderr b/tests/ui-toml/large_futures/large_futures.stderr new file mode 100644 index 00000000000..b92734de2f0 --- /dev/null +++ b/tests/ui-toml/large_futures/large_futures.stderr @@ -0,0 +1,10 @@ +error: large future with a size of 1026 bytes + --> $DIR/large_futures.rs:18:5 + | +LL | should_warn().await; + | ^^^^^^^^^^^^^ help: consider `Box::pin` on it: `Box::pin(should_warn())` + | + = note: `-D clippy::large-futures` implied by `-D warnings` + +error: aborting due to previous error + diff --git a/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr b/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr index 6a246afac76..8447c31722d 100644 --- a/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr +++ b/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr @@ -24,6 +24,7 @@ error: error reading Clippy's configuration file `$DIR/clippy.toml`: unknown fie enforced-import-renames enum-variant-name-threshold enum-variant-size-threshold + future-size-threshold ignore-interior-mutability large-error-threshold literal-representation-threshold diff --git a/tests/ui/large_futures.rs b/tests/ui/large_futures.rs new file mode 100644 index 00000000000..4a8ba995da5 --- /dev/null +++ b/tests/ui/large_futures.rs @@ -0,0 +1,61 @@ +#![feature(generators)] +#![warn(clippy::large_futures)] +#![allow(clippy::future_not_send)] +#![allow(clippy::manual_async_fn)] + +async fn big_fut(_arg: [u8; 1024 * 16]) {} + +async fn wait() { + let f = async { + big_fut([0u8; 1024 * 16]).await; + }; + f.await +} +async fn calls_fut(fut: impl std::future::Future) { + loop { + wait().await; + if true { + return fut.await; + } else { + wait().await; + } + } +} + +pub async fn test() { + let fut = big_fut([0u8; 1024 * 16]); + foo().await; + calls_fut(fut).await; +} + +pub fn foo() -> impl std::future::Future { + async { + let x = [0i32; 1024 * 16]; + async {}.await; + dbg!(x); + } +} + +pub async fn lines() { + async { + let x = [0i32; 1024 * 16]; + async {}.await; + println!("{:?}", x); + } + .await; +} + +pub async fn macro_expn() { + macro_rules! macro_ { + () => { + async { + let x = [0i32; 1024 * 16]; + async {}.await; + println!("macro: {:?}", x); + } + }; + } + macro_!().await +} + +fn main() {} diff --git a/tests/ui/large_futures.stderr b/tests/ui/large_futures.stderr new file mode 100644 index 00000000000..67e0fceff6e --- /dev/null +++ b/tests/ui/large_futures.stderr @@ -0,0 +1,82 @@ +error: large future with a size of 16385 bytes + --> $DIR/large_futures.rs:10:9 + | +LL | big_fut([0u8; 1024 * 16]).await; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider `Box::pin` on it: `Box::pin(big_fut([0u8; 1024 * 16]))` + | + = note: `-D clippy::large-futures` implied by `-D warnings` + +error: large future with a size of 16386 bytes + --> $DIR/large_futures.rs:12:5 + | +LL | f.await + | ^ help: consider `Box::pin` on it: `Box::pin(f)` + +error: large future with a size of 16387 bytes + --> $DIR/large_futures.rs:16:9 + | +LL | wait().await; + | ^^^^^^ help: consider `Box::pin` on it: `Box::pin(wait())` + +error: large future with a size of 16387 bytes + --> $DIR/large_futures.rs:20:13 + | +LL | wait().await; + | ^^^^^^ help: consider `Box::pin` on it: `Box::pin(wait())` + +error: large future with a size of 65540 bytes + --> $DIR/large_futures.rs:27:5 + | +LL | foo().await; + | ^^^^^ help: consider `Box::pin` on it: `Box::pin(foo())` + +error: large future with a size of 49159 bytes + --> $DIR/large_futures.rs:28:5 + | +LL | calls_fut(fut).await; + | ^^^^^^^^^^^^^^ help: consider `Box::pin` on it: `Box::pin(calls_fut(fut))` + +error: large future with a size of 65540 bytes + --> $DIR/large_futures.rs:40:5 + | +LL | / async { +LL | | let x = [0i32; 1024 * 16]; +LL | | async {}.await; +LL | | println!("{:?}", x); +LL | | } + | |_____^ + | +help: consider `Box::pin` on it + | +LL ~ Box::pin(async { +LL + let x = [0i32; 1024 * 16]; +LL + async {}.await; +LL + println!("{:?}", x); +LL + }) + | + +error: large future with a size of 65540 bytes + --> $DIR/large_futures.rs:51:13 + | +LL | / async { +LL | | let x = [0i32; 1024 * 16]; +LL | | async {}.await; +LL | | println!("macro: {:?}", x); +LL | | } + | |_____________^ +... +LL | macro_!().await + | --------- in this macro invocation + | + = note: this error originates in the macro `macro_` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider `Box::pin` on it + | +LL ~ Box::pin(async { +LL + let x = [0i32; 1024 * 16]; +LL + async {}.await; +LL + println!("macro: {:?}", x); +LL + }) + | + +error: aborting due to 8 previous errors +