Skip to content

Commit 1388864

Browse files
committed
Add lint to tell about let else pattern
1 parent 02f3c17 commit 1388864

File tree

9 files changed

+435
-1
lines changed

9 files changed

+435
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3249,6 +3249,7 @@ Released 2018-09-13
32493249
[`manual_filter_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_filter_map
32503250
[`manual_find_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_find_map
32513251
[`manual_flatten`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_flatten
3252+
[`manual_let_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_let_else
32523253
[`manual_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_map
32533254
[`manual_memcpy`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_memcpy
32543255
[`manual_non_exhaustive`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_non_exhaustive

clippy_lints/src/lib.register_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ store.register_lints(&[
230230
manual_assert::MANUAL_ASSERT,
231231
manual_async_fn::MANUAL_ASYNC_FN,
232232
manual_bits::MANUAL_BITS,
233+
manual_let_else::MANUAL_LET_ELSE,
233234
manual_map::MANUAL_MAP,
234235
manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE,
235236
manual_ok_or::MANUAL_OK_OR,

clippy_lints/src/lib.register_restriction.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ store.register_group(true, "clippy::restriction", Some("clippy_restriction"), ve
2626
LintId::of(integer_division::INTEGER_DIVISION),
2727
LintId::of(let_underscore::LET_UNDERSCORE_MUST_USE),
2828
LintId::of(literal_representation::DECIMAL_LITERAL_REPRESENTATION),
29+
LintId::of(manual_let_else::MANUAL_LET_ELSE),
2930
LintId::of(map_err_ignore::MAP_ERR_IGNORE),
3031
LintId::of(matches::REST_PAT_IN_FULLY_BOUND_STRUCTS),
3132
LintId::of(matches::WILDCARD_ENUM_MATCH_ARM),

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ mod main_recursion;
265265
mod manual_assert;
266266
mod manual_async_fn;
267267
mod manual_bits;
268+
mod manual_let_else;
268269
mod manual_map;
269270
mod manual_non_exhaustive;
270271
mod manual_ok_or;
@@ -570,6 +571,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
570571
store.register_late_pass(move || Box::new(matches::Matches::new(msrv)));
571572
store.register_early_pass(move || Box::new(manual_non_exhaustive::ManualNonExhaustive::new(msrv)));
572573
store.register_late_pass(move || Box::new(manual_strip::ManualStrip::new(msrv)));
574+
store.register_late_pass(move || Box::new(manual_let_else::ManualLetElse::new(msrv)));
573575
store.register_early_pass(move || Box::new(redundant_static_lifetimes::RedundantStaticLifetimes::new(msrv)));
574576
store.register_early_pass(move || Box::new(redundant_field_names::RedundantFieldNames::new(msrv)));
575577
store.register_late_pass(move || Box::new(checked_conversions::CheckedConversions::new(msrv)));

clippy_lints/src/manual_let_else.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
use clippy_utils::diagnostics::span_lint;
2+
use clippy_utils::visitors::expr_visitor_no_bodies;
3+
use clippy_utils::{higher, meets_msrv, msrvs, peel_blocks};
4+
use if_chain::if_chain;
5+
use rustc_hir::intravisit::Visitor;
6+
use rustc_hir::{Expr, ExprKind, Pat, QPath, Stmt, StmtKind};
7+
use rustc_lint::{LateContext, LateLintPass, LintContext};
8+
use rustc_middle::lint::in_external_macro;
9+
use rustc_semver::RustcVersion;
10+
use rustc_session::{declare_tool_lint, impl_lint_pass};
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` is more compact.
20+
///
21+
/// ### Example
22+
///
23+
/// ```rust
24+
/// # let w = Some(0);
25+
/// let v = if let Some(v) = w { v } else { return };
26+
/// ```
27+
///
28+
/// Could be written:
29+
///
30+
/// ```rust
31+
/// # #![feature(let_else)]
32+
/// # fn main () {
33+
/// # let w = Some(0);
34+
/// let Some(v) = w else { return };
35+
/// # }
36+
/// ```
37+
#[clippy::version = "1.60.0"]
38+
pub MANUAL_LET_ELSE,
39+
restriction,
40+
"TODO"
41+
}
42+
43+
pub struct ManualLetElse {
44+
msrv: Option<RustcVersion>,
45+
}
46+
47+
impl ManualLetElse {
48+
#[must_use]
49+
pub fn new(msrv: Option<RustcVersion>) -> Self {
50+
Self { msrv }
51+
}
52+
}
53+
54+
impl_lint_pass!(ManualLetElse => [MANUAL_LET_ELSE]);
55+
56+
impl<'tcx> LateLintPass<'tcx> for ManualLetElse {
57+
fn check_stmt(&mut self, cx: &LateContext<'_>, stmt: &'tcx Stmt<'tcx>) {
58+
if !meets_msrv(self.msrv.as_ref(), &msrvs::LET_ELSE) {
59+
return;
60+
}
61+
62+
if in_external_macro(cx.sess(), stmt.span) {
63+
return;
64+
}
65+
66+
if_chain! {
67+
if let StmtKind::Local(local) = stmt.kind;
68+
if let Some(init) = local.init;
69+
if let Some(higher::IfLet { let_pat, let_expr: _, if_then, if_else }) = higher::IfLet::hir(cx, init);
70+
if if_then_simple_identity(let_pat, if_then);
71+
if let Some(if_else) = if_else;
72+
if expr_diverges(cx, if_else);
73+
then {
74+
span_lint(
75+
cx,
76+
MANUAL_LET_ELSE,
77+
stmt.span,
78+
"this could be rewritten as `let else`",
79+
);
80+
}
81+
}
82+
}
83+
84+
extract_msrv_attr!(LateContext);
85+
}
86+
87+
fn expr_diverges(cx: &LateContext<'_>, expr: &'_ Expr<'_>) -> bool {
88+
fn is_never(cx: &LateContext<'_>, expr: &'_ Expr<'_>) -> bool {
89+
if let Some(ty) = cx.typeck_results().expr_ty_opt(expr) {
90+
return ty.is_never();
91+
}
92+
false
93+
}
94+
let mut does_diverge = false;
95+
expr_visitor_no_bodies(|ex| {
96+
match ex.kind {
97+
ExprKind::Continue(_) | ExprKind::Break(_, _) | ExprKind::Ret(_) => {
98+
does_diverge = true;
99+
false
100+
},
101+
ExprKind::Call(call, _) => {
102+
if is_never(cx, ex) || is_never(cx, call) {
103+
does_diverge = true;
104+
return false;
105+
}
106+
true
107+
},
108+
ExprKind::MethodCall(..) => {
109+
if is_never(cx, ex) {
110+
does_diverge = true;
111+
return false;
112+
}
113+
true
114+
},
115+
ExprKind::If(if_expr, if_then, if_else) => {
116+
let else_diverges = if_else.map_or(false, |ex| expr_diverges(cx, ex));
117+
let diverges = expr_diverges(cx, if_expr) || (else_diverges && expr_diverges(cx, if_then));
118+
if diverges {
119+
does_diverge = true;
120+
}
121+
false
122+
},
123+
ExprKind::Match(match_expr, match_arms, _) => {
124+
let diverges =
125+
expr_diverges(cx, match_expr) || match_arms.iter().all(|arm| expr_diverges(cx, arm.body));
126+
if diverges {
127+
does_diverge = true;
128+
}
129+
false
130+
},
131+
132+
// Don't continue into loops, as they are breakable,
133+
// and we'd have to start checking labels.
134+
ExprKind::Loop(..) => false,
135+
136+
// Default: descend
137+
// TODO: support breakable blocks once they are stable.
138+
// For this we'd need to add label support.
139+
_ => true,
140+
}
141+
})
142+
.visit_expr(expr);
143+
does_diverge
144+
}
145+
146+
/// Checks if the passed `if_then` is a simple identity
147+
fn if_then_simple_identity(let_pat: &'_ Pat<'_>, if_then: &'_ Expr<'_>) -> bool {
148+
// TODO support patterns with multiple bindings and tuples, like:
149+
// let (foo, bar) = if let (Some(foo), bar) = g() { (foo, bar) } else { ... }
150+
if_chain! {
151+
if let ExprKind::Path(QPath::Resolved(_ty, path)) = &peel_blocks(if_then).kind;
152+
if let [path_seg] = path.segments;
153+
then {
154+
let mut pat_bindings = Vec::new();
155+
let_pat.each_binding(|_ann, _hir_id, _sp, ident| {
156+
pat_bindings.push(ident);
157+
});
158+
if let [binding] = &pat_bindings[..] {
159+
return path_seg.ident == *binding;
160+
}
161+
}
162+
}
163+
false
164+
}

clippy_lints/src/utils/conf.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ define_Conf! {
156156
///
157157
/// Suppress lints whenever the suggested change would cause breakage for other crates.
158158
(avoid_breaking_exported_api: bool = true),
159-
/// 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.
159+
/// 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, MANUAL_LET_ELSE.
160160
///
161161
/// The minimum rust version that the project supports
162162
(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,99,0 { LET_ELSE }
1516
1,53,0 { OR_PATTERNS, MANUAL_BITS }
1617
1,52,0 { STR_SPLIT_ONCE }
1718
1,51,0 { BORROW_AS_PTR }

tests/ui/manual_let_else.rs

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

0 commit comments

Comments
 (0)