Skip to content

Commit b015d5e

Browse files
committed
Add lint to tell about let else pattern
1 parent 5b09d4e commit b015d5e

File tree

8 files changed

+456
-1
lines changed

8 files changed

+456
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3997,6 +3997,7 @@ Released 2018-09-13
39973997
[`manual_find_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_find_map
39983998
[`manual_flatten`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_flatten
39993999
[`manual_instant_elapsed`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_instant_elapsed
4000+
[`manual_let_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_let_else
40004001
[`manual_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_map
40014002
[`manual_memcpy`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_memcpy
40024003
[`manual_non_exhaustive`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_non_exhaustive

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ mod manual_async_fn;
170170
mod manual_bits;
171171
mod manual_clamp;
172172
mod manual_instant_elapsed;
173+
mod manual_let_else;
173174
mod manual_non_exhaustive;
174175
mod manual_rem_euclid;
175176
mod manual_retain;
@@ -603,6 +604,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
603604
))
604605
});
605606
store.register_late_pass(move |_| Box::new(matches::Matches::new(msrv)));
607+
store.register_late_pass(move |_| Box::new(manual_let_else::ManualLetElse::new(msrv)));
606608
store.register_early_pass(move || Box::new(manual_non_exhaustive::ManualNonExhaustiveStruct::new(msrv)));
607609
store.register_late_pass(move |_| Box::new(manual_non_exhaustive::ManualNonExhaustiveEnum::new(msrv)));
608610
store.register_late_pass(move |_| Box::new(manual_strip::ManualStrip::new(msrv)));

clippy_lints/src/manual_let_else.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
use clippy_utils::diagnostics::span_lint;
2+
use clippy_utils::visitors::{for_each_expr, Descend};
3+
use clippy_utils::{higher, meets_msrv, msrvs, peel_blocks};
4+
use if_chain::if_chain;
5+
use rustc_hir::{Expr, ExprKind, Pat, QPath, Stmt, StmtKind};
6+
use rustc_lint::{LateContext, LateLintPass, LintContext};
7+
use rustc_middle::lint::in_external_macro;
8+
use rustc_semver::RustcVersion;
9+
use rustc_session::{declare_tool_lint, impl_lint_pass};
10+
use std::ops::ControlFlow;
11+
12+
declare_clippy_lint! {
13+
/// ### What it does
14+
///
15+
/// Warn of cases where `let...else` could be used
16+
///
17+
/// ### Why is this bad?
18+
///
19+
/// `let...else` provides a standard construct for this pattern
20+
/// that people can easily recognize. It's also more compact.
21+
///
22+
/// ### Example
23+
///
24+
/// ```rust
25+
/// # let w = Some(0);
26+
/// let v = if let Some(v) = w { v } else { return };
27+
/// ```
28+
///
29+
/// Could be written:
30+
///
31+
/// ```rust
32+
/// # #![feature(let_else)]
33+
/// # fn main () {
34+
/// # let w = Some(0);
35+
/// let Some(v) = w else { return };
36+
/// # }
37+
/// ```
38+
#[clippy::version = "1.67.0"]
39+
pub MANUAL_LET_ELSE,
40+
pedantic,
41+
"manual implementation of a let...else statement"
42+
}
43+
44+
pub struct ManualLetElse {
45+
msrv: Option<RustcVersion>,
46+
}
47+
48+
impl ManualLetElse {
49+
#[must_use]
50+
pub fn new(msrv: Option<RustcVersion>) -> Self {
51+
Self { msrv }
52+
}
53+
}
54+
55+
impl_lint_pass!(ManualLetElse => [MANUAL_LET_ELSE]);
56+
57+
impl<'tcx> LateLintPass<'tcx> for ManualLetElse {
58+
fn check_stmt(&mut self, cx: &LateContext<'_>, stmt: &'tcx Stmt<'tcx>) {
59+
if !meets_msrv(self.msrv, msrvs::LET_ELSE) {
60+
return;
61+
}
62+
63+
if in_external_macro(cx.sess(), stmt.span) {
64+
return;
65+
}
66+
67+
if_chain! {
68+
if let StmtKind::Local(local) = stmt.kind;
69+
if let Some(init) = local.init;
70+
if let Some(higher::IfLet { let_pat, let_expr: _, if_then, if_else }) = higher::IfLet::hir(cx, init);
71+
if if_then_simple_identity(let_pat, if_then);
72+
if let Some(if_else) = if_else;
73+
if expr_diverges(cx, if_else);
74+
then {
75+
span_lint(
76+
cx,
77+
MANUAL_LET_ELSE,
78+
stmt.span,
79+
"this could be rewritten as `let else`",
80+
);
81+
}
82+
}
83+
}
84+
85+
extract_msrv_attr!(LateContext);
86+
}
87+
88+
fn expr_diverges(cx: &LateContext<'_>, expr: &'_ Expr<'_>) -> bool {
89+
fn is_never(cx: &LateContext<'_>, expr: &'_ Expr<'_>) -> bool {
90+
if let Some(ty) = cx.typeck_results().expr_ty_opt(expr) {
91+
return ty.is_never();
92+
}
93+
false
94+
}
95+
// We can't just call is_never on expr and be done, because the type system
96+
// sometimes coerces the ! type to something different before we can get
97+
// our hands on it. So instead, we do a manual search. We do fall back to
98+
// is_never in some places when there is no better alternative.
99+
for_each_expr(expr, |ex| {
100+
match ex.kind {
101+
ExprKind::Continue(_) | ExprKind::Break(_, _) | ExprKind::Ret(_) => ControlFlow::Break(()),
102+
ExprKind::Call(call, _) => {
103+
if is_never(cx, ex) || is_never(cx, call) {
104+
return ControlFlow::Break(());
105+
}
106+
ControlFlow::Continue(Descend::Yes)
107+
},
108+
ExprKind::MethodCall(..) => {
109+
if is_never(cx, ex) {
110+
return ControlFlow::Break(());
111+
}
112+
ControlFlow::Continue(Descend::Yes)
113+
},
114+
ExprKind::If(if_expr, if_then, if_else) => {
115+
let else_diverges = if_else.map_or(false, |ex| expr_diverges(cx, ex));
116+
let diverges = expr_diverges(cx, if_expr) || (else_diverges && expr_diverges(cx, if_then));
117+
if diverges {
118+
return ControlFlow::Break(());
119+
}
120+
ControlFlow::Continue(Descend::No)
121+
},
122+
ExprKind::Match(match_expr, match_arms, _) => {
123+
let diverges =
124+
expr_diverges(cx, match_expr) || match_arms.iter().all(|arm| expr_diverges(cx, arm.body));
125+
if diverges {
126+
return ControlFlow::Break(());
127+
}
128+
ControlFlow::Continue(Descend::No)
129+
},
130+
131+
// Don't continue into loops or labeled blocks, as they are breakable,
132+
// and we'd have to start checking labels.
133+
ExprKind::Block(_, Some(_)) | ExprKind::Loop(..) => ControlFlow::Continue(Descend::No),
134+
135+
// Default: descend
136+
_ => ControlFlow::Continue(Descend::Yes),
137+
}
138+
})
139+
.is_some()
140+
}
141+
142+
/// Checks if the passed `if_then` is a simple identity
143+
fn if_then_simple_identity(let_pat: &'_ Pat<'_>, if_then: &'_ Expr<'_>) -> bool {
144+
// TODO support patterns with multiple bindings and tuples, like:
145+
// let (foo, bar) = if let (Some(foo), bar) = g() { (foo, bar) } else { ... }
146+
if_chain! {
147+
if let ExprKind::Path(QPath::Resolved(_ty, path)) = &peel_blocks(if_then).kind;
148+
if let [path_seg] = path.segments;
149+
then {
150+
let mut pat_bindings = Vec::new();
151+
let_pat.each_binding(|_ann, _hir_id, _sp, ident| {
152+
pat_bindings.push(ident);
153+
});
154+
if let [binding] = &pat_bindings[..] {
155+
return path_seg.ident == *binding;
156+
}
157+
}
158+
}
159+
false
160+
}

clippy_lints/src/utils/conf.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ define_Conf! {
213213
///
214214
/// Suppress lints whenever the suggested change would cause breakage for other crates.
215215
(avoid_breaking_exported_api: bool = true),
216-
/// Lint: MANUAL_SPLIT_ONCE, MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE, APPROX_CONSTANT, DEPRECATED_CFG_ATTR, INDEX_REFUTABLE_SLICE, MAP_CLONE, BORROW_AS_PTR, MANUAL_BITS, ERR_EXPECT, CAST_ABS_TO_UNSIGNED, UNINLINED_FORMAT_ARGS, MANUAL_CLAMP.
216+
/// Lint: MANUAL_SPLIT_ONCE, MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE, APPROX_CONSTANT, DEPRECATED_CFG_ATTR, INDEX_REFUTABLE_SLICE, MAP_CLONE, BORROW_AS_PTR, MANUAL_BITS, ERR_EXPECT, CAST_ABS_TO_UNSIGNED, UNINLINED_FORMAT_ARGS, MANUAL_CLAMP, MANUAL_LET_ELSE.
217217
///
218218
/// The minimum rust version that the project supports
219219
(msrv: Option<String> = None),

clippy_utils/src/msrvs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ macro_rules! msrv_aliases {
1212

1313
// names may refer to stabilized feature flags or library items
1414
msrv_aliases! {
15+
1,65,0 { LET_ELSE }
1516
1,62,0 { BOOL_THEN_SOME }
1617
1,58,0 { FORMAT_ARGS_CAPTURE }
1718
1,53,0 { OR_PATTERNS, MANUAL_BITS, BTREE_MAP_RETAIN, BTREE_SET_RETAIN, ARRAY_INTO_ITERATOR }

src/docs/manual_let_else.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
### What it does
2+
3+
Warn of cases where `let...else` could be used
4+
5+
### Why is this bad?
6+
7+
`let...else` provides a standard construct for this pattern
8+
that people can easily recognize. It's also more compact.
9+
10+
### Example
11+
12+
```
13+
let v = if let Some(v) = w { v } else { return };
14+
```
15+
16+
Could be written:
17+
18+
```
19+
let Some(v) = w else { return };
20+
```

tests/ui/manual_let_else.rs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#![allow(unused_braces, unused_variables, dead_code)]
2+
#![allow(
3+
clippy::collapsible_else_if,
4+
clippy::unused_unit,
5+
clippy::never_loop,
6+
clippy::let_unit_value
7+
)]
8+
#![warn(clippy::manual_let_else)]
9+
10+
fn g() -> Option<()> {
11+
None
12+
}
13+
14+
fn main() {}
15+
16+
fn fire() {
17+
let v = if let Some(v_some) = g() { v_some } else { return };
18+
let v = if let Some(v_some) = g() {
19+
v_some
20+
} else {
21+
return;
22+
};
23+
24+
let v = if let Some(v) = g() {
25+
// Blocks around the identity should have no impact
26+
{
27+
{ v }
28+
}
29+
} else {
30+
// Some computation should still make it fire
31+
g();
32+
return;
33+
};
34+
35+
// continue and break diverge
36+
loop {
37+
let v = if let Some(v_some) = g() { v_some } else { continue };
38+
let v = if let Some(v_some) = g() { v_some } else { break };
39+
}
40+
41+
// panic also diverges
42+
let v = if let Some(v_some) = g() { v_some } else { panic!() };
43+
44+
// abort also diverges
45+
let v = if let Some(v_some) = g() {
46+
v_some
47+
} else {
48+
std::process::abort()
49+
};
50+
51+
// If whose two branches diverge also diverges
52+
let v = if let Some(v_some) = g() {
53+
v_some
54+
} else {
55+
if true { return } else { panic!() }
56+
};
57+
58+
// Top level else if
59+
let v = if let Some(v_some) = g() {
60+
v_some
61+
} else if true {
62+
return;
63+
} else {
64+
panic!("diverge");
65+
};
66+
67+
// All match arms diverge
68+
let v = if let Some(v_some) = g() {
69+
v_some
70+
} else {
71+
match (g(), g()) {
72+
(Some(_), None) => return,
73+
(None, Some(_)) => {
74+
if true {
75+
return;
76+
} else {
77+
panic!();
78+
}
79+
},
80+
_ => return,
81+
}
82+
};
83+
84+
// Tuples supported for the declared variables
85+
let (v, w) = if let Some(v_some) = g().map(|v| (v, 42)) {
86+
v_some
87+
} else {
88+
return;
89+
};
90+
}
91+
92+
fn not_fire() {
93+
let v = if let Some(v_some) = g() {
94+
// Nothing returned. Should not fire.
95+
} else {
96+
return;
97+
};
98+
99+
let w = 0;
100+
let v = if let Some(v_some) = g() {
101+
// Different variable than v_some. Should not fire.
102+
w
103+
} else {
104+
return;
105+
};
106+
107+
let v = if let Some(v_some) = g() {
108+
// Computation in then clause. Should not fire.
109+
g();
110+
v_some
111+
} else {
112+
return;
113+
};
114+
115+
let v = if let Some(v_some) = g() {
116+
v_some
117+
} else {
118+
if false {
119+
return;
120+
}
121+
// This doesn't diverge. Should not fire.
122+
()
123+
};
124+
125+
let v = if let Some(v_some) = g() {
126+
v_some
127+
} else {
128+
// There is one match arm that doesn't diverge. Should not fire.
129+
match (g(), g()) {
130+
(Some(_), None) => return,
131+
(None, Some(_)) => return,
132+
(Some(_), Some(_)) => (),
133+
_ => return,
134+
}
135+
};
136+
137+
let v = if let Some(v_some) = g() {
138+
v_some
139+
} else {
140+
// loop with a break statement inside does not diverge.
141+
loop {
142+
break;
143+
}
144+
};
145+
146+
147+
let v = if let Some(v_some) = g() {
148+
v_some
149+
} else {
150+
enum Uninhabited {}
151+
fn un() -> Uninhabited {
152+
panic!()
153+
}
154+
// Don't lint if the type is uninhabited but not !
155+
un()
156+
};
157+
158+
fn question_mark() -> Option<()> {
159+
let v = if let Some(v) = g() {
160+
v
161+
} else {
162+
// Question mark does not diverge
163+
g()?
164+
};
165+
Some(v)
166+
}
167+
}

0 commit comments

Comments
 (0)