Skip to content

Commit 22f8c13

Browse files
committed
Improve implicit_return
Better suggestions when returning macro calls. Suggest changeing all the break expressions in a loop, not just the final statement. Don't lint divergent functions. Don't suggest returning the result of any divergent fuction.
1 parent 98e2b9f commit 22f8c13

File tree

6 files changed

+366
-140
lines changed

6 files changed

+366
-140
lines changed

clippy_lints/src/implicit_return.rs

Lines changed: 159 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
use clippy_utils::diagnostics::span_lint_and_then;
2-
use clippy_utils::match_panic_def_id;
3-
use clippy_utils::source::snippet_opt;
4-
use if_chain::if_chain;
1+
use clippy_utils::{
2+
diagnostics::span_lint_and_sugg,
3+
source::{snippet_with_applicability, snippet_with_context, walk_span_to_context},
4+
visitors::visit_break_exprs,
5+
};
56
use rustc_errors::Applicability;
67
use rustc_hir::intravisit::FnKind;
7-
use rustc_hir::{Body, Expr, ExprKind, FnDecl, HirId, MatchSource, StmtKind};
8-
use rustc_lint::{LateContext, LateLintPass};
8+
use rustc_hir::{Block, Body, Expr, ExprKind, FnDecl, FnRetTy, HirId};
9+
use rustc_lint::{LateContext, LateLintPass, LintContext};
10+
use rustc_middle::lint::in_external_macro;
911
use rustc_session::{declare_lint_pass, declare_tool_lint};
10-
use rustc_span::source_map::Span;
12+
use rustc_span::{Span, SyntaxContext};
1113

