blob: f26647fa34854cd3bc24ddf26dc821cc405578a3 [file] [log] [blame] [edit]
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::{return_ty, sym};
use rustc_hir::{
Body, BodyOwnerKind, Expr, ExprKind, FnSig, ImplItem, ImplItemKind, Item, ItemKind, OwnerId, PathSegment, QPath,
};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::Ty;
use rustc_session::impl_lint_pass;
use rustc_span::{Ident, Span, Symbol};
declare_clippy_lint! {
/// ### What it does
/// Checks for functions of type `Result` that contain `expect()` or `unwrap()`
///
/// ### Why restrict this?
/// These functions promote recoverable errors to non-recoverable errors,
/// which may be undesirable in code bases which wish to avoid panics,
/// or be a bug in the specific function.
///
/// ### Known problems
/// This can cause false positives in functions that handle both recoverable and non recoverable errors.
///
/// ### Example
/// Before:
/// ```no_run
/// fn divisible_by_3(i_str: String) -> Result<(), String> {
/// let i = i_str
/// .parse::<i32>()
/// .expect("cannot divide the input by three");
///
/// if i % 3 != 0 {
/// Err("Number is not divisible by 3")?
/// }
///
/// Ok(())
/// }
/// ```
///
/// After:
/// ```no_run
/// fn divisible_by_3(i_str: String) -> Result<(), String> {
/// let i = i_str
/// .parse::<i32>()
/// .map_err(|e| format!("cannot divide the input by three: {}", e))?;
///
/// if i % 3 != 0 {
/// Err("Number is not divisible by 3")?
/// }
///
/// Ok(())
/// }
/// ```
#[clippy::version = "1.48.0"]
pub UNWRAP_IN_RESULT,
restriction,
"functions of type `Result<..>` or `Option`<...> that contain `expect()` or `unwrap()`"
}
impl_lint_pass!(UnwrapInResult=> [UNWRAP_IN_RESULT]);
#[derive(Clone, Copy, Eq, PartialEq)]
enum OptionOrResult {
Option,
Result,
}
impl OptionOrResult {
fn with_article(self) -> &'static str {
match self {
Self::Option => "an `Option`",
Self::Result => "a `Result`",
}
}
}
struct OptionOrResultFn {
kind: OptionOrResult,
return_ty_span: Option<Span>,
}
#[derive(Default)]
pub struct UnwrapInResult {
fn_stack: Vec<Option<OptionOrResultFn>>,
current_fn: Option<OptionOrResultFn>,
}
impl UnwrapInResult {
fn enter_item(&mut self, cx: &LateContext<'_>, fn_def_id: OwnerId, sig: &FnSig<'_>) {
self.fn_stack.push(self.current_fn.take());
self.current_fn = is_option_or_result(cx, return_ty(cx, fn_def_id)).map(|kind| OptionOrResultFn {
kind,
return_ty_span: Some(sig.decl.output.span()),
});
}
fn leave_item(&mut self) {
self.current_fn = self.fn_stack.pop().unwrap();
}
}
impl<'tcx> LateLintPass<'tcx> for UnwrapInResult {
fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &'tcx ImplItem<'_>) {
if let ImplItemKind::Fn(sig, _) = &impl_item.kind {
self.enter_item(cx, impl_item.owner_id, sig);
}
}
fn check_impl_item_post(&mut self, _: &LateContext<'tcx>, impl_item: &'tcx ImplItem<'tcx>) {
if let ImplItemKind::Fn(..) = impl_item.kind {
self.leave_item();
}
}
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
if let ItemKind::Fn {
has_body: true, sig, ..
} = &item.kind
{
self.enter_item(cx, item.owner_id, sig);
}
}
fn check_item_post(&mut self, _: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
if let ItemKind::Fn { has_body: true, .. } = item.kind {
self.leave_item();
}
}
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'_>) {
if expr.span.from_expansion() {
return;
}
if let Some(OptionOrResultFn {
kind,
ref mut return_ty_span,
}) = self.current_fn
&& let Some((oor, fn_name)) = is_unwrap_or_expect_call(cx, expr)
&& oor == kind
{
span_lint_and_then(
cx,
UNWRAP_IN_RESULT,
expr.span,
format!("`{fn_name}` used in a function that returns {}", kind.with_article()),
|diag| {
// Issue the note and help only once per function
if let Some(span) = return_ty_span.take() {
diag.span_note(span, "in this function signature");
let complement = if kind == OptionOrResult::Result {
" or calling the `.map_err()` method"
} else {
""
};
diag.help(format!("consider using the `?` operator{complement}"));
}
},
);
}
}
fn check_body(&mut self, cx: &LateContext<'tcx>, body: &Body<'tcx>) {
let body_def_id = cx.tcx.hir_body_owner_def_id(body.id());
if !matches!(cx.tcx.hir_body_owner_kind(body_def_id), BodyOwnerKind::Fn) {
// When entering a body which is not a function, mask the potential surrounding
// function to not apply the lint.
self.fn_stack.push(self.current_fn.take());
}
}
fn check_body_post(&mut self, cx: &LateContext<'tcx>, body: &Body<'tcx>) {
let body_def_id = cx.tcx.hir_body_owner_def_id(body.id());
if !matches!(cx.tcx.hir_body_owner_kind(body_def_id), BodyOwnerKind::Fn) {
// Unmask the potential surrounding function.
self.current_fn = self.fn_stack.pop().unwrap();
}
}
}
fn is_option_or_result(cx: &LateContext<'_>, ty: Ty<'_>) -> Option<OptionOrResult> {
match ty.ty_adt_def().and_then(|def| cx.tcx.get_diagnostic_name(def.did())) {
Some(sym::Option) => Some(OptionOrResult::Option),
Some(sym::Result) => Some(OptionOrResult::Result),
_ => None,
}
}
fn is_unwrap_or_expect_call(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<(OptionOrResult, Symbol)> {
if let ExprKind::Call(func, _) = expr.kind
&& let ExprKind::Path(QPath::TypeRelative(
hir_ty,
PathSegment {
ident:
Ident {
name: name @ (sym::unwrap | sym::expect),
..
},
..
},
)) = func.kind
{
is_option_or_result(cx, cx.typeck_results().node_type(hir_ty.hir_id)).map(|oor| (oor, *name))
} else if let ExprKind::MethodCall(
PathSegment {
ident: Ident {
name: name @ (sym::unwrap | sym::expect),
..
},
..
},
recv,
_,
_,
) = expr.kind
{
is_option_or_result(cx, cx.typeck_results().expr_ty_adjusted(recv)).map(|oor| (oor, *name))
} else {
None
}
}