Skip to content

Commit 39df31c

Browse files
Rewrite redundant_else as a late pass. (#17329)
The main behaviour changes are: * Lint on blocks returning `!`. This greatly increases the number of cases this lints on. * Don't lint inside a macro when the divergence comes from a different macro. * Suggest adding a semicolon when syntactically needed. The old test file was a bit of a mess so it was rewritten. This was done as the final commit and the new code passes both the old and the new tests. changelog: [`redundant_else`]: Take into account divergent function calls.
2 parents e23151c + 322f4dc commit 39df31c

8 files changed

Lines changed: 1543 additions & 410 deletions

File tree

clippy_lints/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,6 @@ rustc_lint::early_lint_methods!(
509509
MiscEarlyLints: misc_early::MiscEarlyLints = misc_early::MiscEarlyLints,
510510
UnusedUnit: unused_unit::UnusedUnit = unused_unit::UnusedUnit,
511511
Precedence: precedence::Precedence = precedence::Precedence,
512-
RedundantElse: redundant_else::RedundantElse = redundant_else::RedundantElse,
513512
NeedlessArbitrarySelfType: needless_arbitrary_self_type::NeedlessArbitrarySelfType = needless_arbitrary_self_type::NeedlessArbitrarySelfType,
514513
LiteralDigitGrouping: literal_representation::LiteralDigitGrouping = literal_representation::LiteralDigitGrouping::new(conf),
515514
DecimalLiteralRepresentation: literal_representation::DecimalLiteralRepresentation = literal_representation::DecimalLiteralRepresentation::new(conf),
@@ -862,6 +861,7 @@ rustc_lint::late_lint_methods!(
862861
ManualAssertEq: manual_assert_eq::ManualAssertEq = manual_assert_eq::ManualAssertEq,
863862
WithCapacityZero: with_capacity_zero::WithCapacityZero = with_capacity_zero::WithCapacityZero,
864863
RefPatterns: ref_patterns::RefPatterns = ref_patterns::RefPatterns,
864+
RedundantElse: redundant_else::RedundantElse = redundant_else::RedundantElse,
865865
// add late passes here, used by `cargo dev new_lint`
866866
]]
867867
);

clippy_lints/src/needless_borrows_for_generic_args.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,9 +356,8 @@ fn is_mixed_projection_predicate<'tcx>(
356356
},
357357
}
358358
}
359-
} else {
360-
false
361359
}
360+
false
362361
}
363362

364363
fn referent_used_exactly_once<'tcx>(

clippy_lints/src/redundant_else.rs

Lines changed: 133 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
use clippy_utils::diagnostics::span_lint_and_sugg;
2-
use clippy_utils::source::{indent_of, reindent_multiline, snippet};
3-
use rustc_ast::ast::{Block, Expr, ExprKind, Stmt, StmtKind};
4-
use rustc_ast::visit::{Visitor, walk_expr};
1+
use clippy_utils::diagnostics::span_lint_hir_and_then;
2+
use clippy_utils::is_from_proc_macro;
3+
use clippy_utils::source::{SpanExt, indent_of, reindent_multiline};
54
use rustc_errors::Applicability;
6-
use rustc_lint::{EarlyContext, EarlyLintPass, LintContext};
5+
use rustc_hir::{Block, Expr, ExprKind, MatchSource, Stmt, StmtKind};
6+
use rustc_lint::{LateContext, LateLintPass};
7+
use rustc_middle::ty::TypeckResults;
78
use rustc_session::declare_lint_pass;
8-
use rustc_span::Span;
9+
use rustc_span::{ExpnKind, SyntaxContext};
910

