use clippy_utils::diagnostics::span_lint_hir_and_then; use clippy_utils::source::snippet; use hir::def::{DefKind, Res}; use if_chain::if_chain; use rustc_ast::ast; use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_errors::Applicability; use rustc_hir as hir; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_session::{declare_tool_lint, impl_lint_pass}; use rustc_span::{edition::Edition, sym, Span}; declare_clippy_lint! { /// ### What it does /// Checks for `#[macro_use] use...`. /// /// ### Why is this bad? /// Since the Rust 2018 edition you can import /// macro's directly, this is considered idiomatic. /// /// ### Example /// ```rust,ignore /// #[macro_use] /// use some_macro; /// ``` #[clippy::version = "1.44.0"] pub MACRO_USE_IMPORTS, pedantic, "#[macro_use] is no longer needed" } #[derive(Clone, Debug, PartialEq, Eq)] struct PathAndSpan { path: String, span: Span, } /// `MacroRefData` includes the name of the macro. #[derive(Debug, Clone)] pub struct MacroRefData { name: String, } impl MacroRefData { pub fn new(name: String) -> Self { Self { name } } } #[derive(Default)] #[expect(clippy::module_name_repetitions)] pub struct MacroUseImports { /// the actual import path used and the span of the attribute above it. The value is /// the location, where the lint should be emitted. imports: Vec<(String, Span, hir::HirId)>, /// the span of the macro reference, kept to ensure only one reference is used per macro call. collected: FxHashSet, mac_refs: Vec, } impl_lint_pass!(MacroUseImports => [MACRO_USE_IMPORTS]); impl MacroUseImports { fn push_unique_macro(&mut self, cx: &LateContext<'_>, span: Span) { let call_site = span.source_callsite(); let name = snippet(cx, cx.sess().source_map().span_until_char(call_site, '!'), "_"); if span.source_callee().is_some() && !self.collected.contains(&call_site) { let name = if name.contains("::") { name.split("::").last().unwrap().to_string() } else { name.to_string() }; self.mac_refs.push(MacroRefData::new(name)); self.collected.insert(call_site); } } fn push_unique_macro_pat_ty(&mut self, cx: &LateContext<'_>, span: Span) { let call_site = span.source_callsite(); let name = snippet(cx, cx.sess().source_map().span_until_char(call_site, '!'), "_"); if span.source_callee().is_some() && !self.collected.contains(&call_site) { self.mac_refs.push(MacroRefData::new(name.to_string())); self.collected.insert(call_site); } } } impl<'tcx> LateLintPass<'tcx> for MacroUseImports { fn check_item(&mut self, cx: &LateContext<'_>, item: &hir::Item<'_>) { if_chain! { if cx.sess().opts.edition >= Edition::Edition2018; if let hir::ItemKind::Use(path, _kind) = &item.kind; let hir_id = item.hir_id(); let attrs = cx.tcx.hir().attrs(hir_id); if let Some(mac_attr) = attrs.iter().find(|attr| attr.has_name(sym::macro_use)); if let Some(id) = path.res.iter().find_map(|res| match res { Res::Def(DefKind::Mod, id) => Some(id), _ => None, }); if !id.is_local(); then { for kid in cx.tcx.module_children(id).iter() { if let Res::Def(DefKind::Macro(_mac_type), mac_id) = kid.res { let span = mac_attr.span; let def_path = cx.tcx.def_path_str(mac_id); self.imports.push((def_path, span, hir_id)); } } } else { if item.span.from_expansion() { self.push_unique_macro_pat_ty(cx, item.span); } } } } fn check_attribute(&mut self, cx: &LateContext<'_>, attr: &ast::Attribute) { if attr.span.from_expansion() { self.push_unique_macro(cx, attr.span); } } fn check_expr(&mut self, cx: &LateContext<'_>, expr: &hir::Expr<'_>) { if expr.span.from_expansion() { self.push_unique_macro(cx, expr.span); } } fn check_stmt(&mut self, cx: &LateContext<'_>, stmt: &hir::Stmt<'_>) { if stmt.span.from_expansion() { self.push_unique_macro(cx, stmt.span); } } fn check_pat(&mut self, cx: &LateContext<'_>, pat: &hir::Pat<'_>) { if pat.span.from_expansion() { self.push_unique_macro_pat_ty(cx, pat.span); } } fn check_ty(&mut self, cx: &LateContext<'_>, ty: &hir::Ty<'_>) { if ty.span.from_expansion() { self.push_unique_macro_pat_ty(cx, ty.span); } } fn check_crate_post(&mut self, cx: &LateContext<'_>) { let mut used = FxHashMap::default(); let mut check_dup = vec![]; for (import, span, hir_id) in &self.imports { let found_idx = self.mac_refs.iter().position(|mac| import.ends_with(&mac.name)); if let Some(idx) = found_idx { self.mac_refs.remove(idx); let seg = import.split("::").collect::>(); match seg.as_slice() { // an empty path is impossible // a path should always consist of 2 or more segments [] | [_] => return, [root, item] => { if !check_dup.contains(&(*item).to_string()) { used.entry(((*root).to_string(), span, hir_id)) .or_insert_with(Vec::new) .push((*item).to_string()); check_dup.push((*item).to_string()); } }, [root, rest @ ..] => { if rest.iter().all(|item| !check_dup.contains(&(*item).to_string())) { let filtered = rest .iter() .filter_map(|item| { if check_dup.contains(&(*item).to_string()) { None } else { Some((*item).to_string()) } }) .collect::>(); used.entry(((*root).to_string(), span, hir_id)) .or_insert_with(Vec::new) .push(filtered.join("::")); check_dup.extend(filtered); } else { let rest = rest.to_vec(); used.entry(((*root).to_string(), span, hir_id)) .or_insert_with(Vec::new) .push(rest.join("::")); check_dup.extend(rest.iter().map(ToString::to_string)); } }, } } } let mut suggestions = vec![]; for ((root, span, hir_id), path) in used { if path.len() == 1 { suggestions.push((span, format!("{root}::{}", path[0]), hir_id)); } else { suggestions.push((span, format!("{root}::{{{}}}", path.join(", ")), hir_id)); } } // If mac_refs is not empty we have encountered an import we could not handle // such as `std::prelude::v1::foo` or some other macro that expands to an import. if self.mac_refs.is_empty() { for (span, import, hir_id) in suggestions { let help = format!("use {import};"); span_lint_hir_and_then( cx, MACRO_USE_IMPORTS, *hir_id, *span, "`macro_use` attributes are no longer needed in the Rust 2018 edition", |diag| { diag.span_suggestion( *span, "remove the attribute and import the macro directly, try", help, Applicability::MaybeIncorrect, ); }, ); } } } }