Skip to content

Commit 587f3de

Browse files
J-ZhengLipitaj
authored andcommitted
add new lint non_std_lazy_statics
detect usage of `once_cell::sync::Lazy` and `lazy_static!`, recommending usage of `std::sync::LazyLock` instead
1 parent 650e0c8 commit 587f3de

15 files changed

+719
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5859,6 +5859,7 @@ Released 2018-09-13
58595859
[`non_minimal_cfg`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_minimal_cfg
58605860
[`non_octal_unix_permissions`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_octal_unix_permissions
58615861
[`non_send_fields_in_send_ty`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_send_fields_in_send_ty
5862+
[`non_std_lazy_statics`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_std_lazy_statics
58625863
[`non_zero_suggestions`]: https://rust-lang.github.io/rust-clippy/master/index.html#non_zero_suggestions
58635864
[`nonminimal_bool`]: https://rust-lang.github.io/rust-clippy/master/index.html#nonminimal_bool
58645865
[`nonsensical_open_options`]: https://rust-lang.github.io/rust-clippy/master/index.html#nonsensical_open_options

book/src/lint_configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,7 @@ The minimum rust version that the project supports. Defaults to the `rust-versio
725725
* [`mem_replace_with_default`](https://rust-lang.github.io/rust-clippy/master/index.html#mem_replace_with_default)
726726
* [`missing_const_for_fn`](https://rust-lang.github.io/rust-clippy/master/index.html#missing_const_for_fn)
727727
* [`needless_borrow`](https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow)
728+
* [`non_std_lazy_statics`](https://rust-lang.github.io/rust-clippy/master/index.html#non_std_lazy_statics)
728729
* [`option_as_ref_deref`](https://rust-lang.github.io/rust-clippy/master/index.html#option_as_ref_deref)
729730
* [`option_map_unwrap_or`](https://rust-lang.github.io/rust-clippy/master/index.html#option_map_unwrap_or)
730731
* [`ptr_as_ptr`](https://rust-lang.github.io/rust-clippy/master/index.html#ptr_as_ptr)

clippy_config/src/conf.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,7 @@ define_Conf! {
608608
mem_replace_with_default,
609609
missing_const_for_fn,
610610
needless_borrow,
611+
non_std_lazy_statics,
611612
option_as_ref_deref,
612613
option_map_unwrap_or,
613614
ptr_as_ptr,

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
568568
crate::non_expressive_names::SIMILAR_NAMES_INFO,
569569
crate::non_octal_unix_permissions::NON_OCTAL_UNIX_PERMISSIONS_INFO,
570570
crate::non_send_fields_in_send_ty::NON_SEND_FIELDS_IN_SEND_TY_INFO,
571+
crate::non_std_lazy_statics::NON_STD_LAZY_STATICS_INFO,
571572
crate::non_zero_suggestions::NON_ZERO_SUGGESTIONS_INFO,
572573
crate::nonstandard_macro_braces::NONSTANDARD_MACRO_BRACES_INFO,
573574
crate::octal_escapes::OCTAL_ESCAPES_INFO,

clippy_lints/src/lib.rs

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

clippy_utils/src/msrvs.rs

Lines changed: 1 addition & 1 deletion
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 }
2222
1,82,0 { IS_NONE_OR, REPEAT_N }
2323
1,81,0 { LINT_REASONS_STABILIZATION }
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,73,0 { MANUAL_DIV_CEIL }
Lines changed: 20 additions & 0 deletions
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+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//! **FAKE** once_cell crate.
2+
3+
pub mod sync {
4+
use std::marker::PhantomData;
5+
6+
pub struct Lazy<T, F = fn() -> T> {
7+
cell: PhantomData<T>,
8+
init: F,
9+
}
10+
unsafe impl<T, F: Send> Sync for Lazy<T, F> {}
11+
impl<T, F> Lazy<T, F> {
12+
pub const fn new(f: F) -> Lazy<T, F> {
13+
Lazy {
14+
cell: PhantomData,
15+
init: f,
16+
}
17+
}
18+
19+
pub fn into_value(this: Lazy<T, F>) -> Result<T, F> {
20+
unimplemented!()
21+
}
22+
23+
pub fn force(_this: &Lazy<T, F>) -> &T {
24+
unimplemented!()
25+
}
26+
27+
pub fn force_mut(_this: &mut Lazy<T, F>) -> &mut T {
28+
unimplemented!()
29+
}
30+
31+
pub fn get(_this: &Lazy<T, F>) -> Option<&T> {
32+
unimplemented!()
33+
}
34+
35+
pub fn get_mut(_this: &mut Lazy<T, F>) -> Option<&mut T> {
36+
unimplemented!()
37+
}
38+
}
39+
}
40+
41+
#[macro_export]
42+
macro_rules! external {
43+
() => {
44+
static OC_DERP: $crate::sync::Lazy<u32> = $crate::sync::Lazy::new(|| 12);
45+
};
46+
}

0 commit comments

Comments
 (0)