blob: f66f33fee1669594beefa96dda511a4ae10234a8 [file] [log] [blame]
use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_then};
use clippy_utils::source::snippet_with_applicability;
use clippy_utils::{def_path_def_ids, is_lint_allowed, match_any_def_paths, peel_hir_expr_refs};
use if_chain::if_chain;
use rustc_ast::ast::LitKind;
use rustc_data_structures::fx::{FxHashSet, FxIndexSet};
use rustc_errors::Applicability;
use rustc_hir as hir;
use rustc_hir::def::{DefKind, Res};
use rustc_hir::def_id::DefId;
use rustc_hir::{Expr, ExprKind, Local, Mutability, Node};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::mir::interpret::{Allocation, ConstValue, GlobalAlloc};
use rustc_middle::ty::{self, Ty};
use rustc_session::{declare_tool_lint, impl_lint_pass};
use rustc_span::symbol::Symbol;
use rustc_span::Span;
use std::str;
declare_clippy_lint! {
/// ### What it does
/// Checks for usage of def paths when a diagnostic item or a `LangItem` could be used.
///
/// ### Why is this bad?
/// The path for an item is subject to change and is less efficient to look up than a
/// diagnostic item or a `LangItem`.
///
/// ### Example
/// ```rust,ignore
/// utils::match_type(cx, ty, &paths::VEC)
/// ```
///
/// Use instead:
/// ```rust,ignore
/// utils::is_type_diagnostic_item(cx, ty, sym::Vec)
/// ```
pub UNNECESSARY_DEF_PATH,
internal,
"using a def path when a diagnostic item or a `LangItem` is available"
}
impl_lint_pass!(UnnecessaryDefPath => [UNNECESSARY_DEF_PATH]);
#[derive(Default)]
pub struct UnnecessaryDefPath {
array_def_ids: FxIndexSet<(DefId, Span)>,
linted_def_ids: FxHashSet<DefId>,
}
impl<'tcx> LateLintPass<'tcx> for UnnecessaryDefPath {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) {
if is_lint_allowed(cx, UNNECESSARY_DEF_PATH, expr.hir_id) {
return;
}
match expr.kind {
ExprKind::Call(func, args) => self.check_call(cx, func, args, expr.span),
ExprKind::Array(elements) => self.check_array(cx, elements, expr.span),
_ => {},
}
}
fn check_crate_post(&mut self, cx: &LateContext<'tcx>) {
for &(def_id, span) in &self.array_def_ids {
if self.linted_def_ids.contains(&def_id) {
continue;
}
let (msg, sugg) = if let Some(sym) = cx.tcx.get_diagnostic_name(def_id) {
("diagnostic item", format!("sym::{sym}"))
} else if let Some(sym) = get_lang_item_name(cx, def_id) {
("language item", format!("LangItem::{sym}"))
} else {
continue;
};
span_lint_and_help(
cx,
UNNECESSARY_DEF_PATH,
span,
&format!("hardcoded path to a {msg}"),
None,
&format!("convert all references to use `{sugg}`"),
);
}
}
}
impl UnnecessaryDefPath {
#[allow(clippy::too_many_lines)]
fn check_call(&mut self, cx: &LateContext<'_>, func: &Expr<'_>, args: &[Expr<'_>], span: Span) {
enum Item {
LangItem(&'static str),
DiagnosticItem(Symbol),
}
static PATHS: &[&[&str]] = &[
&["clippy_utils", "match_def_path"],
&["clippy_utils", "match_trait_method"],
&["clippy_utils", "ty", "match_type"],
&["clippy_utils", "is_expr_path_def_path"],
];
if_chain! {
if let [cx_arg, def_arg, args @ ..] = args;
if let ExprKind::Path(path) = &func.kind;
if let Some(id) = cx.qpath_res(path, func.hir_id).opt_def_id();
if let Some(which_path) = match_any_def_paths(cx, id, PATHS);
let item_arg = if which_path == 4 { &args[1] } else { &args[0] };
// Extract the path to the matched type
if let Some(segments) = path_to_matched_type(cx, item_arg);
let segments: Vec<&str> = segments.iter().map(|sym| &**sym).collect();
if let Some(def_id) = def_path_def_ids(cx, &segments[..]).next();
then {
// Check if the target item is a diagnostic item or LangItem.
#[rustfmt::skip]
let (msg, item) = if let Some(item_name)
= cx.tcx.diagnostic_items(def_id.krate).id_to_name.get(&def_id)
{
(
"use of a def path to a diagnostic item",
Item::DiagnosticItem(*item_name),
)
} else if let Some(item_name) = get_lang_item_name(cx, def_id) {
(
"use of a def path to a `LangItem`",
Item::LangItem(item_name),
)
} else {
return;
};
let has_ctor = match cx.tcx.def_kind(def_id) {
DefKind::Struct => {
let variant = cx.tcx.adt_def(def_id).non_enum_variant();
variant.ctor.is_some() && variant.fields.iter().all(|f| f.vis.is_public())
},
DefKind::Variant => {
let variant = cx.tcx.adt_def(cx.tcx.parent(def_id)).variant_with_id(def_id);
variant.ctor.is_some() && variant.fields.iter().all(|f| f.vis.is_public())
},
_ => false,
};
let mut app = Applicability::MachineApplicable;
let cx_snip = snippet_with_applicability(cx, cx_arg.span, "..", &mut app);
let def_snip = snippet_with_applicability(cx, def_arg.span, "..", &mut app);
let (sugg, with_note) = match (which_path, item) {
// match_def_path
(0, Item::DiagnosticItem(item)) => (
format!("{cx_snip}.tcx.is_diagnostic_item(sym::{item}, {def_snip})"),
has_ctor,
),
(0, Item::LangItem(item)) => (
format!("{cx_snip}.tcx.lang_items().get(LangItem::{item}) == Some({def_snip})"),
has_ctor,
),
// match_trait_method
(1, Item::DiagnosticItem(item)) => {
(format!("is_trait_method({cx_snip}, {def_snip}, sym::{item})"), false)
},
// match_type
(2, Item::DiagnosticItem(item)) => (
format!("is_type_diagnostic_item({cx_snip}, {def_snip}, sym::{item})"),
false,
),
(2, Item::LangItem(item)) => (
format!("is_type_lang_item({cx_snip}, {def_snip}, LangItem::{item})"),
false,
),
// is_expr_path_def_path
(3, Item::DiagnosticItem(item)) if has_ctor => (
format!("is_res_diag_ctor({cx_snip}, path_res({cx_snip}, {def_snip}), sym::{item})",),
false,
),
(3, Item::LangItem(item)) if has_ctor => (
format!("is_res_lang_ctor({cx_snip}, path_res({cx_snip}, {def_snip}), LangItem::{item})",),
false,
),
(3, Item::DiagnosticItem(item)) => (
format!("is_path_diagnostic_item({cx_snip}, {def_snip}, sym::{item})"),
false,
),
(3, Item::LangItem(item)) => (
format!(
"path_res({cx_snip}, {def_snip}).opt_def_id()\
.map_or(false, |id| {cx_snip}.tcx.lang_items().get(LangItem::{item}) == Some(id))",
),
false,
),
_ => return,
};
span_lint_and_then(cx, UNNECESSARY_DEF_PATH, span, msg, |diag| {
diag.span_suggestion(span, "try", sugg, app);
if with_note {
diag.help(
"if this `DefId` came from a constructor expression or pattern then the \
parent `DefId` should be used instead",
);
}
});
self.linted_def_ids.insert(def_id);
}
}
}
fn check_array(&mut self, cx: &LateContext<'_>, elements: &[Expr<'_>], span: Span) {
let Some(path) = path_from_array(elements) else { return };
for def_id in def_path_def_ids(cx, &path.iter().map(AsRef::as_ref).collect::<Vec<_>>()) {
self.array_def_ids.insert((def_id, span));
}
}
}
fn path_to_matched_type(cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> Option<Vec<String>> {
match peel_hir_expr_refs(expr).0.kind {
ExprKind::Path(ref qpath) => match cx.qpath_res(qpath, expr.hir_id) {
Res::Local(hir_id) => {
let parent_id = cx.tcx.hir().parent_id(hir_id);
if let Some(Node::Local(Local { init: Some(init), .. })) = cx.tcx.hir().find(parent_id) {
path_to_matched_type(cx, init)
} else {
None
}
},
Res::Def(DefKind::Static(_), def_id) => read_mir_alloc_def_path(
cx,
cx.tcx.eval_static_initializer(def_id).ok()?.inner(),
cx.tcx.type_of(def_id).instantiate_identity(),
),
Res::Def(DefKind::Const, def_id) => match cx.tcx.const_eval_poly(def_id).ok()? {
ConstValue::ByRef { alloc, offset } if offset.bytes() == 0 => {
read_mir_alloc_def_path(cx, alloc.inner(), cx.tcx.type_of(def_id).instantiate_identity())
},
_ => None,
},
_ => None,
},
ExprKind::Array(exprs) => path_from_array(exprs),
_ => None,
}
}
fn read_mir_alloc_def_path<'tcx>(cx: &LateContext<'tcx>, alloc: &'tcx Allocation, ty: Ty<'_>) -> Option<Vec<String>> {
let (alloc, ty) = if let ty::Ref(_, ty, Mutability::Not) = *ty.kind() {
let &alloc = alloc.provenance().ptrs().values().next()?;
if let GlobalAlloc::Memory(alloc) = cx.tcx.global_alloc(alloc) {
(alloc.inner(), ty)
} else {
return None;
}
} else {
(alloc, ty)
};
if let ty::Array(ty, _) | ty::Slice(ty) = *ty.kind()
&& let ty::Ref(_, ty, Mutability::Not) = *ty.kind()
&& ty.is_str()
{
alloc
.provenance()
.ptrs()
.values()
.map(|&alloc| {
if let GlobalAlloc::Memory(alloc) = cx.tcx.global_alloc(alloc) {
let alloc = alloc.inner();
str::from_utf8(alloc.inspect_with_uninit_and_ptr_outside_interpreter(0..alloc.len()))
.ok().map(ToOwned::to_owned)
} else {
None
}
})
.collect()
} else {
None
}
}
fn path_from_array(exprs: &[Expr<'_>]) -> Option<Vec<String>> {
exprs
.iter()
.map(|expr| {
if let ExprKind::Lit(lit) = &expr.kind {
if let LitKind::Str(sym, _) = lit.node {
return Some((*sym.as_str()).to_owned());
}
}
None
})
.collect()
}
fn get_lang_item_name(cx: &LateContext<'_>, def_id: DefId) -> Option<&'static str> {
if let Some((lang_item, _)) = cx.tcx.lang_items().iter().find(|(_, id)| *id == def_id) {
Some(lang_item.variant_name())
} else {
None
}
}