Skip to content

Commit 8fd1a3f

Browse files
committed
add new lint [once_cell_lazy] to detect usage of static Lazy type from once_cell
1 parent 3e84ca8 commit 8fd1a3f

13 files changed

+602
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5677,6 +5677,7 @@ Released 2018-09-13
56775677
[`obfuscated_if_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#obfuscated_if_else
56785678
[`octal_escapes`]: https://rust-lang.github.io/rust-clippy/master/index.html#octal_escapes
56795679
[`ok_expect`]: https://rust-lang.github.io/rust-clippy/master/index.html#ok_expect
5680+
[`once_cell_lazy`]: https://rust-lang.github.io/rust-clippy/master/index.html#once_cell_lazy
56805681
[`only_used_in_recursion`]: https://rust-lang.github.io/rust-clippy/master/index.html#only_used_in_recursion
56815682
[`op_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#op_ref
56825683
[`option_and_then_some`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_and_then_some

clippy_config/src/conf.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ define_Conf! {
265265
///
266266
/// Suppress lints whenever the suggested change would cause breakage for other crates.
267267
(avoid_breaking_exported_api: bool = true),
268-
/// Lint: MANUAL_SPLIT_ONCE, MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, OPTION_MAP_UNWRAP_OR, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE, APPROX_CONSTANT, DEPRECATED_CFG_ATTR, INDEX_REFUTABLE_SLICE, MAP_CLONE, BORROW_AS_PTR, MANUAL_BITS, ERR_EXPECT, CAST_ABS_TO_UNSIGNED, UNINLINED_FORMAT_ARGS, MANUAL_CLAMP, MANUAL_LET_ELSE, UNCHECKED_DURATION_SUBTRACTION, COLLAPSIBLE_STR_REPLACE, SEEK_FROM_CURRENT, SEEK_REWIND, UNNECESSARY_LAZY_EVALUATIONS, TRANSMUTE_PTR_TO_REF, ALMOST_COMPLETE_RANGE, NEEDLESS_BORROW, DERIVABLE_IMPLS, MANUAL_IS_ASCII_CHECK, MANUAL_REM_EUCLID, MANUAL_RETAIN, TYPE_REPETITION_IN_BOUNDS, TUPLE_ARRAY_CONVERSIONS, MANUAL_TRY_FOLD, MANUAL_HASH_ONE, ITER_KV_MAP, MANUAL_C_STR_LITERALS, ASSIGNING_CLONES, LEGACY_NUMERIC_CONSTANTS, MANUAL_PATTERN_CHAR_COMPARISON.
268+
/// Lint: MANUAL_SPLIT_ONCE, MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, OPTION_MAP_UNWRAP_OR, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE, APPROX_CONSTANT, DEPRECATED_CFG_ATTR, INDEX_REFUTABLE_SLICE, MAP_CLONE, BORROW_AS_PTR, MANUAL_BITS, ERR_EXPECT, CAST_ABS_TO_UNSIGNED, UNINLINED_FORMAT_ARGS, MANUAL_CLAMP, MANUAL_LET_ELSE, UNCHECKED_DURATION_SUBTRACTION, COLLAPSIBLE_STR_REPLACE, SEEK_FROM_CURRENT, SEEK_REWIND, UNNECESSARY_LAZY_EVALUATIONS, TRANSMUTE_PTR_TO_REF, ALMOST_COMPLETE_RANGE, NEEDLESS_BORROW, DERIVABLE_IMPLS, MANUAL_IS_ASCII_CHECK, MANUAL_REM_EUCLID, MANUAL_RETAIN, TYPE_REPETITION_IN_BOUNDS, TUPLE_ARRAY_CONVERSIONS, MANUAL_TRY_FOLD, MANUAL_HASH_ONE, ITER_KV_MAP, MANUAL_C_STR_LITERALS, ASSIGNING_CLONES, LEGACY_NUMERIC_CONSTANTS, MANUAL_PATTERN_CHAR_COMPARISON, ONCE_CELL_LAZY.
269269
///
270270
/// The minimum rust version that the project supports. Defaults to the `rust-version` field in `Cargo.toml`
271271
#[default_text = ""]

clippy_config/src/msrvs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ macro_rules! msrv_aliases {
1717

1818
// names may refer to stabilized feature flags or library items
1919
msrv_aliases! {
20+
1,80,0 { LAZY_CELL }
2021
1,77,0 { C_STR_LITERALS }
2122
1,76,0 { PTR_FROM_REF, OPTION_RESULT_INSPECT }
2223
1,71,0 { TUPLE_ARRAY_CONVERSIONS, BUILD_HASHER_HASH_ONE }

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
255255
crate::large_include_file::LARGE_INCLUDE_FILE_INFO,
256256
crate::large_stack_arrays::LARGE_STACK_ARRAYS_INFO,
257257
crate::large_stack_frames::LARGE_STACK_FRAMES_INFO,
258+
crate::lazy_lock_like::ONCE_CELL_LAZY_INFO,
258259
crate::legacy_numeric_constants::LEGACY_NUMERIC_CONSTANTS_INFO,
259260
crate::len_zero::COMPARISON_TO_EMPTY_INFO,
260261
crate::len_zero::LEN_WITHOUT_IS_EMPTY_INFO,

clippy_lints/src/lazy_lock_like.rs

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

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ mod large_futures;
186186
mod large_include_file;
187187
mod large_stack_arrays;
188188
mod large_stack_frames;
189+
mod lazy_lock_like;
189190
mod legacy_numeric_constants;
190191
mod len_zero;
191192
mod let_if_seq;
@@ -1171,6 +1172,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
11711172
});
11721173
store.register_late_pass(move |_| Box::new(string_patterns::StringPatterns::new(msrv())));
11731174
store.register_early_pass(|| Box::new(field_scoped_visibility_modifiers::FieldScopedVisibilityModifiers));
1175+
store.register_late_pass(move |_| Box::new(lazy_lock_like::OnceCellLazy::new(msrv())));
11741176
// add lints here, do not remove this comment, it's used in `new_lint`
11751177
}
11761178

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
}

0 commit comments

Comments
 (0)