extern crate rustc_hir;
extern crate rustc_middle;
extern crate rustc_span;
use clippy_utils::ty::implements_trait;
use rustc_hir::def::{DefKind, Res};
use rustc_hir::def_id::DefId;
use rustc_hir::{self as hir, Expr, ExprKind, ImplItemKind, ItemKind, QPath};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_middle::ty::{Ty, TypeckResults};
use rustc_span::hygiene::{ExpnKind, MacroKind};
dylint_linting::declare_late_lint! {
#[doc = include_str!("../../docs/de13_common_patterns/de1302_error_from_to_string/README.md")]
pub DE1302_ERROR_FROM_TO_STRING,
Deny,
"calling .to_string() in From<XxxError> impl destroys the error chain (DE1302)"
}
fn implements_error<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
let Some(error_did) = cx.tcx.get_diagnostic_item(rustc_span::sym::Error) else {
return false;
};
implements_trait(cx, ty, error_did, &[])
}
struct ToStringVisitor<'tcx, 'cx> {
cx: &'cx LateContext<'tcx>,
typeck: &'tcx TypeckResults<'tcx>,
source_ty: Ty<'tcx>,
error_assoc_ty: Option<Ty<'tcx>>,
}
impl<'tcx> ToStringVisitor<'tcx, '_> {
fn emit(&self, span: rustc_span::Span) {
self.cx.span_lint(DE1302_ERROR_FROM_TO_STRING, span, |diag| {
diag.primary_message(
"`.to_string()` in `From`/`TryFrom` impl destroys the error chain (DE1302)",
);
diag.help(
"store the source error directly, use an enum variant, or use `#[from]` with thiserror",
);
diag.note(
"`.to_string()` discards the original error type: `.source()` returns None and the error cannot be downcast",
);
});
}
fn is_relevant_receiver(&self, ty: Ty<'tcx>) -> bool {
let inner = ty.peel_refs();
if let Some(e) = self.error_assoc_ty
&& inner == e
{
return true;
}
inner == self.source_ty && implements_error(self.cx, inner)
}
fn visit_closure_body(&mut self, closure: &'tcx hir::Closure<'tcx>) {
let body = self.cx.tcx.hir_body(closure.body);
let prev = std::mem::replace(&mut self.typeck, self.cx.tcx.typeck(closure.def_id));
hir::intravisit::walk_expr(self, body.value);
self.typeck = prev;
}
}
fn is_to_string_def<'tcx>(cx: &LateContext<'tcx>, def_id: DefId) -> bool {
let Some(to_string_trait) = cx.tcx.get_diagnostic_item(rustc_span::sym::ToString) else {
return false;
};
cx.tcx.trait_of_assoc(def_id) == Some(to_string_trait)
}
fn is_hidden_expansion(span: rustc_span::Span) -> bool {
matches!(
span.ctxt().outer_expn_data().kind,
ExpnKind::Macro(MacroKind::Attr | MacroKind::Derive, _) | ExpnKind::Desugaring(_)
)
}
impl<'tcx> hir::intravisit::Visitor<'tcx> for ToStringVisitor<'tcx, '_> {
fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) {
let hidden = is_hidden_expansion(expr.span);
match expr.kind {
ExprKind::MethodCall(seg, recv, args, _) if !hidden => {
if seg.ident.name.as_str() == "to_string"
&& args.is_empty()
&& let Some(def_id) = self.typeck.type_dependent_def_id(expr.hir_id)
&& is_to_string_def(self.cx, def_id)
{
let recv_ty = self.typeck.expr_ty(recv);
if self.is_relevant_receiver(recv_ty) {
self.emit(expr.span);
}
}
}
ExprKind::Call(callee, [arg]) if !hidden => {
if is_to_string_path(self.cx, callee) {
let arg_ty = self.typeck.expr_ty(arg);
if self.is_relevant_receiver(arg_ty) {
self.emit(expr.span);
}
}
}
ExprKind::Closure(closure) => {
self.visit_closure_body(closure);
return;
}
_ => {}
}
hir::intravisit::walk_expr(self, expr);
}
}
fn is_to_string_path<'tcx>(cx: &LateContext<'tcx>, callee: &Expr<'tcx>) -> bool {
let ExprKind::Path(qpath) = &callee.kind else {
return false;
};
let res = match qpath {
QPath::Resolved(_, path) => path.res,
QPath::TypeRelative(..) => cx.qpath_res(qpath, callee.hir_id),
};
let Res::Def(DefKind::AssocFn, def_id) = res else {
return false;
};
is_to_string_def(cx, def_id)
}
impl<'tcx> LateLintPass<'tcx> for De1302ErrorFromToString {
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'tcx>) {
let ItemKind::Impl(impl_block) = item.kind else {
return;
};
let Some(trait_ref) = impl_block.of_trait else {
return;
};
let Some(last_seg) = trait_ref.trait_ref.path.segments.last() else {
return;
};
let conversion_method = match last_seg.ident.name.as_str() {
"From" => "from",
"TryFrom" => "try_from",
_ => return,
};
let impl_def_id = item.owner_id.def_id;
let impl_trait_ref = cx.tcx.impl_trait_ref(impl_def_id).instantiate_identity();
let source_ty = impl_trait_ref.args.type_at(1); let target_ty = impl_trait_ref.args.type_at(0);
let error_assoc_ty: Option<Ty<'tcx>> = if conversion_method == "try_from" {
impl_block.items.iter().find_map(|item_ref| {
let node = cx.tcx.hir_node_by_def_id(item_ref.owner_id.def_id);
let hir::Node::ImplItem(impl_item) = node else {
return None;
};
if impl_item.ident.name.as_str() != "Error" {
return None;
}
if !matches!(impl_item.kind, ImplItemKind::Type(..)) {
return None;
}
let ty = cx
.tcx
.type_of(item_ref.owner_id.def_id)
.instantiate_identity();
implements_error(cx, ty).then_some(ty)
})
} else {
None
};
if !implements_error(cx, source_ty)
&& !implements_error(cx, target_ty)
&& error_assoc_ty.is_none()
{
return;
}
for item_ref in impl_block.items {
let node = cx.tcx.hir_node_by_def_id(item_ref.owner_id.def_id);
let hir::Node::ImplItem(impl_item) = node else {
continue;
};
if impl_item.ident.name.as_str() != conversion_method {
continue;
}
let ImplItemKind::Fn(_, body_id) = impl_item.kind else {
continue;
};
let body = cx.tcx.hir_body(body_id);
let typeck = cx.tcx.typeck(item_ref.owner_id.def_id);
let mut visitor = ToStringVisitor {
cx,
typeck,
source_ty,
error_assoc_ty,
};
hir::intravisit::walk_expr(&mut visitor, body.value);
}
}
}