| use clippy_config::Conf; | |
| use clippy_utils::diagnostics::span_lint_and_sugg; | |
| use clippy_utils::macros::macro_backtrace; | |
| use clippy_utils::msrvs::{self, Msrv}; | |
| use clippy_utils::qualify_min_const_fn::is_min_const_fn; | |
| use clippy_utils::source::snippet; | |
| use clippy_utils::{fn_has_unsatisfiable_preds, peel_blocks, sym}; | |
| use rustc_errors::Applicability; | |
| use rustc_hir::{Expr, ExprKind, intravisit}; | |
| use rustc_lint::{LateContext, LateLintPass}; | |
| use rustc_session::impl_lint_pass; | |
| declare_clippy_lint! { | |
| /// ### What it does | |
| /// Suggests to use `const` in `thread_local!` macro if possible. | |
| /// ### Why is this bad? | |
| /// | |
| /// The `thread_local!` macro wraps static declarations and makes them thread-local. | |
| /// It supports using a `const` keyword that may be used for declarations that can | |
| /// be evaluated as a constant expression. This can enable a more efficient thread | |
| /// local implementation that can avoid lazy initialization. For types that do not | |
| /// need to be dropped, this can enable an even more efficient implementation that | |
| /// does not need to track any additional state. | |
| /// | |
| /// https://doc.rust-lang.org/std/macro.thread_local.html | |
| /// | |
| /// ### Example | |
| /// ```no_run | |
| /// thread_local! { | |
| /// static BUF: String = String::new(); | |
| /// } | |
| /// ``` | |
| /// Use instead: | |
| /// ```no_run | |
| /// thread_local! { | |
| /// static BUF: String = const { String::new() }; | |
| /// } | |
| /// ``` | |
| #[clippy::version = "1.77.0"] | |
| pub MISSING_CONST_FOR_THREAD_LOCAL, | |
| perf, | |
| "suggest using `const` in `thread_local!` macro" | |
| } | |
| impl_lint_pass!(MissingConstForThreadLocal => [MISSING_CONST_FOR_THREAD_LOCAL]); | |
| pub struct MissingConstForThreadLocal { | |
| msrv: Msrv, | |
| } | |
| impl MissingConstForThreadLocal { | |
| pub fn new(conf: &'static Conf) -> Self { | |
| Self { msrv: conf.msrv } | |
| } | |
| } | |
| #[inline] | |
| fn is_thread_local_initializer( | |
| cx: &LateContext<'_>, | |
| fn_kind: intravisit::FnKind<'_>, | |
| span: rustc_span::Span, | |
| ) -> Option<bool> { | |
| let macro_def_id = span.source_callee()?.macro_def_id?; | |
| Some( | |
| cx.tcx.is_diagnostic_item(sym::thread_local_macro, macro_def_id) | |
| && matches!(fn_kind, intravisit::FnKind::ItemFn(..)), | |
| ) | |
| } | |
| fn is_unreachable(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { | |
| if let Some(macro_call) = macro_backtrace(expr.span).next() | |
| && let Some(diag_name) = cx.tcx.get_diagnostic_name(macro_call.def_id) | |
| { | |
| return (matches!( | |
| diag_name, | |
| sym::core_panic_macro | |
| | sym::std_panic_macro | |
| | sym::core_panic_2015_macro | |
| | sym::std_panic_2015_macro | |
| | sym::core_panic_2021_macro | |
| ) && !cx.tcx.hir_is_inside_const_context(expr.hir_id)) | |
| || matches!( | |
| diag_name, | |
| sym::unimplemented_macro | sym::todo_macro | sym::unreachable_macro | sym::unreachable_2015_macro | |
| ); | |
| } | |
| false | |
| } | |
| #[inline] | |
| fn initializer_can_be_made_const(cx: &LateContext<'_>, defid: rustc_span::def_id::DefId, msrv: Msrv) -> bool { | |
| // Building MIR for `fn`s with unsatisfiable preds results in ICE. | |
| if !fn_has_unsatisfiable_preds(cx, defid) | |
| && let mir = cx.tcx.optimized_mir(defid) | |
| && let Ok(()) = is_min_const_fn(cx, mir, msrv) | |
| { | |
| return true; | |
| } | |
| false | |
| } | |
| impl<'tcx> LateLintPass<'tcx> for MissingConstForThreadLocal { | |
| fn check_fn( | |
| &mut self, | |
| cx: &LateContext<'tcx>, | |
| fn_kind: intravisit::FnKind<'tcx>, | |
| _: &'tcx rustc_hir::FnDecl<'tcx>, | |
| body: &'tcx rustc_hir::Body<'tcx>, | |
| span: rustc_span::Span, | |
| local_defid: rustc_span::def_id::LocalDefId, | |
| ) { | |
| let defid = local_defid.to_def_id(); | |
| if is_thread_local_initializer(cx, fn_kind, span).unwrap_or(false) | |
| // Some implementations of `thread_local!` include an initializer fn. | |
| // In the case of a const initializer, the init fn is also const, | |
| // so we can skip the lint in that case. This occurs only on some | |
| // backends due to conditional compilation: | |
| // https://doc.rust-lang.org/src/std/sys/common/thread_local/mod.rs.html | |
| // for details on this issue, see: | |
| // https://github.com/rust-lang/rust-clippy/pull/12276 | |
| && !cx.tcx.is_const_fn(defid) | |
| && let ExprKind::Block(block, _) = body.value.kind | |
| && let Some(unpeeled) = block.expr | |
| && let ret_expr = peel_blocks(unpeeled) | |
| // A common pattern around threadlocal! is to make the value unreachable | |
| // to force an initialization before usage | |
| // https://github.com/rust-lang/rust-clippy/issues/12637 | |
| // we ensure that this is reachable before we check in mir | |
| && !is_unreachable(cx, ret_expr) | |
| && initializer_can_be_made_const(cx, defid, self.msrv) | |
| // we know that the function is const-qualifiable, so now | |
| // we need only to get the initializer expression to span-lint it. | |
| && let initializer_snippet = snippet(cx, ret_expr.span, "thread_local! { ... }") | |
| && initializer_snippet != "thread_local! { ... }" | |
| && self.msrv.meets(cx, msrvs::THREAD_LOCAL_CONST_INIT) | |
| { | |
| span_lint_and_sugg( | |
| cx, | |
| MISSING_CONST_FOR_THREAD_LOCAL, | |
| unpeeled.span, | |
| "initializer for `thread_local` value can be made `const`", | |
| "replace with", | |
| format!("const {{ {initializer_snippet} }}"), | |
| Applicability::MachineApplicable, | |
| ); | |
| } | |
| } | |
| } |