1011
declare_clippy_lint! {
1112
/// ### What it does
@@ -46,125 +47,147 @@ declare_clippy_lint! {
4647

4748
declare_lint_pass!(RedundantElse => [REDUNDANT_ELSE]);
4849

49-
impl EarlyLintPass for RedundantElse {
50-
fn check_stmt(&mut self, cx: &EarlyContext<'_>, stmt: &Stmt) {
51-
if stmt.span.in_external_macro(cx.sess().source_map()) {
52-
return;
53-
}
54-
// Only look at expressions that are a whole statement
55-
let expr: &Expr = match &stmt.kind {
56-
StmtKind::Expr(expr) | StmtKind::Semi(expr) => expr,
57-
_ => return,
58-
};
59-
// if else
60-
let (mut then, mut els): (&Block, &Expr) = match &expr.kind {
61-
ExprKind::If(_, then, Some(els)) => (then, els),
62-
_ => return,
63-
};
64-
loop {
65-
if !BreakVisitor::default().check_block(then) {
66-
// then block does not always break
67-
return;
68-
}
69-
match &els.kind {
70-
// else if else
71-
ExprKind::If(_, next_then, Some(next_els)) => {
72-
then = next_then;
73-
els = next_els;
74-
},
75-
// else if without else
76-
ExprKind::If(..) => return,
77-
// done
78-
_ => break,
79-
}
50+
impl<'tcx> LateLintPass<'tcx> for RedundantElse {
51+
fn check_block_post(&mut self, cx: &LateContext<'tcx>, b: &'tcx Block<'_>) {
52+
if let Some(e) = b.expr {
53+
check(cx, b.span.ctxt(), false, e);
8054
}
55+
}
8156

82-
let mut app = Applicability::MachineApplicable;
83-
if let ExprKind::Block(block, _) = &els.kind {
84-
for stmt in &block.stmts {
85-
// If the `else` block contains a local binding or a macro invocation, Clippy shouldn't auto-fix it
86-
if matches!(&stmt.kind, StmtKind::Let(_) | StmtKind::MacCall(_)) {
87-
app = Applicability::Unspecified;
88-
break;
89-
}
90-
}
57+
fn check_stmt(&mut self, cx: &LateContext<'tcx>, s: &'tcx Stmt<'_>) {
58+
if let StmtKind::Expr(e) | StmtKind::Semi(e) = s.kind {
59+
check(cx, s.span.ctxt(), matches!(s.kind, StmtKind::Expr(_)), e);
9160
}
92-
93-
// FIXME: The indentation of the suggestion would be the same as the one of the macro invocation in this implementation, see https://github.com/rust-lang/rust-clippy/pull/13936#issuecomment-2569548202
94-
span_lint_and_sugg(
95-
cx,
96-
REDUNDANT_ELSE,
97-
els.span.with_lo(then.span.hi()),
98-
"redundant else block",
99-
"remove the `else` block and move the contents out",
100-
make_sugg(cx, els.span, "..", Some(expr.span)),
101-
app,
102-
);
10361
}
10462
}
10563

106-
/// Call `check` functions to check if an expression always breaks control flow
107-
#[derive(Default)]
108-
struct BreakVisitor {
109-
is_break: bool,
110-
}
111-
112-
impl<'ast> Visitor<'ast> for BreakVisitor {
113-
fn visit_block(&mut self, block: &'ast Block) {
114-
self.is_break = match block.stmts.as_slice() {
115-
[.., last] => self.check_stmt(last),
116-
_ => false,
117-
};
118-
}
119-
120-
fn visit_expr(&mut self, expr: &'ast Expr) {
121-
self.is_break = match expr.kind {
122-
ExprKind::Break(..) | ExprKind::Continue(..) | ExprKind::Ret(..) => true,
123-
ExprKind::Match(_, ref arms, _) => arms.iter().all(|arm|
124-
arm.body.is_none() || arm.body.as_deref().is_some_and(|body| self.check_expr(body))
125-
),
126-
ExprKind::If(_, ref then, Some(ref els)) => self.check_block(then) && self.check_expr(els),
127-
ExprKind::If(_, _, None)
128-
// ignore loops for simplicity
129-
| ExprKind::While(..) | ExprKind::ForLoop { .. } | ExprKind::Loop(..) => false,
130-
_ => {
131-
walk_expr(self, expr);
132-
return;
64+
fn check<'tcx>(cx: &LateContext<'tcx>, ctxt: SyntaxContext, needs_semi: bool, e: &'tcx Expr<'_>) {
65+
// Find the final `else` block in an `if` chain.
66+
let mut prev_then = None;
67+
let mut next = e;
68+
let (then, else_) = loop {
69+
match next.kind {
70+
ExprKind::If(_, then, Some(else_))
71+
if is_never(cx.typeck_results(), ctxt, then)
72+
&& then.span.ctxt() == ctxt
73+
&& else_.span.ctxt() == ctxt =>
74+
{
75+
prev_then = Some(then);
76+
next = else_;
13377
},
134-
};
135-
}
136-
}
137-
138-
impl BreakVisitor {
139-
fn check<T>(&mut self, item: T, visit: fn(&mut Self, T)) -> bool {
140-
visit(self, item);
141-
std::mem::replace(&mut self.is_break, false)
142-
}
78+
ExprKind::Block(b, _) if let Some(then) = prev_then => break (then, b),
79+
_ => return,
80+
}
81+
};
14382

144-
fn check_block(&mut self, block: &Block) -> bool {
145-
self.check(block, Self::visit_block)
83+
if e.span.ctxt() == ctxt
84+
&& !ctxt.in_external_macro(cx.tcx.sess.source_map())
85+
&& !is_from_proc_macro(cx, e)
86+
&& let Some(src) = else_.span.get_text(cx)
87+
&& let Some(src) = src.strip_prefix('{')
88+
&& let Some(src) = src.strip_suffix('}')
89+
// FIXME(@Jarcho): `indent_of` walks to the root context before getting the indent
90+
// which gives the wrong result here.
91+
&& let Some(indent) = indent_of(cx, e.span)
92+
{
93+
let sp = else_.span.with_lo(then.span.hi());
94+
span_lint_hir_and_then(cx, REDUNDANT_ELSE, e.hir_id, sp, "redundant else block", |diag| {
95+
let mut sugg = reindent_multiline(src.trim_end(), false, Some(indent));
96+
if needs_semi && else_.expr.is_some_and(|e| expr_needs_semi(ctxt, e)) {
97+
sugg.push(';');
98+
}
99+
diag.span_suggestion(
100+
sp,
101+
"remove the `else` block and move the contents out",
102+
sugg,
103+
if ctxt.is_root() && else_.stmts.iter().all(|s| !matches!(s.kind, StmtKind::Let(_))) {
104+
Applicability::MachineApplicable
105+
} else {
106+
Applicability::MaybeIncorrect
107+
},
108+
);
109+
});
146110
}
111+
}
147112

148-
fn check_expr(&mut self, expr: &Expr) -> bool {
149-
self.check(expr, Self::visit_expr)
113+
/// Checks if an expression, viewed from the specified context, needs a trailing semicolon
114+
/// to be parsed as a statement.
115+
fn expr_needs_semi(ctxt: SyntaxContext, e: &Expr<'_>) -> bool {
116+
match e.kind {
117+
ExprKind::Block(..) | ExprKind::Loop(..) | ExprKind::Match(..) | ExprKind::If(..) if ctxt == e.span.ctxt() => {
118+
false
119+
},
120+
ExprKind::Loop(..) => {
121+
let expn = e.span.ctxt().outer_expn_data();
122+
ctxt != expn.call_site.ctxt() || !matches!(expn.kind, ExpnKind::Desugaring(_))
123+
},
124+
ExprKind::Match(_, _, MatchSource::ForLoopDesugar) => ctxt != e.span.ctxt().outer_expn_data().call_site.ctxt(),
125+
_ => true,
150126
}
127+
}
151128

152-
fn check_stmt(&mut self, stmt: &Stmt) -> bool {
153-
self.check(stmt, Self::visit_stmt)
129+
fn is_never(typeck: &TypeckResults<'_>, ctxt: SyntaxContext, e: &Expr<'_>) -> bool {
130+
if ctxt.is_root() {
131+
is_never_root(typeck, e)
132+
} else {
133+
is_never_mac(ctxt, e)
154134
}
155135
}
156136

157-
// Extract the inner contents of an `else` block str
158-
// e.g. `{ foo(); bar(); }` -> `foo(); bar();`
159-
fn extract_else_block(mut block: &str) -> String {
160-
block = block.strip_prefix("{").unwrap_or(block);
161-
block = block.strip_suffix("}").unwrap_or(block);
162-
block.trim_end().to_string()
137+
fn is_never_root(typeck: &TypeckResults<'_>, e: &Expr<'_>) -> bool {
138+
match e.kind {
139+
ExprKind::Break(..) | ExprKind::Continue(_) | ExprKind::Ret(_) | ExprKind::Become(..) => true,
140+
ExprKind::DropTemps(e) => is_never_root(typeck, e),
141+
ExprKind::Block(b, _)
142+
if let Some(e) = b.expr
143+
&& !b.targeted_by_break =>
144+
{
145+
is_never_root(typeck, e)
146+
},
147+
ExprKind::Match(_, arms, _) => arms.iter().all(|a| is_never_root(typeck, a.body)),
148+
ExprKind::If(_, then, Some(else_)) => is_never_root(typeck, then) && is_never_root(typeck, else_),
149+
ExprKind::Call(..)
150+
| ExprKind::MethodCall(..)
151+
| ExprKind::Binary(..)
152+
| ExprKind::Unary(..)
153+
| ExprKind::Block(..)
154+
| ExprKind::Loop(..)
155+
| ExprKind::Path(_) => typeck.expr_ty(e).is_never(),
156+
_ => false,
157+
}
163158
}
164159

165-
fn make_sugg(cx: &EarlyContext<'_>, els_span: Span, default: &str, indent_relative_to: Option<Span>) -> String {
166-
let extracted = extract_else_block(&snippet(cx, els_span, default));
167-
let indent = indent_relative_to.and_then(|s| indent_of(cx, s));
168-
169-
reindent_multiline(&extracted, false, indent)
160+
fn is_never_mac(ctxt: SyntaxContext, mut e: &Expr<'_>) -> bool {
161+
loop {
162+
let next = match e.kind {
163+
ExprKind::Break(..) | ExprKind::Continue(_) | ExprKind::Ret(_) | ExprKind::Become(..) => return true,
164+
ExprKind::DropTemps(e) => e,
165+
ExprKind::Block(b, _)
166+
if !b.targeted_by_break
167+
&& let Some(e) = match (b.expr, b.stmts) {
168+
(Some(e), _) => Some(e),
169+
(None, [.., s]) if let StmtKind::Expr(e) | StmtKind::Semi(e) = s.kind => Some(e),
170+
_ => None,
171+
}
172+
&& ctxt == b.span.ctxt() =>
173+
{
174+
e
175+
},
176+
ExprKind::Match(_, arms, _) => {
177+
return arms
178+
.iter()
179+
.all(|a| ctxt == a.span.ctxt() && ctxt == a.body.span.ctxt() && is_never_mac(ctxt, a.body));
180+
},
181+
ExprKind::If(_, then, Some(else_))
182+
if ctxt == then.span.ctxt() && ctxt == else_.span.ctxt() && is_never_mac(ctxt, then) =>
183+
{
184+
else_
185+
},
186+
_ => return false,
187+
};
188+
if ctxt != next.span.ctxt() {
189+
return false;
190+
}
191+
e = next;
192+
}
170193
}

src/main.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,8 @@ pub fn main() {
4242
process::exit(clippy_lints::explain(
4343
&lint.strip_prefix("clippy::").unwrap_or(&lint).replace('-', "_"),
4444
));
45-
} else {
46-
show_help();
4745
}
46+
show_help();
4847
return;
4948
}
5049

tests/integration.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,13 @@ fn integration_test() {
102102
panic!("incompatible crate versions");
103103
} else if stderr.contains("failed to run `rustc` to learn about target-specific information") {
104104
panic!("couldn't find librustc_driver, consider setting `LD_LIBRARY_PATH`");
105-
} else {
106-
assert!(
107-
!stderr.contains("toolchain") || !stderr.contains("is not installed"),
108-
"missing required toolchain"
109-
);
110105
}
111106

107+
assert!(
108+
!stderr.contains("toolchain") || !stderr.contains("is not installed"),
109+
"missing required toolchain"
110+
);
111+
112112
match output.status.code() {
113113
Some(0) => println!("Compilation successful"),
114114
Some(code) => eprintln!("Compilation failed. Exit code: {code}"),

0 commit comments

Comments
 (0)