about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--book/src/lint_configuration.md1
-rw-r--r--clippy_config/src/conf.rs1
-rw-r--r--clippy_lints/src/declared_lints.rs1
-rw-r--r--clippy_lints/src/lib.rs2
-rw-r--r--clippy_lints/src/non_std_lazy_statics.rs306
-rw-r--r--clippy_utils/src/msrvs.rs2
-rw-r--r--tests/ui/non_std_lazy_static/auxiliary/lazy_static.rs20
-rw-r--r--tests/ui/non_std_lazy_static/auxiliary/once_cell.rs55
-rw-r--r--tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.fixed72
-rw-r--r--tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.rs72
-rw-r--r--tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.stderr100
-rw-r--r--tests/ui/non_std_lazy_static/non_std_lazy_static_no_std.rs20
-rw-r--r--tests/ui/non_std_lazy_static/non_std_lazy_static_other_once_cell.rs12
-rw-r--r--tests/ui/non_std_lazy_static/non_std_lazy_static_unfixable.rs43
-rw-r--r--tests/ui/non_std_lazy_static/non_std_lazy_static_unfixable.stderr66
16 files changed, 773 insertions, 1 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b87d026fda1..ce5b8fb392f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5906,6 +5906,7 @@ Released 2018-09-13
 [`non_minimal_cfg`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_minimal_cfg
 [`non_octal_unix_permissions`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_octal_unix_permissions
 [`non_send_fields_in_send_ty`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_send_fields_in_send_ty
+[`non_std_lazy_statics`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_std_lazy_statics
 [`non_zero_suggestions`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_zero_suggestions
 [`nonminimal_bool`]: https://rust-lang.github.io/rust-clippy/master/index.html#nonminimal_bool
 [`nonsensical_open_options`]: https://rust-lang.github.io/rust-clippy/master/index.html#nonsensical_open_options
diff --git a/book/src/lint_configuration.md b/book/src/lint_configuration.md
index a3e10088db2..999df0efaac 100644
--- a/book/src/lint_configuration.md
+++ b/book/src/lint_configuration.md
@@ -762,6 +762,7 @@ The minimum rust version that the project supports. Defaults to the `rust-versio
 * [`mem_replace_with_default`](https://rust-lang.github.io/rust-clippy/master/index.html#mem_replace_with_default)
 * [`missing_const_for_fn`](https://rust-lang.github.io/rust-clippy/master/index.html#missing_const_for_fn)
 * [`needless_borrow`](https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow)
+* [`non_std_lazy_statics`](https://rust-lang.github.io/rust-clippy/master/index.html#non_std_lazy_statics)
 * [`option_as_ref_deref`](https://rust-lang.github.io/rust-clippy/master/index.html#option_as_ref_deref)
 * [`option_map_unwrap_or`](https://rust-lang.github.io/rust-clippy/master/index.html#option_map_unwrap_or)
 * [`ptr_as_ptr`](https://rust-lang.github.io/rust-clippy/master/index.html#ptr_as_ptr)
diff --git a/clippy_config/src/conf.rs b/clippy_config/src/conf.rs
index 9bf6c675f00..4b0fcbfc3bc 100644
--- a/clippy_config/src/conf.rs
+++ b/clippy_config/src/conf.rs
@@ -631,6 +631,7 @@ define_Conf! {
         mem_replace_with_default,
         missing_const_for_fn,
         needless_borrow,
+        non_std_lazy_statics,
         option_as_ref_deref,
         option_map_unwrap_or,
         ptr_as_ptr,
diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs
index 6d6b415f8cd..86410c16d95 100644
--- a/clippy_lints/src/declared_lints.rs
+++ b/clippy_lints/src/declared_lints.rs
@@ -576,6 +576,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
     crate::non_expressive_names::SIMILAR_NAMES_INFO,
     crate::non_octal_unix_permissions::NON_OCTAL_UNIX_PERMISSIONS_INFO,
     crate::non_send_fields_in_send_ty::NON_SEND_FIELDS_IN_SEND_TY_INFO,
+    crate::non_std_lazy_statics::NON_STD_LAZY_STATICS_INFO,
     crate::non_zero_suggestions::NON_ZERO_SUGGESTIONS_INFO,
     crate::nonstandard_macro_braces::NONSTANDARD_MACRO_BRACES_INFO,
     crate::octal_escapes::OCTAL_ESCAPES_INFO,
diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs
index 85f99a9e055..4b700673d0f 100644
--- a/clippy_lints/src/lib.rs
+++ b/clippy_lints/src/lib.rs
@@ -279,6 +279,7 @@ mod non_copy_const;
 mod non_expressive_names;
 mod non_octal_unix_permissions;
 mod non_send_fields_in_send_ty;
+mod non_std_lazy_statics;
 mod non_zero_suggestions;
 mod nonstandard_macro_braces;
 mod octal_escapes;
@@ -974,5 +975,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
     store.register_late_pass(move |_| Box::new(arbitrary_source_item_ordering::ArbitrarySourceItemOrdering::new(conf)));
     store.register_late_pass(|_| Box::new(unneeded_struct_pattern::UnneededStructPattern));
     store.register_late_pass(|_| Box::<unnecessary_semicolon::UnnecessarySemicolon>::default());
+    store.register_late_pass(move |_| Box::new(non_std_lazy_statics::NonStdLazyStatic::new(conf)));
     // add lints here, do not remove this comment, it's used in `new_lint`
 }
diff --git a/clippy_lints/src/non_std_lazy_statics.rs b/clippy_lints/src/non_std_lazy_statics.rs
new file mode 100644
index 00000000000..87466d6b61b
--- /dev/null
+++ b/clippy_lints/src/non_std_lazy_statics.rs
@@ -0,0 +1,306 @@
+use clippy_config::Conf;
+use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
+use clippy_utils::msrvs::Msrv;
+use clippy_utils::visitors::for_each_expr;
+use clippy_utils::{def_path_def_ids, fn_def_id, path_def_id};
+use rustc_data_structures::fx::FxIndexMap;
+use rustc_errors::Applicability;
+use rustc_hir::def::{DefKind, Res};
+use rustc_hir::def_id::{CrateNum, DefId};
+use rustc_hir::{self as hir, BodyId, Expr, ExprKind, Item, ItemKind};
+use rustc_lint::{LateContext, LateLintPass, LintContext};
+use rustc_middle::lint::in_external_macro;
+use rustc_session::impl_lint_pass;
+use rustc_span::Span;
+
+declare_clippy_lint! {
+    /// ### What it does
+    /// Lints when `once_cell::sync::Lazy` or `lazy_static!` are used to define a static variable,
+    /// and suggests replacing such cases with `std::sync::LazyLock` instead.
+    ///
+    /// Note: This lint will not trigger in crate with `no_std` context, or with MSRV < 1.80.0. It
+    /// also will not trigger on `once_cell::sync::Lazy` usage in crates which use other types
+    /// from `once_cell`, such as `once_cell::race::OnceBox`.
+    ///
+    /// ### Why restrict this?
+    /// - Reduces the need for an extra dependency
+    /// - Enforce convention of using standard library types when possible
+    ///
+    /// ### Example
+    /// ```ignore
+    /// lazy_static! {
+    ///     static ref FOO: String = "foo".to_uppercase();
+    /// }
+    /// static BAR: once_cell::sync::Lazy<String> = once_cell::sync::Lazy::new(|| "BAR".to_lowercase());
+    /// ```
+    /// Use instead:
+    /// ```ignore
+    /// static FOO: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "FOO".to_lowercase());
+    /// static BAR: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "BAR".to_lowercase());
+    /// ```
+    #[clippy::version = "1.81.0"]
+    pub NON_STD_LAZY_STATICS,
+    pedantic,
+    "lazy static that could be replaced by `std::sync::LazyLock`"
+}
+
+/// A list containing functions with corresponding replacements in `LazyLock`.
+///
+/// Some functions could be replaced as well if we have replaced `Lazy` to `LazyLock`,
+/// therefore after suggesting replace the type, we need to make sure the function calls can be
+/// replaced, otherwise the suggestions cannot be applied thus the applicability should be
+/// `Unspecified` or `MaybeIncorret`.
+static FUNCTION_REPLACEMENTS: &[(&str, Option<&str>)] = &[
+    ("once_cell::sync::Lazy::force", Some("std::sync::LazyLock::force")),
+    ("once_cell::sync::Lazy::get", None), // `std::sync::LazyLock::get` is experimental
+    ("once_cell::sync::Lazy::new", Some("std::sync::LazyLock::new")),
+    // Note: `Lazy::{into_value, get_mut, force_mut}` are not in the list.
+    // Because the lint only checks for `static`s, and using these functions with statics
+    // will either be a hard error or triggers `static_mut_ref` that will be hard errors.
+    // But keep in mind that if somehow we decide to expand this lint to catch non-statics,
+    // add those functions into the list.
+];
+
+pub struct NonStdLazyStatic {
+    msrv: Msrv,
+    lazy_static_lazy_static: Vec<DefId>,
+    once_cell_crate: Vec<CrateNum>,
+    once_cell_sync_lazy: Vec<DefId>,
+    once_cell_sync_lazy_new: Vec<DefId>,
+    sugg_map: FxIndexMap<DefId, Option<String>>,
+    lazy_type_defs: FxIndexMap<DefId, LazyInfo>,
+    uses_other_once_cell_types: bool,
+}
+
+impl NonStdLazyStatic {
+    #[must_use]
+    pub fn new(conf: &'static Conf) -> Self {
+        Self {
+            msrv: conf.msrv.clone(),
+            lazy_static_lazy_static: Vec::new(),
+            once_cell_crate: Vec::new(),
+            once_cell_sync_lazy: Vec::new(),
+            once_cell_sync_lazy_new: Vec::new(),
+            sugg_map: FxIndexMap::default(),
+            lazy_type_defs: FxIndexMap::default(),
+            uses_other_once_cell_types: false,
+        }
+    }
+}
+
+impl_lint_pass!(NonStdLazyStatic => [NON_STD_LAZY_STATICS]);
+
+/// Return if current MSRV does not meet the requirement for `lazy_cell` feature,
+/// or current context has `no_std` attribute.
+macro_rules! ensure_prerequisite {
+    ($msrv:expr, $cx:ident) => {
+        if !$msrv.meets(clippy_utils::msrvs::LAZY_CELL) || clippy_utils::is_no_std_crate($cx) {
+            return;
+        }
+    };
+}
+
+impl<'hir> LateLintPass<'hir> for NonStdLazyStatic {
+    extract_msrv_attr!(LateContext);
+
+    fn check_crate(&mut self, cx: &LateContext<'hir>) {
+        // Do not lint if current crate does not support `LazyLock`.
+        ensure_prerequisite!(self.msrv, cx);
+
+        // Fetch def_ids for external paths
+        self.lazy_static_lazy_static = def_path_def_ids(cx.tcx, &["lazy_static", "lazy_static"]).collect();
+        self.once_cell_sync_lazy = def_path_def_ids(cx.tcx, &["once_cell", "sync", "Lazy"]).collect();
+        self.once_cell_sync_lazy_new = def_path_def_ids(cx.tcx, &["once_cell", "sync", "Lazy", "new"]).collect();
+        // And CrateNums for `once_cell` crate
+        self.once_cell_crate = self.once_cell_sync_lazy.iter().map(|d| d.krate).collect();
+
+        // Convert hardcoded fn replacement list into a map with def_id
+        for (path, sugg) in FUNCTION_REPLACEMENTS {
+            let path_vec: Vec<&str> = path.split("::").collect();
+            for did in def_path_def_ids(cx.tcx, &path_vec) {
+                self.sugg_map.insert(did, sugg.map(ToOwned::to_owned));
+            }
+        }
+    }
+
+    fn check_item(&mut self, cx: &LateContext<'hir>, item: &Item<'hir>) {
+        ensure_prerequisite!(self.msrv, cx);
+
+        if let ItemKind::Static(..) = item.kind
+            && let Some(macro_call) = clippy_utils::macros::root_macro_call(item.span)
+            && self.lazy_static_lazy_static.contains(&macro_call.def_id)
+        {
+            span_lint(
+                cx,
+                NON_STD_LAZY_STATICS,
+                macro_call.span,
+                "this macro has been superceded by `std::sync::LazyLock`",
+            );
+            return;
+        }
+
+        if in_external_macro(cx.sess(), item.span) {
+            return;
+        }
+
+        if let Some(lazy_info) = LazyInfo::from_item(self, cx, item) {
+            self.lazy_type_defs.insert(item.owner_id.to_def_id(), lazy_info);
+        }
+    }
+
+    fn check_expr(&mut self, cx: &LateContext<'hir>, expr: &Expr<'hir>) {
+        ensure_prerequisite!(self.msrv, cx);
+
+        // All functions in the `FUNCTION_REPLACEMENTS` have only one args
+        if let ExprKind::Call(callee, [arg]) = expr.kind
+            && let Some(call_def_id) = fn_def_id(cx, expr)
+            && self.sugg_map.contains_key(&call_def_id)
+            && let ExprKind::Path(qpath) = arg.peel_borrows().kind
+            && let Some(arg_def_id) = cx.typeck_results().qpath_res(&qpath, arg.hir_id).opt_def_id()
+            && let Some(lazy_info) = self.lazy_type_defs.get_mut(&arg_def_id)
+        {
+            lazy_info.calls_span_and_id.insert(callee.span, call_def_id);
+        }
+    }
+
+    fn check_ty(&mut self, cx: &LateContext<'hir>, ty: &'hir rustc_hir::Ty<'hir>) {
+        ensure_prerequisite!(self.msrv, cx);
+
+        // Record if types from `once_cell` besides `sync::Lazy` are used.
+        if let rustc_hir::TyKind::Path(qpath) = ty.peel_refs().kind
+            && let Some(ty_def_id) = cx.qpath_res(&qpath, ty.hir_id).opt_def_id()
+            // Is from `once_cell` crate
+            && self.once_cell_crate.contains(&ty_def_id.krate)
+            // And is NOT `once_cell::sync::Lazy`
+            && !self.once_cell_sync_lazy.contains(&ty_def_id)
+        {
+            self.uses_other_once_cell_types = true;
+        }
+    }
+
+    fn check_crate_post(&mut self, cx: &LateContext<'hir>) {
+        ensure_prerequisite!(self.msrv, cx);
+
+        if !self.uses_other_once_cell_types {
+            for (_, lazy_info) in &self.lazy_type_defs {
+                lazy_info.lint(cx, &self.sugg_map);
+            }
+        }
+    }
+}
+
+struct LazyInfo {
+    /// Span of the [`hir::Ty`] without including args.
+    /// i.e.:
+    /// ```ignore
+    /// static FOO: Lazy<String> = Lazy::new(...);
+    /// //          ^^^^
+    /// ```
+    ty_span_no_args: Span,
+    /// `Span` and `DefId` of calls on `Lazy` type.
+    /// i.e.:
+    /// ```ignore
+    /// static FOO: Lazy<String> = Lazy::new(...);
+    /// //                         ^^^^^^^^^
+    /// ```
+    calls_span_and_id: FxIndexMap<Span, DefId>,
+}
+
+impl LazyInfo {
+    fn from_item(state: &NonStdLazyStatic, cx: &LateContext<'_>, item: &Item<'_>) -> Option<Self> {
+        // Check if item is a `once_cell:sync::Lazy` static.
+        if let ItemKind::Static(ty, _, body_id) = item.kind
+            && let Some(path_def_id) = path_def_id(cx, ty)
+            && let hir::TyKind::Path(hir::QPath::Resolved(_, path)) = ty.kind
+            && state.once_cell_sync_lazy.contains(&path_def_id)
+        {
+            let ty_span_no_args = path_span_without_args(path);
+            let body = cx.tcx.hir().body(body_id);
+
+            // visit body to collect `Lazy::new` calls
+            let mut new_fn_calls = FxIndexMap::default();
+            for_each_expr::<(), ()>(cx, body, |ex| {
+                if let Some((fn_did, call_span)) = fn_def_id_and_span_from_body(cx, ex, body_id)
+                    && state.once_cell_sync_lazy_new.contains(&fn_did)
+                {
+                    new_fn_calls.insert(call_span, fn_did);
+                }
+                std::ops::ControlFlow::Continue(())
+            });
+
+            Some(LazyInfo {
+                ty_span_no_args,
+                calls_span_and_id: new_fn_calls,
+            })
+        } else {
+            None
+        }
+    }
+
+    fn lint(&self, cx: &LateContext<'_>, sugg_map: &FxIndexMap<DefId, Option<String>>) {
+        // Applicability might get adjusted to `Unspecified` later if any calls
+        // in `calls_span_and_id` are not replaceable judging by the `sugg_map`.
+        let mut appl = Applicability::MachineApplicable;
+        let mut suggs = vec![(self.ty_span_no_args, "std::sync::LazyLock".to_string())];
+
+        for (span, def_id) in &self.calls_span_and_id {
+            let maybe_sugg = sugg_map.get(def_id).cloned().flatten();
+            if let Some(sugg) = maybe_sugg {
+                suggs.push((*span, sugg));
+            } else {
+                // If NO suggested replacement, not machine applicable
+                appl = Applicability::Unspecified;
+            }
+        }
+
+        span_lint_and_then(
+            cx,
+            NON_STD_LAZY_STATICS,
+            self.ty_span_no_args,
+            "this type has been superceded by `LazyLock` in the standard library",
+            |diag| {
+                diag.multipart_suggestion("use `std::sync::LazyLock` instead", suggs, appl);
+            },
+        );
+    }
+}
+
+/// Return the span of a given `Path` without including any of its args.
+///
+/// NB: Re-write of a private function `rustc_lint::non_local_def::path_span_without_args`.
+fn path_span_without_args(path: &hir::Path<'_>) -> Span {
+    path.segments
+        .last()
+        .and_then(|seg| seg.args)
+        .map_or(path.span, |args| path.span.until(args.span_ext))
+}
+
+/// Returns the `DefId` and `Span` of the callee if the given expression is a function call.
+///
+/// NB: Modified from [`clippy_utils::fn_def_id`], to support calling in an static `Item`'s body.
+fn fn_def_id_and_span_from_body(cx: &LateContext<'_>, expr: &Expr<'_>, body_id: BodyId) -> Option<(DefId, Span)> {
+    // FIXME: find a way to cache the result.
+    let typeck = cx.tcx.typeck_body(body_id);
+    match &expr.kind {
+        ExprKind::Call(
+            Expr {
+                kind: ExprKind::Path(qpath),
+                hir_id: path_hir_id,
+                span,
+                ..
+            },
+            ..,
+        ) => {
+            // Only return Fn-like DefIds, not the DefIds of statics/consts/etc that contain or
+            // deref to fn pointers, dyn Fn, impl Fn - #8850
+            if let Res::Def(DefKind::Fn | DefKind::Ctor(..) | DefKind::AssocFn, id) =
+                typeck.qpath_res(qpath, *path_hir_id)
+            {
+                Some((id, *span))
+            } else {
+                None
+            }
+        },
+        _ => None,
+    }
+}
diff --git a/clippy_utils/src/msrvs.rs b/clippy_utils/src/msrvs.rs
index d9ef817306c..d73cb7e3561 100644
--- a/clippy_utils/src/msrvs.rs
+++ b/clippy_utils/src/msrvs.rs
@@ -21,7 +21,7 @@ msrv_aliases! {
     1,83,0 { CONST_EXTERN_FN, CONST_FLOAT_BITS_CONV, CONST_FLOAT_CLASSIFY, CONST_MUT_REFS, CONST_UNWRAP }
     1,82,0 { IS_NONE_OR, REPEAT_N, RAW_REF_OP }
     1,81,0 { LINT_REASONS_STABILIZATION, ERROR_IN_CORE, EXPLICIT_SELF_TYPE_ELISION }
-    1,80,0 { BOX_INTO_ITER }
+    1,80,0 { BOX_INTO_ITER, LAZY_CELL }
     1,77,0 { C_STR_LITERALS }
     1,76,0 { PTR_FROM_REF, OPTION_RESULT_INSPECT }
     1,74,0 { REPR_RUST }
diff --git a/tests/ui/non_std_lazy_static/auxiliary/lazy_static.rs b/tests/ui/non_std_lazy_static/auxiliary/lazy_static.rs
new file mode 100644
index 00000000000..85fb4e66079
--- /dev/null
+++ b/tests/ui/non_std_lazy_static/auxiliary/lazy_static.rs
@@ -0,0 +1,20 @@
+//! **FAKE** lazy_static crate.
+
+#[macro_export]
+macro_rules! lazy_static {
+    (static ref $N:ident : $T:ty = $e:expr; $($t:tt)*) => {
+        static $N : &::core::marker::PhantomData<$T> = &::core::marker::PhantomData;
+
+        $crate::lazy_static! { $($t)* }
+    };
+    () => ()
+}
+
+#[macro_export]
+macro_rules! external {
+    () => {
+        $crate::lazy_static! {
+            static ref LZ_DERP: u32 = 12;
+        }
+    };
+}
diff --git a/tests/ui/non_std_lazy_static/auxiliary/once_cell.rs b/tests/ui/non_std_lazy_static/auxiliary/once_cell.rs
new file mode 100644
index 00000000000..e860a0f7572
--- /dev/null
+++ b/tests/ui/non_std_lazy_static/auxiliary/once_cell.rs
@@ -0,0 +1,55 @@
+//! **FAKE** once_cell crate.
+
+pub mod sync {
+    use std::marker::PhantomData;
+
+    pub struct Lazy<T, F = fn() -> T> {
+        cell: PhantomData<T>,
+        init: F,
+    }
+    unsafe impl<T, F: Send> Sync for Lazy<T, F> {}
+    impl<T, F> Lazy<T, F> {
+        pub const fn new(f: F) -> Lazy<T, F> {
+            Lazy {
+                cell: PhantomData,
+                init: f,
+            }
+        }
+
+        pub fn into_value(this: Lazy<T, F>) -> Result<T, F> {
+            unimplemented!()
+        }
+
+        pub fn force(_this: &Lazy<T, F>) -> &T {
+            unimplemented!()
+        }
+
+        pub fn force_mut(_this: &mut Lazy<T, F>) -> &mut T {
+            unimplemented!()
+        }
+
+        pub fn get(_this: &Lazy<T, F>) -> Option<&T> {
+            unimplemented!()
+        }
+
+        pub fn get_mut(_this: &mut Lazy<T, F>) -> Option<&mut T> {
+            unimplemented!()
+        }
+    }
+}
+pub mod race {
+    pub struct OnceBox<T>(T);
+
+    impl<T> OnceBox<T> {
+        pub fn get(&self) -> Option<&T> {
+            Some(&self.0)
+        }
+    }
+}
+
+#[macro_export]
+macro_rules! external {
+    () => {
+        static OC_DERP: $crate::sync::Lazy<u32> = $crate::sync::Lazy::new(|| 12);
+    };
+}
diff --git a/tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.fixed b/tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.fixed
new file mode 100644
index 00000000000..f7c56b6fffe
--- /dev/null
+++ b/tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.fixed
@@ -0,0 +1,72 @@
+//@aux-build:once_cell.rs
+//@aux-build:lazy_static.rs
+
+#![warn(clippy::non_std_lazy_statics)]
+#![allow(static_mut_refs)]
+
+use once_cell::sync::Lazy;
+
+fn main() {}
+
+static LAZY_FOO: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "foo".to_uppercase());
+//~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+static LAZY_BAR: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
+    //~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+    let x = "bar";
+    x.to_uppercase()
+});
+static LAZY_BAZ: std::sync::LazyLock<String> = { std::sync::LazyLock::new(|| "baz".to_uppercase()) };
+//~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+static LAZY_QUX: std::sync::LazyLock<String> = {
+    //~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+    if "qux".len() == 3 {
+        std::sync::LazyLock::new(|| "qux".to_uppercase())
+    } else if "qux".is_ascii() {
+        std::sync::LazyLock::new(|| "qux".to_lowercase())
+    } else {
+        std::sync::LazyLock::new(|| "qux".to_string())
+    }
+};
+
+fn non_static() {
+    let _: Lazy<i32> = Lazy::new(|| 1);
+    let _: Lazy<String> = Lazy::new(|| String::from("hello"));
+    #[allow(clippy::declare_interior_mutable_const)]
+    const DONT_DO_THIS: Lazy<i32> = Lazy::new(|| 1);
+}
+
+mod once_cell_lazy_with_fns {
+    use once_cell::sync::Lazy;
+
+    static LAZY_FOO: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "foo".to_uppercase());
+    //~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+    static LAZY_BAR: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "bar".to_uppercase());
+    //~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+    static mut LAZY_BAZ: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "baz".to_uppercase());
+    //~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+
+    fn calling_replaceable_fns() {
+        let _ = std::sync::LazyLock::force(&LAZY_FOO);
+        let _ = std::sync::LazyLock::force(&LAZY_BAR);
+        unsafe {
+            let _ = std::sync::LazyLock::force(&LAZY_BAZ);
+        }
+    }
+}
+
+#[clippy::msrv = "1.79"]
+mod msrv_not_meet {
+    use lazy_static::lazy_static;
+    use once_cell::sync::Lazy;
+
+    static LAZY_FOO: Lazy<String> = Lazy::new(|| "foo".to_uppercase());
+
+    lazy_static! {
+        static ref LAZY_BAZ: f64 = 12.159 * 548;
+    }
+}
+
+mod external_macros {
+    once_cell::external!();
+    lazy_static::external!();
+}
diff --git a/tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.rs b/tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.rs
new file mode 100644
index 00000000000..90bc428137c
--- /dev/null
+++ b/tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.rs
@@ -0,0 +1,72 @@
+//@aux-build:once_cell.rs
+//@aux-build:lazy_static.rs
+
+#![warn(clippy::non_std_lazy_statics)]
+#![allow(static_mut_refs)]
+
+use once_cell::sync::Lazy;
+
+fn main() {}
+
+static LAZY_FOO: Lazy<String> = Lazy::new(|| "foo".to_uppercase());
+//~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+static LAZY_BAR: Lazy<String> = Lazy::new(|| {
+    //~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+    let x = "bar";
+    x.to_uppercase()
+});
+static LAZY_BAZ: Lazy<String> = { Lazy::new(|| "baz".to_uppercase()) };
+//~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+static LAZY_QUX: Lazy<String> = {
+    //~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+    if "qux".len() == 3 {
+        Lazy::new(|| "qux".to_uppercase())
+    } else if "qux".is_ascii() {
+        Lazy::new(|| "qux".to_lowercase())
+    } else {
+        Lazy::new(|| "qux".to_string())
+    }
+};
+
+fn non_static() {
+    let _: Lazy<i32> = Lazy::new(|| 1);
+    let _: Lazy<String> = Lazy::new(|| String::from("hello"));
+    #[allow(clippy::declare_interior_mutable_const)]
+    const DONT_DO_THIS: Lazy<i32> = Lazy::new(|| 1);
+}
+
+mod once_cell_lazy_with_fns {
+    use once_cell::sync::Lazy;
+
+    static LAZY_FOO: Lazy<String> = Lazy::new(|| "foo".to_uppercase());
+    //~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+    static LAZY_BAR: Lazy<String> = Lazy::new(|| "bar".to_uppercase());
+    //~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+    static mut LAZY_BAZ: Lazy<String> = Lazy::new(|| "baz".to_uppercase());
+    //~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+
+    fn calling_replaceable_fns() {
+        let _ = Lazy::force(&LAZY_FOO);
+        let _ = Lazy::force(&LAZY_BAR);
+        unsafe {
+            let _ = Lazy::force(&LAZY_BAZ);
+        }
+    }
+}
+
+#[clippy::msrv = "1.79"]
+mod msrv_not_meet {
+    use lazy_static::lazy_static;
+    use once_cell::sync::Lazy;
+
+    static LAZY_FOO: Lazy<String> = Lazy::new(|| "foo".to_uppercase());
+
+    lazy_static! {
+        static ref LAZY_BAZ: f64 = 12.159 * 548;
+    }
+}
+
+mod external_macros {
+    once_cell::external!();
+    lazy_static::external!();
+}
diff --git a/tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.stderr b/tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.stderr
new file mode 100644
index 00000000000..f956f4b8d52
--- /dev/null
+++ b/tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.stderr
@@ -0,0 +1,100 @@
+error: this type has been superceded by `LazyLock` in the standard library
+  --> tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.rs:11:18
+   |
+LL | static LAZY_FOO: Lazy<String> = Lazy::new(|| "foo".to_uppercase());
+   |                  ^^^^
+   |
+   = note: `-D clippy::non-std-lazy-statics` implied by `-D warnings`
+   = help: to override `-D warnings` add `#[allow(clippy::non_std_lazy_statics)]`
+help: use `std::sync::LazyLock` instead
+   |
+LL | static LAZY_FOO: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "foo".to_uppercase());
+   |                  ~~~~~~~~~~~~~~~~~~~           ~~~~~~~~~~~~~~~~~~~~~~~~
+
+error: this type has been superceded by `LazyLock` in the standard library
+  --> tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.rs:13:18
+   |
+LL | static LAZY_BAR: Lazy<String> = Lazy::new(|| {
+   |                  ^^^^
+   |
+help: use `std::sync::LazyLock` instead
+   |
+LL | static LAZY_BAR: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
+   |                  ~~~~~~~~~~~~~~~~~~~           ~~~~~~~~~~~~~~~~~~~~~~~~
+
+error: this type has been superceded by `LazyLock` in the standard library
+  --> tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.rs:18:18
+   |
+LL | static LAZY_BAZ: Lazy<String> = { Lazy::new(|| "baz".to_uppercase()) };
+   |                  ^^^^
+   |
+help: use `std::sync::LazyLock` instead
+   |
+LL | static LAZY_BAZ: std::sync::LazyLock<String> = { std::sync::LazyLock::new(|| "baz".to_uppercase()) };
+   |                  ~~~~~~~~~~~~~~~~~~~             ~~~~~~~~~~~~~~~~~~~~~~~~
+
+error: this type has been superceded by `LazyLock` in the standard library
+  --> tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.rs:20:18
+   |
+LL | static LAZY_QUX: Lazy<String> = {
+   |                  ^^^^
+   |
+help: use `std::sync::LazyLock` instead
+   |
+LL ~ static LAZY_QUX: std::sync::LazyLock<String> = {
+LL |
+LL |     if "qux".len() == 3 {
+LL ~         std::sync::LazyLock::new(|| "qux".to_uppercase())
+LL |     } else if "qux".is_ascii() {
+LL ~         std::sync::LazyLock::new(|| "qux".to_lowercase())
+LL |     } else {
+LL ~         std::sync::LazyLock::new(|| "qux".to_string())
+   |
+
+error: this type has been superceded by `LazyLock` in the standard library
+  --> tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.rs:41:22
+   |
+LL |     static LAZY_FOO: Lazy<String> = Lazy::new(|| "foo".to_uppercase());
+   |                      ^^^^
+   |
+help: use `std::sync::LazyLock` instead
+   |
+LL ~     static LAZY_FOO: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "foo".to_uppercase());
+LL |
+...
+LL |     fn calling_replaceable_fns() {
+LL ~         let _ = std::sync::LazyLock::force(&LAZY_FOO);
+   |
+
+error: this type has been superceded by `LazyLock` in the standard library
+  --> tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.rs:43:22
+   |
+LL |     static LAZY_BAR: Lazy<String> = Lazy::new(|| "bar".to_uppercase());
+   |                      ^^^^
+   |
+help: use `std::sync::LazyLock` instead
+   |
+LL ~     static LAZY_BAR: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "bar".to_uppercase());
+LL |
+...
+LL |         let _ = Lazy::force(&LAZY_FOO);
+LL ~         let _ = std::sync::LazyLock::force(&LAZY_BAR);
+   |
+
+error: this type has been superceded by `LazyLock` in the standard library
+  --> tests/ui/non_std_lazy_static/non_std_lazy_static_fixable.rs:45:26
+   |
+LL |     static mut LAZY_BAZ: Lazy<String> = Lazy::new(|| "baz".to_uppercase());
+   |                          ^^^^
+   |
+help: use `std::sync::LazyLock` instead
+   |
+LL ~     static mut LAZY_BAZ: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "baz".to_uppercase());
+LL |
+...
+LL |         unsafe {
+LL ~             let _ = std::sync::LazyLock::force(&LAZY_BAZ);
+   |
+
+error: aborting due to 7 previous errors
+
diff --git a/tests/ui/non_std_lazy_static/non_std_lazy_static_no_std.rs b/tests/ui/non_std_lazy_static/non_std_lazy_static_no_std.rs
new file mode 100644
index 00000000000..6208612c698
--- /dev/null
+++ b/tests/ui/non_std_lazy_static/non_std_lazy_static_no_std.rs
@@ -0,0 +1,20 @@
+//@aux-build:once_cell.rs
+//@aux-build:lazy_static.rs
+
+#![warn(clippy::non_std_lazy_statics)]
+#![no_std]
+
+use lazy_static::lazy_static;
+use once_cell::sync::Lazy;
+
+fn main() {}
+
+static LAZY_FOO: Lazy<usize> = Lazy::new(|| 42);
+static LAZY_BAR: Lazy<i32> = Lazy::new(|| {
+    let x: i32 = 0;
+    x.saturating_add(100)
+});
+
+lazy_static! {
+    static ref LAZY_BAZ: f64 = 12.159 * 548;
+}
diff --git a/tests/ui/non_std_lazy_static/non_std_lazy_static_other_once_cell.rs b/tests/ui/non_std_lazy_static/non_std_lazy_static_other_once_cell.rs
new file mode 100644
index 00000000000..8701a4b7729
--- /dev/null
+++ b/tests/ui/non_std_lazy_static/non_std_lazy_static_other_once_cell.rs
@@ -0,0 +1,12 @@
+//@aux-build:once_cell.rs
+
+#![warn(clippy::non_std_lazy_statics)]
+
+// Should not error, since we used a type besides `sync::Lazy`
+fn use_once_cell_race(x: once_cell::race::OnceBox<String>) {
+    let _foo = x.get();
+}
+
+use once_cell::sync::Lazy;
+
+static LAZY_BAZ: Lazy<String> = Lazy::new(|| "baz".to_uppercase());
diff --git a/tests/ui/non_std_lazy_static/non_std_lazy_static_unfixable.rs b/tests/ui/non_std_lazy_static/non_std_lazy_static_unfixable.rs
new file mode 100644
index 00000000000..34f8dd1ccb2
--- /dev/null
+++ b/tests/ui/non_std_lazy_static/non_std_lazy_static_unfixable.rs
@@ -0,0 +1,43 @@
+//@aux-build:once_cell.rs
+//@aux-build:lazy_static.rs
+//@no-rustfix
+
+#![warn(clippy::non_std_lazy_statics)]
+#![allow(static_mut_refs)]
+
+mod once_cell_lazy {
+    use once_cell::sync::Lazy;
+
+    static LAZY_FOO: Lazy<String> = Lazy::new(|| "foo".to_uppercase());
+    //~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+    static mut LAZY_BAR: Lazy<String> = Lazy::new(|| "bar".to_uppercase());
+    //~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+    static mut LAZY_BAZ: Lazy<String> = Lazy::new(|| "baz".to_uppercase());
+    //~^ ERROR: this type has been superceded by `LazyLock` in the standard library
+
+    fn calling_irreplaceable_fns() {
+        let _ = Lazy::get(&LAZY_FOO);
+
+        unsafe {
+            let _ = Lazy::get_mut(&mut LAZY_BAR);
+            let _ = Lazy::force_mut(&mut LAZY_BAZ);
+        }
+    }
+}
+
+mod lazy_static_lazy_static {
+    use lazy_static::lazy_static;
+
+    lazy_static! {
+        static ref LAZY_FOO: String = "foo".to_uppercase();
+    }
+    //~^^^ ERROR: this macro has been superceded by `std::sync::LazyLock`
+    lazy_static! {
+        static ref LAZY_BAR: String = "bar".to_uppercase();
+        static ref LAZY_BAZ: String = "baz".to_uppercase();
+    }
+    //~^^^^ ERROR: this macro has been superceded by `std::sync::LazyLock`
+    //~| ERROR: this macro has been superceded by `std::sync::LazyLock`
+}
+
+fn main() {}
diff --git a/tests/ui/non_std_lazy_static/non_std_lazy_static_unfixable.stderr b/tests/ui/non_std_lazy_static/non_std_lazy_static_unfixable.stderr
new file mode 100644
index 00000000000..66dc435f982
--- /dev/null
+++ b/tests/ui/non_std_lazy_static/non_std_lazy_static_unfixable.stderr
@@ -0,0 +1,66 @@
+error: this macro has been superceded by `std::sync::LazyLock`
+  --> tests/ui/non_std_lazy_static/non_std_lazy_static_unfixable.rs:31:5
+   |
+LL | /     lazy_static! {
+LL | |         static ref LAZY_FOO: String = "foo".to_uppercase();
+LL | |     }
+   | |_____^
+   |
+   = note: `-D clippy::non-std-lazy-statics` implied by `-D warnings`
+   = help: to override `-D warnings` add `#[allow(clippy::non_std_lazy_statics)]`
+
+error: this macro has been superceded by `std::sync::LazyLock`
+  --> tests/ui/non_std_lazy_static/non_std_lazy_static_unfixable.rs:35:5
+   |
+LL | /     lazy_static! {
+LL | |         static ref LAZY_BAR: String = "bar".to_uppercase();
+LL | |         static ref LAZY_BAZ: String = "baz".to_uppercase();
+LL | |     }
+   | |_____^
+
+error: this macro has been superceded by `std::sync::LazyLock`
+  --> tests/ui/non_std_lazy_static/non_std_lazy_static_unfixable.rs:35:5
+   |
+LL | /     lazy_static! {
+LL | |         static ref LAZY_BAR: String = "bar".to_uppercase();
+LL | |         static ref LAZY_BAZ: String = "baz".to_uppercase();
+LL | |     }
+   | |_____^
+   |
+   = note: duplicate diagnostic emitted due to `-Z deduplicate-diagnostics=no`
+
+error: this type has been superceded by `LazyLock` in the standard library
+  --> tests/ui/non_std_lazy_static/non_std_lazy_static_unfixable.rs:11:22
+   |
+LL |     static LAZY_FOO: Lazy<String> = Lazy::new(|| "foo".to_uppercase());
+   |                      ^^^^
+   |
+help: use `std::sync::LazyLock` instead
+   |
+LL |     static LAZY_FOO: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "foo".to_uppercase());
+   |                      ~~~~~~~~~~~~~~~~~~~           ~~~~~~~~~~~~~~~~~~~~~~~~
+
+error: this type has been superceded by `LazyLock` in the standard library
+  --> tests/ui/non_std_lazy_static/non_std_lazy_static_unfixable.rs:13:26
+   |
+LL |     static mut LAZY_BAR: Lazy<String> = Lazy::new(|| "bar".to_uppercase());
+   |                          ^^^^
+   |
+help: use `std::sync::LazyLock` instead
+   |
+LL |     static mut LAZY_BAR: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "bar".to_uppercase());
+   |                          ~~~~~~~~~~~~~~~~~~~           ~~~~~~~~~~~~~~~~~~~~~~~~
+
+error: this type has been superceded by `LazyLock` in the standard library
+  --> tests/ui/non_std_lazy_static/non_std_lazy_static_unfixable.rs:15:26
+   |
+LL |     static mut LAZY_BAZ: Lazy<String> = Lazy::new(|| "baz".to_uppercase());
+   |                          ^^^^
+   |
+help: use `std::sync::LazyLock` instead
+   |
+LL |     static mut LAZY_BAZ: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "baz".to_uppercase());
+   |                          ~~~~~~~~~~~~~~~~~~~           ~~~~~~~~~~~~~~~~~~~~~~~~
+
+error: aborting due to 6 previous errors
+