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