|
| 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 | +} |
0 commit comments