Skip to content

Commit 4825666

Browse files
authored
add new lint non_std_lazy_statics (#13770)
fixes #12895 detects usage of `once_cell::sync::Lazy` and `lazy_static!`, recommending usage of `std::sync::LazyLock` instead Many thanks to @J-ZhengLi for putting in most of the work in #12968 ---- changelog: new lint [`non_std_lazy_statics`]
2 parents d7e20a9 + 2671e4a commit 4825666

16 files changed

+773
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5906,6 +5906,7 @@ Released 2018-09-13
59065906
[`non_minimal_cfg`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_minimal_cfg
59075907
[`non_octal_unix_permissions`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_octal_unix_permissions
59085908
[`non_send_fields_in_send_ty`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_send_fields_in_send_ty
5909+
[`non_std_lazy_statics`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_std_lazy_statics
59095910
[`non_zero_suggestions`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_zero_suggestions
59105911
[`nonminimal_bool`]: https://rust-lang.github.io/rust-clippy/master/index.html#nonminimal_bool
59115912
[`nonsensical_open_options`]: https://rust-lang.github.io/rust-clippy/master/index.html#nonsensical_open_options

book/src/lint_configuration.md

+1
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,7 @@ The minimum rust version that the project supports. Defaults to the `rust-versio
762762
* [`mem_replace_with_default`](https://rust-lang.github.io/rust-clippy/master/index.html#mem_replace_with_default)
763763
* [`missing_const_for_fn`](https://rust-lang.github.io/rust-clippy/master/index.html#missing_const_for_fn)
764764
* [`needless_borrow`](https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow)
765+
* [`non_std_lazy_statics`](https://rust-lang.github.io/rust-clippy/master/index.html#non_std_lazy_statics)
765766
* [`option_as_ref_deref`](https://rust-lang.github.io/rust-clippy/master/index.html#option_as_ref_deref)
766767
* [`option_map_unwrap_or`](https://rust-lang.github.io/rust-clippy/master/index.html#option_map_unwrap_or)
767768
* [`ptr_as_ptr`](https://rust-lang.github.io/rust-clippy/master/index.html#ptr_as_ptr)

clippy_config/src/conf.rs

+1
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,7 @@ define_Conf! {
631631
mem_replace_with_default,
632632
missing_const_for_fn,
633633
needless_borrow,
634+
non_std_lazy_statics,
634635
option_as_ref_deref,
635636
option_map_unwrap_or,
636637
ptr_as_ptr,

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
576576
crate::non_expressive_names::SIMILAR_NAMES_INFO,
577577
crate::non_octal_unix_permissions::NON_OCTAL_UNIX_PERMISSIONS_INFO,
578578
crate::non_send_fields_in_send_ty::NON_SEND_FIELDS_IN_SEND_TY_INFO,
579+
crate::non_std_lazy_statics::NON_STD_LAZY_STATICS_INFO,
579580
crate::non_zero_suggestions::NON_ZERO_SUGGESTIONS_INFO,
580581
crate::nonstandard_macro_braces::NONSTANDARD_MACRO_BRACES_INFO,
581582
crate::octal_escapes::OCTAL_ESCAPES_INFO,

clippy_lints/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ mod non_copy_const;
279279
mod non_expressive_names;
280280
mod non_octal_unix_permissions;
281281
mod non_send_fields_in_send_ty;
282+
mod non_std_lazy_statics;
282283
mod non_zero_suggestions;
283284
mod nonstandard_macro_braces;
284285
mod octal_escapes;
@@ -974,5 +975,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
974975
store.register_late_pass(move |_| Box::new(arbitrary_source_item_ordering::ArbitrarySourceItemOrdering::new(conf)));
975976
store.register_late_pass(|_| Box::new(unneeded_struct_pattern::UnneededStructPattern));
976977
store.register_late_pass(|_| Box::<unnecessary_semicolon::UnnecessarySemicolon>::default());
978+
store.register_late_pass(move |_| Box::new(non_std_lazy_statics::NonStdLazyStatic::new(conf)));
977979
// add lints here, do not remove this comment, it's used in `new_lint`
978980
}
+306
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
use clippy_config::Conf;
2+
use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
3+
use clippy_utils::msrvs::Msrv;
4+
use clippy_utils::visitors::for_each_expr;
5+
use clippy_utils::{def_path_def_ids, fn_def_id, path_def_id};
6+
use rustc_data_structures::fx::FxIndexMap;
7+
use rustc_errors::Applicability;
8+
use rustc_hir::def::{DefKind, Res};
9+
use rustc_hir::def_id::{CrateNum, DefId};
10+
use rustc_hir::{self as hir, BodyId, Expr, ExprKind, Item, ItemKind};
11+
use rustc_lint::{LateContext, LateLintPass, LintContext};
12+
use rustc_middle::lint::in_external_macro;
13+
use rustc_session::impl_lint_pass;
14+
use rustc_span::Span;
15+
16+
declare_clippy_lint! {
17+
/// ### What it does
18+
/// Lints when `once_cell::sync::Lazy` or `lazy_static!` are used to define a static variable,
19+
/// and suggests replacing such cases with `std::sync::LazyLock` instead.
20+
///
21+
/// Note: This lint will not trigger in crate with `no_std` context, or with MSRV < 1.80.0. It
22+
/// also will not trigger on `once_cell::sync::Lazy` usage in crates which use other types
23+
/// from `once_cell`, such as `once_cell::race::OnceBox`.
24+
///
25+
/// ### Why restrict this?
26+
/// - Reduces the need for an extra dependency
27+
/// - Enforce convention of using standard library types when possible
28+
///
29+
/// ### Example
30+
/// ```ignore
31+
/// lazy_static! {
32+
/// static ref FOO: String = "foo".to_uppercase();
33+
/// }
34+
/// static BAR: once_cell::sync::Lazy<String> = once_cell::sync::Lazy::new(|| "BAR".to_lowercase());
35+
/// ```
36+
/// Use instead:
37+
/// ```ignore
38+
/// static FOO: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "FOO".to_lowercase());
39+
/// static BAR: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| "BAR".to_lowercase());
40+
/// ```
41+
#[clippy::version = "1.81.0"]
42+
pub NON_STD_LAZY_STATICS,
43+
pedantic,
44+
"lazy static that could be replaced by `std::sync::LazyLock`"
45+
}
46+
47+
/// A list containing functions with corresponding replacements in `LazyLock`.
48+
///
49+
/// Some functions could be replaced as well if we have replaced `Lazy` to `LazyLock`,
50+
/// therefore after suggesting replace the type, we need to make sure the function calls can be
51+
/// replaced, otherwise the suggestions cannot be applied thus the applicability should be
52+
/// `Unspecified` or `MaybeIncorret`.
53+
static FUNCTION_REPLACEMENTS: &[(&str, Option<&str>)] = &[
54+
("once_cell::sync::Lazy::force", Some("std::sync::LazyLock::force")),
55+
("once_cell::sync::Lazy::get", None), // `std::sync::LazyLock::get` is experimental
56+
("once_cell::sync::Lazy::new", Some("std::sync::LazyLock::new")),
57+
// Note: `Lazy::{into_value, get_mut, force_mut}` are not in the list.
58+
// Because the lint only checks for `static`s, and using these functions with statics
59+
// will either be a hard error or triggers `static_mut_ref` that will be hard errors.
60+
// But keep in mind that if somehow we decide to expand this lint to catch non-statics,
61+
// add those functions into the list.
62+
];
63+
64+
pub struct NonStdLazyStatic {
65+
msrv: Msrv,
66+
lazy_static_lazy_static: Vec<DefId>,
67+
once_cell_crate: Vec<CrateNum>,
68+
once_cell_sync_lazy: Vec<DefId>,
69+
once_cell_sync_lazy_new: Vec<DefId>,
70+
sugg_map: FxIndexMap<DefId, Option<String>>,
71+
lazy_type_defs: FxIndexMap<DefId, LazyInfo>,
72+
uses_other_once_cell_types: bool,
73+
}
74+
75+
impl NonStdLazyStatic {
76+
#[must_use]
77+
pub fn new(conf: &'static Conf) -> Self {
78+
Self {
79+
msrv: conf.msrv.clone(),
80+
lazy_static_lazy_static: Vec::new(),
81+
once_cell_crate: Vec::new(),
82+
once_cell_sync_lazy: Vec::new(),
83+
once_cell_sync_lazy_new: Vec::new(),
84+
sugg_map: FxIndexMap::default(),
85+
lazy_type_defs: FxIndexMap::default(),
86+
uses_other_once_cell_types: false,
87+
}
88+
}
89+
}
90+
91+
impl_lint_pass!(NonStdLazyStatic => [NON_STD_LAZY_STATICS]);
92+
93+
/// Return if current MSRV does not meet the requirement for `lazy_cell` feature,
94+
/// or current context has `no_std` attribute.
95+
macro_rules! ensure_prerequisite {
96+
($msrv:expr, $cx:ident) => {
97+
if !$msrv.meets(clippy_utils::msrvs::LAZY_CELL) || clippy_utils::is_no_std_crate($cx) {
98+
return;
99+
}
100+
};
101+
}
102+
103+
impl<'hir> LateLintPass<'hir> for NonStdLazyStatic {
104+
extract_msrv_attr!(LateContext);
105+
106+
fn check_crate(&mut self, cx: &LateContext<'hir>) {
107+
// Do not lint if current crate does not support `LazyLock`.
108+
ensure_prerequisite!(self.msrv, cx);
109+
110+
// Fetch def_ids for external paths
111+
self.lazy_static_lazy_static = def_path_def_ids(cx.tcx, &["lazy_static", "lazy_static"]).collect();
112+
self.once_cell_sync_lazy = def_path_def_ids(cx.tcx, &["once_cell", "sync", "Lazy"]).collect();
113+
self.once_cell_sync_lazy_new = def_path_def_ids(cx.tcx, &["once_cell", "sync", "Lazy", "new"]).collect();
114+
// And CrateNums for `once_cell` crate
115+
self.once_cell_crate = self.once_cell_sync_lazy.iter().map(|d| d.krate).collect();
116+
117+
// Convert hardcoded fn replacement list into a map with def_id
118+
for (path, sugg) in FUNCTION_REPLACEMENTS {
119+
let path_vec: Vec<&str> = path.split("::").collect();
120+
for did in def_path_def_ids(cx.tcx, &path_vec) {
121+
self.sugg_map.insert(did, sugg.map(ToOwned::to_owned));
122+
}
123+
}
124+
}
125+
126+
fn check_item(&mut self, cx: &LateContext<'hir>, item: &Item<'hir>) {
127+
ensure_prerequisite!(self.msrv, cx);
128+
129+
if let ItemKind::Static(..) = item.kind
130+
&& let Some(macro_call) = clippy_utils::macros::root_macro_call(item.span)
131+
&& self.lazy_static_lazy_static.contains(&macro_call.def_id)
132+
{
133+
span_lint(
134+
cx,
135+
NON_STD_LAZY_STATICS,
136+
macro_call.span,
137+
"this macro has been superceded by `std::sync::LazyLock`",
138+
);
139+
return;
140+
}
141+
142+
if in_external_macro(cx.sess(), item.span) {
143+
return;
144+
}
145+
146+
if let Some(lazy_info) = LazyInfo::from_item(self, cx, item) {
147+
self.lazy_type_defs.insert(item.owner_id.to_def_id(), lazy_info);
148+
}
149+
}
150+
151+
fn check_expr(&mut self, cx: &LateContext<'hir>, expr: &Expr<'hir>) {
152+
ensure_prerequisite!(self.msrv, cx);
153+
154+
// All functions in the `FUNCTION_REPLACEMENTS` have only one args
155+
if let ExprKind::Call(callee, [arg]) = expr.kind
156+
&& let Some(call_def_id) = fn_def_id(cx, expr)
157+
&& self.sugg_map.contains_key(&call_def_id)
158+
&& let ExprKind::Path(qpath) = arg.peel_borrows().kind
159+
&& let Some(arg_def_id) = cx.typeck_results().qpath_res(&qpath, arg.hir_id).opt_def_id()
160+
&& let Some(lazy_info) = self.lazy_type_defs.get_mut(&arg_def_id)
161+
{
162+
lazy_info.calls_span_and_id.insert(callee.span, call_def_id);
163+
}
164+
}
165+
166+
fn check_ty(&mut self, cx: &LateContext<'hir>, ty: &'hir rustc_hir::Ty<'hir>) {
167+
ensure_prerequisite!(self.msrv, cx);
168+
169+
// Record if types from `once_cell` besides `sync::Lazy` are used.
170+
if let rustc_hir::TyKind::Path(qpath) = ty.peel_refs().kind
171+
&& let Some(ty_def_id) = cx.qpath_res(&qpath, ty.hir_id).opt_def_id()
172+
// Is from `once_cell` crate
173+
&& self.once_cell_crate.contains(&ty_def_id.krate)
174+
// And is NOT `once_cell::sync::Lazy`
175+
&& !self.once_cell_sync_lazy.contains(&ty_def_id)
176+
{
177+
self.uses_other_once_cell_types = true;
178+
}
179+
}
180+
181+
fn check_crate_post(&mut self, cx: &LateContext<'hir>) {
182+
ensure_prerequisite!(self.msrv, cx);
183+
184+
if !self.uses_other_once_cell_types {
185+
for (_, lazy_info) in &self.lazy_type_defs {
186+
lazy_info.lint(cx, &self.sugg_map);
187+
}
188+
}
189+
}
190+
}
191+
192+
struct LazyInfo {
193+
/// Span of the [`hir::Ty`] without including args.
194+
/// i.e.:
195+
/// ```ignore
196+
/// static FOO: Lazy<String> = Lazy::new(...);
197+
/// // ^^^^
198+
/// ```
199+
ty_span_no_args: Span,
200+
/// `Span` and `DefId` of calls on `Lazy` type.
201+
/// i.e.:
202+
/// ```ignore
203+
/// static FOO: Lazy<String> = Lazy::new(...);
204+
/// // ^^^^^^^^^
205+
/// ```
206+
calls_span_and_id: FxIndexMap<Span, DefId>,
207+
}
208+
209+
impl LazyInfo {
210+
fn from_item(state: &NonStdLazyStatic, cx: &LateContext<'_>, item: &Item<'_>) -> Option<Self> {
211+
// Check if item is a `once_cell:sync::Lazy` static.
212+
if let ItemKind::Static(ty, _, body_id) = item.kind
213+
&& let Some(path_def_id) = path_def_id(cx, ty)
214+
&& let hir::TyKind::Path(hir::QPath::Resolved(_, path)) = ty.kind
215+
&& state.once_cell_sync_lazy.contains(&path_def_id)
216+
{
217+
let ty_span_no_args = path_span_without_args(path);
218+
let body = cx.tcx.hir().body(body_id);
219+
220+
// visit body to collect `Lazy::new` calls
221+
let mut new_fn_calls = FxIndexMap::default();
222+
for_each_expr::<(), ()>(cx, body, |ex| {
223+
if let Some((fn_did, call_span)) = fn_def_id_and_span_from_body(cx, ex, body_id)
224+
&& state.once_cell_sync_lazy_new.contains(&fn_did)
225+
{
226+
new_fn_calls.insert(call_span, fn_did);
227+
}
228+
std::ops::ControlFlow::Continue(())
229+
});
230+
231+
Some(LazyInfo {
232+
ty_span_no_args,
233+
calls_span_and_id: new_fn_calls,
234+
})
235+
} else {
236+
None
237+
}
238+
}
239+
240+
fn lint(&self, cx: &LateContext<'_>, sugg_map: &FxIndexMap<DefId, Option<String>>) {
241+
// Applicability might get adjusted to `Unspecified` later if any calls
242+
// in `calls_span_and_id` are not replaceable judging by the `sugg_map`.
243+
let mut appl = Applicability::MachineApplicable;
244+
let mut suggs = vec![(self.ty_span_no_args, "std::sync::LazyLock".to_string())];
245+
246+
for (span, def_id) in &self.calls_span_and_id {
247+
let maybe_sugg = sugg_map.get(def_id).cloned().flatten();
248+
if let Some(sugg) = maybe_sugg {
249+
suggs.push((*span, sugg));
250+
} else {
251+
// If NO suggested replacement, not machine applicable
252+
appl = Applicability::Unspecified;
253+
}
254+
}
255+
256+
span_lint_and_then(
257+
cx,
258+
NON_STD_LAZY_STATICS,
259+
self.ty_span_no_args,
260+
"this type has been superceded by `LazyLock` in the standard library",
261+
|diag| {
262+
diag.multipart_suggestion("use `std::sync::LazyLock` instead", suggs, appl);
263+
},
264+
);
265+
}
266+
}
267+
268+
/// Return the span of a given `Path` without including any of its args.
269+
///
270+
/// NB: Re-write of a private function `rustc_lint::non_local_def::path_span_without_args`.
271+
fn path_span_without_args(path: &hir::Path<'_>) -> Span {
272+
path.segments
273+
.last()
274+
.and_then(|seg| seg.args)
275+
.map_or(path.span, |args| path.span.until(args.span_ext))
276+
}
277+
278+
/// Returns the `DefId` and `Span` of the callee if the given expression is a function call.
279+
///
280+
/// NB: Modified from [`clippy_utils::fn_def_id`], to support calling in an static `Item`'s body.
281+
fn fn_def_id_and_span_from_body(cx: &LateContext<'_>, expr: &Expr<'_>, body_id: BodyId) -> Option<(DefId, Span)> {
282+
// FIXME: find a way to cache the result.
283+
let typeck = cx.tcx.typeck_body(body_id);
284+
match &expr.kind {
285+
ExprKind::Call(
286+
Expr {
287+
kind: ExprKind::Path(qpath),
288+
hir_id: path_hir_id,
289+
span,
290+
..
291+
},
292+
..,
293+
) => {
294+
// Only return Fn-like DefIds, not the DefIds of statics/consts/etc that contain or
295+
// deref to fn pointers, dyn Fn, impl Fn - #8850
296+
if let Res::Def(DefKind::Fn | DefKind::Ctor(..) | DefKind::AssocFn, id) =
297+
typeck.qpath_res(qpath, *path_hir_id)
298+
{
299+
Some((id, *span))
300+
} else {
301+
None
302+
}
303+
},
304+
_ => None,
305+
}
306+
}

clippy_utils/src/msrvs.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ msrv_aliases! {
2121
1,83,0 { CONST_EXTERN_FN, CONST_FLOAT_BITS_CONV, CONST_FLOAT_CLASSIFY, CONST_MUT_REFS, CONST_UNWRAP }
2222
1,82,0 { IS_NONE_OR, REPEAT_N, RAW_REF_OP }
2323
1,81,0 { LINT_REASONS_STABILIZATION, ERROR_IN_CORE, EXPLICIT_SELF_TYPE_ELISION }
24-
1,80,0 { BOX_INTO_ITER }
24+
1,80,0 { BOX_INTO_ITER, LAZY_CELL }
2525
1,77,0 { C_STR_LITERALS }
2626
1,76,0 { PTR_FROM_REF, OPTION_RESULT_INSPECT }
2727
1,74,0 { REPR_RUST }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//! **FAKE** lazy_static crate.
2+
3+
#[macro_export]
4+
macro_rules! lazy_static {
5+
(static ref $N:ident : $T:ty = $e:expr; $($t:tt)*) => {
6+
static $N : &::core::marker::PhantomData<$T> = &::core::marker::PhantomData;
7+
8+
$crate::lazy_static! { $($t)* }
9+
};
10+
() => ()
11+
}
12+
13+
#[macro_export]
14+
macro_rules! external {
15+
() => {
16+
$crate::lazy_static! {
17+
static ref LZ_DERP: u32 = 12;
18+
}
19+
};
20+
}

0 commit comments

Comments
 (0)