1214
declare_clippy_lint! {
1315
/// **What it does:** Checks for missing return statements at the end of a block.
@@ -39,109 +41,184 @@ declare_clippy_lint! {
3941

4042
declare_lint_pass!(ImplicitReturn => [IMPLICIT_RETURN]);
4143

42-
static LINT_BREAK: &str = "change `break` to `return` as shown";
43-
static LINT_RETURN: &str = "add `return` as shown";
44-
45-
fn lint(cx: &LateContext<'_>, outer_span: Span, inner_span: Span, msg: &str) {
46-
let outer_span = outer_span.source_callsite();
47-
let inner_span = inner_span.source_callsite();
48-
49-
span_lint_and_then(cx, IMPLICIT_RETURN, outer_span, "missing `return` statement", |diag| {
50-
if let Some(snippet) = snippet_opt(cx, inner_span) {
51-
diag.span_suggestion(
52-
outer_span,
53-
msg,
54-
format!("return {}", snippet),
55-
Applicability::MachineApplicable,
56-
);
57-
}
58-
});
44+
fn lint_return(cx: &LateContext<'_>, span: Span) {
45+
let mut app = Applicability::MachineApplicable;
46+
let snip = snippet_with_applicability(cx, span, "..", &mut app);
47+
span_lint_and_sugg(
48+
cx,
49+
IMPLICIT_RETURN,
50+
span,
51+
"missing `return` statement",
52+
"add `return` as shown",
53+
format!("return {}", snip),
54+
app,
55+
);
56+
}
57+
58+
fn lint_break(cx: &LateContext<'_>, break_span: Span, expr_span: Span) {
59+
let mut app = Applicability::MachineApplicable;
60+
let snip = snippet_with_context(cx, expr_span, break_span.ctxt(), "..", &mut app).0;
61+
span_lint_and_sugg(
62+
cx,
63+
IMPLICIT_RETURN,
64+
break_span,
65+
"missing `return` statement",
66+
"change `break` to `return` as shown",
67+
format!("return {}", snip),
68+
app,
69+
)
70+
}
71+
72+
enum LintLocation {
73+
/// The lint was applied to a parent expression.
74+
Parent,
75+
/// The lint was applied to this expression, a child, or not applied.
76+
Inner,
5977
}
78+
impl LintLocation {
79+
fn still_parent(self, b: bool) -> Self {
80+
if b { self } else { Self::Inner }
81+
}
6082

61-
fn expr_match(cx: &LateContext<'_>, expr: &Expr<'_>) {
83+
fn is_parent(&self) -> bool {
84+
matches!(*self, Self::Parent)
85+
}
86+
}
87+
88+
// Gets the call site if the span is in a child context. Otherwise returns `None`.
89+
fn get_call_site(span: Span, ctxt: SyntaxContext) -> Option<Span> {
90+
(span.ctxt() != ctxt).then(|| walk_span_to_context(span, ctxt).unwrap_or(span))
91+
}
92+
93+
fn lint_implicit_returns(
94+
cx: &LateContext<'tcx>,
95+
expr: &'tcx Expr<'_>,
96+
// The context of the function body.
97+
ctxt: SyntaxContext,
98+
// Whether the expression is from a macro expansion.
99+
call_site_span: Option<Span>,
100+
) -> LintLocation {
62101
match expr.kind {
63-
// loops could be using `break` instead of `return`
64-
ExprKind::Block(block, ..) | ExprKind::Loop(block, ..) => {
65-
if let Some(expr) = &block.expr {
66-
expr_match(cx, expr);
67-
}
68-
// only needed in the case of `break` with `;` at the end
69-
else if let Some(stmt) = block.stmts.last() {
70-
if_chain! {
71-
if let StmtKind::Semi(expr, ..) = &stmt.kind;
72-
// make sure it's a break, otherwise we want to skip
73-
if let ExprKind::Break(.., Some(break_expr)) = &expr.kind;
74-
then {
75-
lint(cx, expr.span, break_expr.span, LINT_BREAK);
76-
}
77-
}
78-
}
79-
},
80-
// use `return` instead of `break`
81-
ExprKind::Break(.., break_expr) => {
82-
if let Some(break_expr) = break_expr {
83-
lint(cx, expr.span, break_expr.span, LINT_BREAK);
102+
ExprKind::Block(
103+
Block {
104+
expr: Some(block_expr), ..
105+
},
106+
_,
107+
) => lint_implicit_returns(
108+
cx,
109+
block_expr,
110+
ctxt,
111+
call_site_span.or_else(|| get_call_site(block_expr.span, ctxt)),
112+
)
113+
.still_parent(call_site_span.is_some()),
114+
115+
ExprKind::If(_, then_expr, Some(else_expr)) => {
116+
// Both `then_expr` or `else_expr` are required to be blocks in the same context as the `if`. Don't
117+
// bother checking.
118+
let res = lint_implicit_returns(cx, then_expr, ctxt, call_site_span).still_parent(call_site_span.is_some());
119+
if res.is_parent() {
120+
// The return was added as a parent of this if expression.
121+
return res;
84122
}
123+
lint_implicit_returns(cx, else_expr, ctxt, call_site_span).still_parent(call_site_span.is_some())
85124
},
86-
ExprKind::If(.., if_expr, else_expr) => {
87-
expr_match(cx, if_expr);
88125

89-
if let Some(else_expr) = else_expr {
90-
expr_match(cx, else_expr);
126+
ExprKind::Match(_, arms, _) => {
127+
for arm in arms {
128+
let res = lint_implicit_returns(
129+
cx,
130+
arm.body,
131+
ctxt,
132+
call_site_span.or_else(|| get_call_site(arm.body.span, ctxt)),
133+
)
134+
.still_parent(call_site_span.is_some());
135+
if res.is_parent() {
136+
// The return was added as a parent of this match expression.
137+
return res;
138+
}
91139
}
140+
LintLocation::Inner
92141
},
93-
ExprKind::Match(.., arms, source) => {
94-
let check_all_arms = match source {
95-
MatchSource::IfLetDesugar {
96-
contains_else_clause: has_else,
97-
} => has_else,
98-
_ => true,
99-
};
100-
101-
if check_all_arms {
102-
for arm in arms {
103-
expr_match(cx, arm.body);
142+
143+
ExprKind::Loop(block, ..) => {
144+
let mut add_return = false;
145+
visit_break_exprs(block, |break_expr, dest, sub_expr| {
146+
if dest.target_id.ok() == Some(expr.hir_id) {
147+
if call_site_span.is_none() && break_expr.span.ctxt() == ctxt {
148+
lint_break(cx, break_expr.span, sub_expr.unwrap().span);
149+
} else {
150+
// the break expression is from a macro call, add a return to the loop
151+
add_return = true;
152+
}
153+
}
154+
});
155+
if add_return {
156+
#[allow(clippy::option_if_let_else)]
157+
if let Some(span) = call_site_span {
158+
lint_return(cx, span);
159+
LintLocation::Parent
160+
} else {
161+
lint_return(cx, expr.span);
162+
LintLocation::Inner
104163
}
105164
} else {
106-
expr_match(cx, arms.first().expect("`if let` doesn't have a single arm").body);
165+
LintLocation::Inner
107166
}
108167
},
109-
// skip if it already has a return statement
110-
ExprKind::Ret(..) => (),
111-
// make sure it's not a call that panics
112-
ExprKind::Call(expr, ..) => {
113-
if_chain! {
114-
if let ExprKind::Path(qpath) = &expr.kind;
115-
if let Some(path_def_id) = cx.qpath_res(qpath, expr.hir_id).opt_def_id();
116-
if match_panic_def_id(cx, path_def_id);
117-
then { }
118-
else {
119-
lint(cx, expr.span, expr.span, LINT_RETURN)
120-
}
168+
169+
// If expressions without an else clause, and blocks without a final expression can only be the final expression
170+
// if they are divergent, or return the unit type.
171+
ExprKind::If(_, _, None) | ExprKind::Block(Block { expr: None, .. }, _) | ExprKind::Ret(_) => {
172+
LintLocation::Inner
173+
},
174+
175+
// Any divergent expression doesn't need a return statement.
176+
ExprKind::MethodCall(..)
177+
| ExprKind::Call(..)
178+
| ExprKind::Binary(..)
179+
| ExprKind::Unary(..)
180+
| ExprKind::Index(..)
181+
if cx.typeck_results().expr_ty(expr).is_never() =>
182+
{
183+
LintLocation::Inner
184+
},
185+
186+
_ =>
187+
{
188+
#[allow(clippy::option_if_let_else)]
189+
if let Some(span) = call_site_span {
190+
lint_return(cx, span);
191+
LintLocation::Parent
192+
} else {
193+
lint_return(cx, expr.span);
194+
LintLocation::Inner
121195
}
122196
},
123-
// everything else is missing `return`
124-
_ => lint(cx, expr.span, expr.span, LINT_RETURN),
125197
}
126198
}
127199

128200
impl<'tcx> LateLintPass<'tcx> for ImplicitReturn {
129201
fn check_fn(
130202
&mut self,
131203
cx: &LateContext<'tcx>,
132-
_: FnKind<'tcx>,
133-
_: &'tcx FnDecl<'_>,
204+
kind: FnKind<'tcx>,
205+
decl: &'tcx FnDecl<'_>,
134206
body: &'tcx Body<'_>,
135207
span: Span,
136208
_: HirId,
137209
) {
138-
if span.from_expansion() {
210+
if (!matches!(kind, FnKind::Closure) && matches!(decl.output, FnRetTy::DefaultReturn(_)))
211+
|| span.ctxt() != body.value.span.ctxt()
212+
|| in_external_macro(cx.sess(), span)
213+
{
139214
return;
140215
}
141-
let body = cx.tcx.hir().body(body.id());
142-
if cx.typeck_results().expr_ty(&body.value).is_unit() {
216+
217+
let res_ty = cx.typeck_results().expr_ty(&body.value);
218+
if res_ty.is_unit() || res_ty.is_never() {
143219
return;
144220
}
145-
expr_match(cx, &body.value);
221+
222+
lint_implicit_returns(cx, &body.value, body.value.span.ctxt(), None);
146223
}
147224
}

clippy_utils/src/source.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -280,24 +280,33 @@ pub fn snippet_with_context(
280280
default: &'a str,
281281
applicability: &mut Applicability,
282282
) -> (Cow<'a, str>, bool) {
283-
let outer_span = hygiene::walk_chain(span, outer);
284-
let (span, is_macro_call) = if outer_span.ctxt() == outer {
285-
(outer_span, span.ctxt() != outer)
286-
} else {
287-
// The span is from a macro argument, and the outer context is the macro using the argument
288-
if *applicability != Applicability::Unspecified {
289-
*applicability = Applicability::MaybeIncorrect;
290-
}
291-
// TODO: get the argument span.
292-
(span, false)
293-
};
283+
let (span, is_macro_call) = walk_span_to_context(span, outer).map_or_else(
284+
|| {
285+
// The span is from a macro argument, and the outer context is the macro using the argument
286+
if *applicability != Applicability::Unspecified {
287+
*applicability = Applicability::MaybeIncorrect;
288+
}
289+
// TODO: get the argument span.
290+
(span, false)
291+
},
292+
|outer_span| (outer_span, span.ctxt() != outer),
293+
);
294294

295295
(
296296
snippet_with_applicability(cx, span, default, applicability),
297297
is_macro_call,
298298
)
299299
}
300300

301+
/// Walks the span up to the target context, thereby returning the macro call site if the span is
302+
/// inside a macro expansion, or the original span if it is not. Note this will return `None` in the
303+
/// case of the span being in a macro expansion, but the target context is from expanding a macro
304+
/// argument.
305+
pub fn walk_span_to_context(span: Span, outer: SyntaxContext) -> Option<Span> {
306+
let outer_span = hygiene::walk_chain(span, outer);
307+
(outer_span.ctxt() == outer).then(|| outer_span)
308+
}
309+
301310
/// Removes block comments from the given `Vec` of lines.
302311
///
303312
/// # Examples

clippy_utils/src/visitors.rs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::path_to_local_id;
22
use rustc_hir as hir;
3-
use rustc_hir::intravisit::{self, walk_expr, NestedVisitorMap, Visitor};
4-
use rustc_hir::{Arm, Body, Expr, HirId, Stmt};
3+
use rustc_hir::intravisit::{self, walk_expr, ErasedMap, NestedVisitorMap, Visitor};
4+
use rustc_hir::{Arm, Block, Body, Destination, Expr, ExprKind, HirId, Stmt};
55
use rustc_lint::LateContext;
66
use rustc_middle::hir::map::Map;
77

@@ -188,3 +188,54 @@ impl<'v> Visitor<'v> for LocalUsedVisitor<'v> {
188188
NestedVisitorMap::OnlyBodies(self.hir)
189189
}
190190
}
191+
192+
pub trait Visitable<'tcx> {
193+
fn visit<V: Visitor<'tcx>>(self, v: &mut V);
194+
}
195+
impl Visitable<'tcx> for &'tcx Expr<'tcx> {
196+
fn visit<V: Visitor<'tcx>>(self, v: &mut V) {
197+
v.visit_expr(self)
198+
}
199+
}
200+
impl Visitable<'tcx> for &'tcx Block<'tcx> {
201+
fn visit<V: Visitor<'tcx>>(self, v: &mut V) {
202+
v.visit_block(self)
203+
}
204+
}
205+
impl<'tcx> Visitable<'tcx> for &'tcx Stmt<'tcx> {
206+
fn visit<V: Visitor<'tcx>>(self, v: &mut V) {
207+
v.visit_stmt(self)
208+
}
209+
}
210+
impl<'tcx> Visitable<'tcx> for &'tcx Body<'tcx> {
211+
fn visit<V: Visitor<'tcx>>(self, v: &mut V) {
212+
v.visit_body(self)
213+
}
214+
}
215+
impl<'tcx> Visitable<'tcx> for &'tcx Arm<'tcx> {
216+
fn visit<V: Visitor<'tcx>>(self, v: &mut V) {
217+
v.visit_arm(self)
218+
}
219+
}
220+
221+
pub fn visit_break_exprs<'tcx>(
222+
node: impl Visitable<'tcx>,
223+
f: impl FnMut(&'tcx Expr<'tcx>, Destination, Option<&'tcx Expr<'tcx>>),
224+
) {
225+
struct V<F>(F);
226+
impl<'tcx, F: FnMut(&'tcx Expr<'tcx>, Destination, Option<&'tcx Expr<'tcx>>)> Visitor<'tcx> for V<F> {
227+
type Map = ErasedMap<'tcx>;
228+
fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
229+
NestedVisitorMap::None
230+
}
231+
232+
fn visit_expr(&mut self, e: &'tcx Expr<'_>) {
233+
if let ExprKind::Break(dest, sub_expr) = e.kind {
234+
self.0(e, dest, sub_expr)
235+
}
236+
walk_expr(self, e);
237+
}
238+
}
239+
240+
node.visit(&mut V(f));
241+
}

0 commit comments

Comments
 (0)