rust/clippy_lints/src/returns.rs

504 lines
19 KiB
Rust

use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_hir_and_then};
use clippy_utils::source::{snippet_with_context, SpanRangeExt};
use clippy_utils::sugg::has_enclosing_paren;
use clippy_utils::visitors::{for_each_expr, Descend};
use clippy_utils::{
binary_expr_needs_parentheses, fn_def_id, is_from_proc_macro, is_inside_let_else, is_res_lang_ctor, path_res,
path_to_local_id, span_contains_cfg, span_find_starting_semi,
};
use core::ops::ControlFlow;
use rustc_ast::NestedMetaItem;
use rustc_errors::Applicability;
use rustc_hir::intravisit::FnKind;
use rustc_hir::LangItem::ResultErr;
use rustc_hir::{
Block, Body, Expr, ExprKind, FnDecl, HirId, ItemKind, LangItem, MatchSource, Node, OwnerNode, PatKind, QPath, Stmt,
StmtKind,
};
use rustc_lint::{LateContext, LateLintPass, Level, LintContext};
use rustc_middle::lint::in_external_macro;
use rustc_middle::ty::adjustment::Adjust;
use rustc_middle::ty::{self, GenericArgKind, Ty};
use rustc_session::declare_lint_pass;
use rustc_span::def_id::LocalDefId;
use rustc_span::{sym, BytePos, Pos, Span};
use std::borrow::Cow;
use std::fmt::Display;
declare_clippy_lint! {
/// ### What it does
/// Checks for `let`-bindings, which are subsequently
/// returned.
///
/// ### Why is this bad?
/// It is just extraneous code. Remove it to make your code
/// more rusty.
///
/// ### Known problems
/// In the case of some temporaries, e.g. locks, eliding the variable binding could lead
/// to deadlocks. See [this issue](https://github.com/rust-lang/rust/issues/37612).
/// This could become relevant if the code is later changed to use the code that would have been
/// bound without first assigning it to a let-binding.
///
/// ### Example
/// ```no_run
/// fn foo() -> String {
/// let x = String::new();
/// x
/// }
/// ```
/// instead, use
/// ```no_run
/// fn foo() -> String {
/// String::new()
/// }
/// ```
#[clippy::version = "pre 1.29.0"]
pub LET_AND_RETURN,
style,
"creating a let-binding and then immediately returning it like `let x = expr; x` at the end of a block"
}
declare_clippy_lint! {
/// ### What it does
/// Checks for return statements at the end of a block.
///
/// ### Why is this bad?
/// Removing the `return` and semicolon will make the code
/// more rusty.
///
/// ### Example
/// ```no_run
/// fn foo(x: usize) -> usize {
/// return x;
/// }
/// ```
/// simplify to
/// ```no_run
/// fn foo(x: usize) -> usize {
/// x
/// }
/// ```
#[clippy::version = "pre 1.29.0"]
pub NEEDLESS_RETURN,
// This lint requires some special handling in `check_final_expr` for `#[expect]`.
// This handling needs to be updated if the group gets changed. This should also
// be caught by tests.
style,
"using a return statement like `return expr;` where an expression would suffice"
}
declare_clippy_lint! {
/// ### What it does
/// Checks for return statements on `Err` paired with the `?` operator.
///
/// ### Why is this bad?
/// The `return` is unnecessary.
///
/// Returns may be used to add attributes to the return expression. Return
/// statements with attributes are therefore be accepted by this lint.
///
/// ### Example
/// ```rust,ignore
/// fn foo(x: usize) -> Result<(), Box<dyn Error>> {
/// if x == 0 {
/// return Err(...)?;
/// }
/// Ok(())
/// }
/// ```
/// simplify to
/// ```rust,ignore
/// fn foo(x: usize) -> Result<(), Box<dyn Error>> {
/// if x == 0 {
/// Err(...)?;
/// }
/// Ok(())
/// }
/// ```
/// if paired with `try_err`, use instead:
/// ```rust,ignore
/// fn foo(x: usize) -> Result<(), Box<dyn Error>> {
/// if x == 0 {
/// return Err(...);
/// }
/// Ok(())
/// }
/// ```
#[clippy::version = "1.73.0"]
pub NEEDLESS_RETURN_WITH_QUESTION_MARK,
style,
"using a return statement like `return Err(expr)?;` where removing it would suffice"
}
#[derive(PartialEq, Eq)]
enum RetReplacement<'tcx> {
Empty,
Block,
Unit,
NeedsPar(Cow<'tcx, str>, Applicability),
Expr(Cow<'tcx, str>, Applicability),
}
impl<'tcx> RetReplacement<'tcx> {
fn sugg_help(&self) -> &'static str {
match self {
Self::Empty | Self::Expr(..) => "remove `return`",
Self::Block => "replace `return` with an empty block",
Self::Unit => "replace `return` with a unit value",
Self::NeedsPar(..) => "remove `return` and wrap the sequence with parentheses",
}
}
fn applicability(&self) -> Applicability {
match self {
Self::Expr(_, ap) | Self::NeedsPar(_, ap) => *ap,
_ => Applicability::MachineApplicable,
}
}
}
impl<'tcx> Display for RetReplacement<'tcx> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Empty => write!(f, ""),
Self::Block => write!(f, "{{}}"),
Self::Unit => write!(f, "()"),
Self::NeedsPar(inner, _) => write!(f, "({inner})"),
Self::Expr(inner, _) => write!(f, "{inner}"),
}
}
}
declare_lint_pass!(Return => [LET_AND_RETURN, NEEDLESS_RETURN, NEEDLESS_RETURN_WITH_QUESTION_MARK]);
/// Checks if a return statement is "needed" in the middle of a block, or if it can be removed. This
/// is the case when the enclosing block expression is coerced to some other type, which only works
/// because of the never-ness of `return` expressions
fn stmt_needs_never_type(cx: &LateContext<'_>, stmt_hir_id: HirId) -> bool {
cx.tcx
.hir()
.parent_iter(stmt_hir_id)
.find_map(|(_, node)| if let Node::Expr(expr) = node { Some(expr) } else { None })
.is_some_and(|e| {
cx.typeck_results()
.expr_adjustments(e)
.iter()
.any(|adjust| adjust.target != cx.tcx.types.unit && matches!(adjust.kind, Adjust::NeverToAny))
})
}
impl<'tcx> LateLintPass<'tcx> for Return {
fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'_>) {
if !in_external_macro(cx.sess(), stmt.span)
&& let StmtKind::Semi(expr) = stmt.kind
&& let ExprKind::Ret(Some(ret)) = expr.kind
// return Err(...)? desugars to a match
// over a Err(...).branch()
// which breaks down to a branch call, with the callee being
// the constructor of the Err variant
&& let ExprKind::Match(maybe_cons, _, MatchSource::TryDesugar(_)) = ret.kind
&& let ExprKind::Call(_, [maybe_result_err]) = maybe_cons.kind
&& let ExprKind::Call(maybe_constr, _) = maybe_result_err.kind
&& is_res_lang_ctor(cx, path_res(cx, maybe_constr), ResultErr)
// Ensure this is not the final stmt, otherwise removing it would cause a compile error
&& let OwnerNode::Item(item) = cx.tcx.hir_owner_node(cx.tcx.hir().get_parent_item(expr.hir_id))
&& let ItemKind::Fn(_, _, body) = item.kind
&& let block = cx.tcx.hir().body(body).value
&& let ExprKind::Block(block, _) = block.kind
&& !is_inside_let_else(cx.tcx, expr)
&& let [.., final_stmt] = block.stmts
&& final_stmt.hir_id != stmt.hir_id
&& !is_from_proc_macro(cx, expr)
&& !stmt_needs_never_type(cx, stmt.hir_id)
{
span_lint_and_sugg(
cx,
NEEDLESS_RETURN_WITH_QUESTION_MARK,
expr.span.until(ret.span),
"unneeded `return` statement with `?` operator",
"remove it",
String::new(),
Applicability::MachineApplicable,
);
}
}
fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx Block<'_>) {
// we need both a let-binding stmt and an expr
if let Some(retexpr) = block.expr
&& let Some(stmt) = block.stmts.iter().last()
&& let StmtKind::Let(local) = &stmt.kind
&& local.ty.is_none()
&& cx.tcx.hir().attrs(local.hir_id).is_empty()
&& let Some(initexpr) = &local.init
&& let PatKind::Binding(_, local_id, _, _) = local.pat.kind
&& path_to_local_id(retexpr, local_id)
&& !last_statement_borrows(cx, initexpr)
&& !in_external_macro(cx.sess(), initexpr.span)
&& !in_external_macro(cx.sess(), retexpr.span)
&& !local.span.from_expansion()
&& !span_contains_cfg(cx, stmt.span.between(retexpr.span))
{
span_lint_hir_and_then(
cx,
LET_AND_RETURN,
retexpr.hir_id,
retexpr.span,
"returning the result of a `let` binding from a block",
|err| {
err.span_label(local.span, "unnecessary `let` binding");
if let Some(src) = initexpr.span.get_source_text(cx) {
let sugg = if binary_expr_needs_parentheses(initexpr) {
if has_enclosing_paren(&src) {
src.to_owned()
} else {
format!("({src})")
}
} else if !cx.typeck_results().expr_adjustments(retexpr).is_empty() {
if has_enclosing_paren(&src) {
format!("{src} as _")
} else {
format!("({src}) as _")
}
} else {
src.to_owned()
};
err.multipart_suggestion(
"return the expression directly",
vec![(local.span, String::new()), (retexpr.span, sugg)],
Applicability::MachineApplicable,
);
} else {
err.span_help(initexpr.span, "this expression can be directly returned");
}
},
);
}
}
fn check_fn(
&mut self,
cx: &LateContext<'tcx>,
kind: FnKind<'tcx>,
_: &'tcx FnDecl<'tcx>,
body: &'tcx Body<'tcx>,
sp: Span,
_: LocalDefId,
) {
if sp.from_expansion() {
return;
}
match kind {
FnKind::Closure => {
// when returning without value in closure, replace this `return`
// with an empty block to prevent invalid suggestion (see #6501)
let replacement = if let ExprKind::Ret(None) = &body.value.kind {
RetReplacement::Block
} else {
RetReplacement::Empty
};
check_final_expr(cx, body.value, vec![], replacement, None);
},
FnKind::ItemFn(..) | FnKind::Method(..) => {
check_block_return(cx, &body.value.kind, sp, vec![]);
},
}
}
}
// if `expr` is a block, check if there are needless returns in it
fn check_block_return<'tcx>(cx: &LateContext<'tcx>, expr_kind: &ExprKind<'tcx>, sp: Span, mut semi_spans: Vec<Span>) {
if let ExprKind::Block(block, _) = expr_kind {
if let Some(block_expr) = block.expr {
check_final_expr(cx, block_expr, semi_spans, RetReplacement::Empty, None);
} else if let Some(stmt) = block.stmts.iter().last() {
match stmt.kind {
StmtKind::Expr(expr) => {
check_final_expr(cx, expr, semi_spans, RetReplacement::Empty, None);
},
StmtKind::Semi(semi_expr) => {
// Remove ending semicolons and any whitespace ' ' in between.
// Without `return`, the suggestion might not compile if the semicolon is retained
if let Some(semi_span) = stmt.span.trim_start(semi_expr.span) {
let semi_span_to_remove =
span_find_starting_semi(cx.sess().source_map(), semi_span.with_hi(sp.hi()));
semi_spans.push(semi_span_to_remove);
}
check_final_expr(cx, semi_expr, semi_spans, RetReplacement::Empty, None);
},
_ => (),
}
}
}
}
fn check_final_expr<'tcx>(
cx: &LateContext<'tcx>,
expr: &'tcx Expr<'tcx>,
semi_spans: Vec<Span>, /* containing all the places where we would need to remove semicolons if finding an
* needless return */
replacement: RetReplacement<'tcx>,
match_ty_opt: Option<Ty<'_>>,
) {
let peeled_drop_expr = expr.peel_drop_temps();
match &peeled_drop_expr.kind {
// simple return is always "bad"
ExprKind::Ret(ref inner) => {
// check if expr return nothing
let ret_span = if inner.is_none() && replacement == RetReplacement::Empty {
extend_span_to_previous_non_ws(cx, peeled_drop_expr.span)
} else {
peeled_drop_expr.span
};
let replacement = if let Some(inner_expr) = inner {
// if desugar of `do yeet`, don't lint
if let ExprKind::Call(path_expr, _) = inner_expr.kind
&& let ExprKind::Path(QPath::LangItem(LangItem::TryTraitFromYeet, ..)) = path_expr.kind
{
return;
}
let mut applicability = Applicability::MachineApplicable;
let (snippet, _) = snippet_with_context(cx, inner_expr.span, ret_span.ctxt(), "..", &mut applicability);
if binary_expr_needs_parentheses(inner_expr) {
RetReplacement::NeedsPar(snippet, applicability)
} else {
RetReplacement::Expr(snippet, applicability)
}
} else {
match match_ty_opt {
Some(match_ty) => {
match match_ty.kind() {
// If the code got till here with
// tuple not getting detected before it,
// then we are sure it's going to be Unit
// type
ty::Tuple(_) => RetReplacement::Unit,
// We don't want to anything in this case
// cause we can't predict what the user would
// want here
_ => return,
}
},
None => replacement,
}
};
let borrows = inner.map_or(false, |inner| last_statement_borrows(cx, inner));
if borrows {
return;
}
if ret_span.from_expansion() {
return;
}
// Returns may be used to turn an expression into a statement in rustc's AST.
// This allows the addition of attributes, like `#[allow]` (See: clippy#9361)
// `#[expect(clippy::needless_return)]` needs to be handled separately to
// actually fulfill the expectation (clippy::#12998)
match cx.tcx.hir().attrs(expr.hir_id) {
[] => {},
[attr] => {
if matches!(Level::from_attr(attr), Some(Level::Expect(_)))
&& let metas = attr.meta_item_list()
&& let Some(lst) = metas
&& let [NestedMetaItem::MetaItem(meta_item)] = lst.as_slice()
&& let [tool, lint_name] = meta_item.path.segments.as_slice()
&& tool.ident.name == sym::clippy
&& matches!(
lint_name.ident.name.as_str(),
"needless_return" | "style" | "all" | "warnings"
)
{
// This is an expectation of the `needless_return` lint
} else {
return;
}
},
_ => return,
}
emit_return_lint(cx, ret_span, semi_spans, &replacement, expr.hir_id);
},
ExprKind::If(_, then, else_clause_opt) => {
check_block_return(cx, &then.kind, peeled_drop_expr.span, semi_spans.clone());
if let Some(else_clause) = else_clause_opt {
check_block_return(cx, &else_clause.kind, peeled_drop_expr.span, semi_spans);
}
},
// a match expr, check all arms
// an if/if let expr, check both exprs
// note, if without else is going to be a type checking error anyways
// (except for unit type functions) so we don't match it
ExprKind::Match(_, arms, MatchSource::Normal) => {
let match_ty = cx.typeck_results().expr_ty(peeled_drop_expr);
for arm in *arms {
check_final_expr(cx, arm.body, semi_spans.clone(), RetReplacement::Unit, Some(match_ty));
}
},
// if it's a whole block, check it
other_expr_kind => check_block_return(cx, other_expr_kind, peeled_drop_expr.span, semi_spans),
}
}
fn emit_return_lint(
cx: &LateContext<'_>,
ret_span: Span,
semi_spans: Vec<Span>,
replacement: &RetReplacement<'_>,
at: HirId,
) {
span_lint_hir_and_then(
cx,
NEEDLESS_RETURN,
at,
ret_span,
"unneeded `return` statement",
|diag| {
let suggestions = std::iter::once((ret_span, replacement.to_string()))
.chain(semi_spans.into_iter().map(|span| (span, String::new())))
.collect();
diag.multipart_suggestion_verbose(replacement.sugg_help(), suggestions, replacement.applicability());
},
);
}
fn last_statement_borrows<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> bool {
for_each_expr(cx, expr, |e| {
if let Some(def_id) = fn_def_id(cx, e)
&& cx
.tcx
.fn_sig(def_id)
.instantiate_identity()
.skip_binder()
.output()
.walk()
.any(|arg| matches!(arg.unpack(), GenericArgKind::Lifetime(re) if !re.is_static()))
{
ControlFlow::Break(())
} else {
ControlFlow::Continue(Descend::from(!e.span.from_expansion()))
}
})
.is_some()
}
// Go backwards while encountering whitespace and extend the given Span to that point.
fn extend_span_to_previous_non_ws(cx: &LateContext<'_>, sp: Span) -> Span {
if let Ok(prev_source) = cx.sess().source_map().span_to_prev_source(sp) {
let ws = [b' ', b'\t', b'\n'];
if let Some(non_ws_pos) = prev_source.bytes().rposition(|c| !ws.contains(&c)) {
let len = prev_source.len() - non_ws_pos - 1;
return sp.with_lo(sp.lo() - BytePos::from_usize(len));
}
}
sp
}