Skip to content

Commit b698a15

Browse files
committed
Auto merge of #8437 - est31:let_else_lint, r=flip1995
Add lint to tell about let else pattern Adds a lint to tell the user if the let_else pattern should be used. ~~The PR is blocked probably on rustfmt support, as clippy shouldn't suggest features that aren't yet fully supported by all tools.~~ Edit: I guess adding it as a restriction lint for now is the best option, it can be turned into a style lint later. --- changelog: addition of a new lint to check for manual `let else`
2 parents 5b09d4e + dcde480 commit b698a15

15 files changed

+1016
-16
lines changed

CHANGELOG.md

+1
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/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
251251
crate::manual_bits::MANUAL_BITS_INFO,
252252
crate::manual_clamp::MANUAL_CLAMP_INFO,
253253
crate::manual_instant_elapsed::MANUAL_INSTANT_ELAPSED_INFO,
254+
crate::manual_let_else::MANUAL_LET_ELSE_INFO,
254255
crate::manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE_INFO,
255256
crate::manual_rem_euclid::MANUAL_REM_EUCLID_INFO,
256257
crate::manual_retain::MANUAL_RETAIN_INFO,

clippy_lints/src/dereference.rs

+1-3
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,7 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> {
274274
}
275275

276276
let typeck = cx.typeck_results();
277-
let (kind, sub_expr) = if let Some(x) = try_parse_ref_op(cx.tcx, typeck, expr) {
278-
x
279-
} else {
277+
let Some((kind, sub_expr)) = try_parse_ref_op(cx.tcx, typeck, expr) else {
280278
// The whole chain of reference operations has been seen
281279
if let Some((state, data)) = self.state.take() {
282280
report(cx, expr, state, data);

clippy_lints/src/lib.rs

+3
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,8 @@ 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+
let matches_for_let_else = conf.matches_for_let_else;
608+
store.register_late_pass(move |_| Box::new(manual_let_else::ManualLetElse::new(msrv, matches_for_let_else)));
606609
store.register_early_pass(move || Box::new(manual_non_exhaustive::ManualNonExhaustiveStruct::new(msrv)));
607610
store.register_late_pass(move |_| Box::new(manual_non_exhaustive::ManualNonExhaustiveEnum::new(msrv)));
608611
store.register_late_pass(move |_| Box::new(manual_strip::ManualStrip::new(msrv)));

clippy_lints/src/manual_let_else.rs

+297
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::higher::IfLetOrMatch;
3+
use clippy_utils::source::snippet_opt;
4+
use clippy_utils::ty::is_type_diagnostic_item;
5+
use clippy_utils::visitors::{for_each_expr, Descend};
6+
use clippy_utils::{meets_msrv, msrvs, peel_blocks};
7+
use if_chain::if_chain;
8+
use rustc_data_structures::fx::FxHashSet;
9+
use rustc_errors::Applicability;
10+
use rustc_hir::{Expr, ExprKind, MatchSource, Pat, PatKind, QPath, Stmt, StmtKind};
11+
use rustc_lint::{LateContext, LateLintPass, LintContext};
12+
use rustc_middle::lint::in_external_macro;
13+
use rustc_semver::RustcVersion;
14+
use rustc_session::{declare_tool_lint, impl_lint_pass};
15+
use rustc_span::symbol::sym;
16+
use rustc_span::Span;
17+
use serde::Deserialize;
18+
use std::ops::ControlFlow;
19+
20+
declare_clippy_lint! {
21+
/// ### What it does
22+
///
23+
/// Warn of cases where `let...else` could be used
24+
///
25+
/// ### Why is this bad?
26+
///
27+
/// `let...else` provides a standard construct for this pattern
28+
/// that people can easily recognize. It's also more compact.
29+
///
30+
/// ### Example
31+
///
32+
/// ```rust
33+
/// # let w = Some(0);
34+
/// let v = if let Some(v) = w { v } else { return };
35+
/// ```
36+
///
37+
/// Could be written:
38+
///
39+
/// ```rust
40+
/// # #![feature(let_else)]
41+
/// # fn main () {
42+
/// # let w = Some(0);
43+
/// let Some(v) = w else { return };
44+
/// # }
45+
/// ```
46+
#[clippy::version = "1.67.0"]
47+
pub MANUAL_LET_ELSE,
48+
pedantic,
49+
"manual implementation of a let...else statement"
50+
}
51+
52+
pub struct ManualLetElse {
53+
msrv: Option<RustcVersion>,
54+
matches_behaviour: MatchLintBehaviour,
55+
}
56+
57+
impl ManualLetElse {
58+
#[must_use]
59+
pub fn new(msrv: Option<RustcVersion>, matches_behaviour: MatchLintBehaviour) -> Self {
60+
Self {
61+
msrv,
62+
matches_behaviour,
63+
}
64+
}
65+
}
66+
67+
impl_lint_pass!(ManualLetElse => [MANUAL_LET_ELSE]);
68+
69+
impl<'tcx> LateLintPass<'tcx> for ManualLetElse {
70+
fn check_stmt(&mut self, cx: &LateContext<'_>, stmt: &'tcx Stmt<'tcx>) {
71+
let if_let_or_match = if_chain! {
72+
if meets_msrv(self.msrv, msrvs::LET_ELSE);
73+
if !in_external_macro(cx.sess(), stmt.span);
74+
if let StmtKind::Local(local) = stmt.kind;
75+
if let Some(init) = local.init;
76+
if local.els.is_none();
77+
if local.ty.is_none();
78+
if init.span.ctxt() == stmt.span.ctxt();
79+
if let Some(if_let_or_match) = IfLetOrMatch::parse(cx, init);
80+
then {
81+
if_let_or_match
82+
} else {
83+
return;
84+
}
85+
};
86+
87+
match if_let_or_match {
88+
IfLetOrMatch::IfLet(if_let_expr, let_pat, if_then, if_else) => if_chain! {
89+
if expr_is_simple_identity(let_pat, if_then);
90+
if let Some(if_else) = if_else;
91+
if expr_diverges(cx, if_else);
92+
then {
93+
emit_manual_let_else(cx, stmt.span, if_let_expr, let_pat, if_else);
94+
}
95+
},
96+
IfLetOrMatch::Match(match_expr, arms, source) => {
97+
if self.matches_behaviour == MatchLintBehaviour::Never {
98+
return;
99+
}
100+
if source != MatchSource::Normal {
101+
return;
102+
}
103+
// Any other number than two arms doesn't (neccessarily)
104+
// have a trivial mapping to let else.
105+
if arms.len() != 2 {
106+
return;
107+
}
108+
// Guards don't give us an easy mapping either
109+
if arms.iter().any(|arm| arm.guard.is_some()) {
110+
return;
111+
}
112+
let check_types = self.matches_behaviour == MatchLintBehaviour::WellKnownTypes;
113+
let diverging_arm_opt = arms
114+
.iter()
115+
.enumerate()
116+
.find(|(_, arm)| expr_diverges(cx, arm.body) && pat_allowed_for_else(cx, arm.pat, check_types));
117+
let Some((idx, diverging_arm)) = diverging_arm_opt else { return; };
118+
let pat_arm = &arms[1 - idx];
119+
if !expr_is_simple_identity(pat_arm.pat, pat_arm.body) {
120+
return;
121+
}
122+
123+
emit_manual_let_else(cx, stmt.span, match_expr, pat_arm.pat, diverging_arm.body);
124+
},
125+
}
126+
}
127+
128+
extract_msrv_attr!(LateContext);
129+
}
130+
131+
fn emit_manual_let_else(cx: &LateContext<'_>, span: Span, expr: &Expr<'_>, pat: &Pat<'_>, else_body: &Expr<'_>) {
132+
span_lint_and_then(
133+
cx,
134+
MANUAL_LET_ELSE,
135+
span,
136+
"this could be rewritten as `let...else`",
137+
|diag| {
138+
// This is far from perfect, for example there needs to be:
139+
// * mut additions for the bindings
140+
// * renamings of the bindings
141+
// * unused binding collision detection with existing ones
142+
// * putting patterns with at the top level | inside ()
143+
// for this to be machine applicable.
144+
let app = Applicability::HasPlaceholders;
145+
146+
if let Some(sn_pat) = snippet_opt(cx, pat.span) &&
147+
let Some(sn_expr) = snippet_opt(cx, expr.span) &&
148+
let Some(sn_else) = snippet_opt(cx, else_body.span)
149+
{
150+
let else_bl = if matches!(else_body.kind, ExprKind::Block(..)) {
151+
sn_else
152+
} else {
153+
format!("{{ {sn_else} }}")
154+
};
155+
let sugg = format!("let {sn_pat} = {sn_expr} else {else_bl};");
156+
diag.span_suggestion(span, "consider writing", sugg, app);
157+
}
158+
},
159+
);
160+
}
161+
162+
fn expr_diverges(cx: &LateContext<'_>, expr: &'_ Expr<'_>) -> bool {
163+
fn is_never(cx: &LateContext<'_>, expr: &'_ Expr<'_>) -> bool {
164+
if let Some(ty) = cx.typeck_results().expr_ty_opt(expr) {
165+
return ty.is_never();
166+
}
167+
false
168+
}
169+
// We can't just call is_never on expr and be done, because the type system
170+
// sometimes coerces the ! type to something different before we can get
171+
// our hands on it. So instead, we do a manual search. We do fall back to
172+
// is_never in some places when there is no better alternative.
173+
for_each_expr(expr, |ex| {
174+
match ex.kind {
175+
ExprKind::Continue(_) | ExprKind::Break(_, _) | ExprKind::Ret(_) => ControlFlow::Break(()),
176+
ExprKind::Call(call, _) => {
177+
if is_never(cx, ex) || is_never(cx, call) {
178+
return ControlFlow::Break(());
179+
}
180+
ControlFlow::Continue(Descend::Yes)
181+
},
182+
ExprKind::MethodCall(..) => {
183+
if is_never(cx, ex) {
184+
return ControlFlow::Break(());
185+
}
186+
ControlFlow::Continue(Descend::Yes)
187+
},
188+
ExprKind::If(if_expr, if_then, if_else) => {
189+
let else_diverges = if_else.map_or(false, |ex| expr_diverges(cx, ex));
190+
let diverges = expr_diverges(cx, if_expr) || (else_diverges && expr_diverges(cx, if_then));
191+
if diverges {
192+
return ControlFlow::Break(());
193+
}
194+
ControlFlow::Continue(Descend::No)
195+
},
196+
ExprKind::Match(match_expr, match_arms, _) => {
197+
let diverges = expr_diverges(cx, match_expr)
198+
|| match_arms.iter().all(|arm| {
199+
let guard_diverges = arm.guard.as_ref().map_or(false, |g| expr_diverges(cx, g.body()));
200+
guard_diverges || expr_diverges(cx, arm.body)
201+
});
202+
if diverges {
203+
return ControlFlow::Break(());
204+
}
205+
ControlFlow::Continue(Descend::No)
206+
},
207+
208+
// Don't continue into loops or labeled blocks, as they are breakable,
209+
// and we'd have to start checking labels.
210+
ExprKind::Block(_, Some(_)) | ExprKind::Loop(..) => ControlFlow::Continue(Descend::No),
211+
212+
// Default: descend
213+
_ => ControlFlow::Continue(Descend::Yes),
214+
}
215+
})
216+
.is_some()
217+
}
218+
219+
fn pat_allowed_for_else(cx: &LateContext<'_>, pat: &'_ Pat<'_>, check_types: bool) -> bool {
220+
// Check whether the pattern contains any bindings, as the
221+
// binding might potentially be used in the body.
222+
// TODO: only look for *used* bindings.
223+
let mut has_bindings = false;
224+
pat.each_binding_or_first(&mut |_, _, _, _| has_bindings = true);
225+
if has_bindings {
226+
return false;
227+
}
228+
229+
// If we shouldn't check the types, exit early.
230+
if !check_types {
231+
return true;
232+
}
233+
234+
// Check whether any possibly "unknown" patterns are included,
235+
// because users might not know which values some enum has.
236+
// Well-known enums are excepted, as we assume people know them.
237+
// We do a deep check, to be able to disallow Err(En::Foo(_))
238+
// for usage of the En::Foo variant, as we disallow En::Foo(_),
239+
// but we allow Err(_).
240+
let typeck_results = cx.typeck_results();
241+
let mut has_disallowed = false;
242+
pat.walk_always(|pat| {
243+
// Only do the check if the type is "spelled out" in the pattern
244+
if !matches!(
245+
pat.kind,
246+
PatKind::Struct(..) | PatKind::TupleStruct(..) | PatKind::Path(..)
247+
) {
248+
return;
249+
};
250+
let ty = typeck_results.pat_ty(pat);
251+
// Option and Result are allowed, everything else isn't.
252+
if !(is_type_diagnostic_item(cx, ty, sym::Option) || is_type_diagnostic_item(cx, ty, sym::Result)) {
253+
has_disallowed = true;
254+
}
255+
});
256+
!has_disallowed
257+
}
258+
259+
/// Checks if the passed block is a simple identity referring to bindings created by the pattern
260+
fn expr_is_simple_identity(pat: &'_ Pat<'_>, expr: &'_ Expr<'_>) -> bool {
261+
// We support patterns with multiple bindings and tuples, like:
262+
// let ... = if let (Some(foo), bar) = g() { (foo, bar) } else { ... }
263+
let peeled = peel_blocks(expr);
264+
let paths = match peeled.kind {
265+
ExprKind::Tup(exprs) | ExprKind::Array(exprs) => exprs,
266+
ExprKind::Path(_) => std::slice::from_ref(peeled),
267+
_ => return false,
268+
};
269+
let mut pat_bindings = FxHashSet::default();
270+
pat.each_binding_or_first(&mut |_ann, _hir_id, _sp, ident| {
271+
pat_bindings.insert(ident);
272+
});
273+
if pat_bindings.len() < paths.len() {
274+
return false;
275+
}
276+
for path in paths {
277+
if_chain! {
278+
if let ExprKind::Path(QPath::Resolved(_ty, path)) = path.kind;
279+
if let [path_seg] = path.segments;
280+
then {
281+
if !pat_bindings.remove(&path_seg.ident) {
282+
return false;
283+
}
284+
} else {
285+
return false;
286+
}
287+
}
288+
}
289+
true
290+
}
291+
292+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize)]
293+
pub enum MatchLintBehaviour {
294+
AllTypes,
295+
WellKnownTypes,
296+
Never,
297+
}

clippy_lints/src/utils/conf.rs

+7-1
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),
@@ -335,6 +335,12 @@ define_Conf! {
335335
///
336336
/// Enables verbose mode. Triggers if there is more than one uppercase char next to each other
337337
(upper_case_acronyms_aggressive: bool = false),
338+
/// Lint: MANUAL_LET_ELSE.
339+
///
340+
/// Whether the matches should be considered by the lint, and whether there should
341+
/// be filtering for common types.
342+
(matches_for_let_else: crate::manual_let_else::MatchLintBehaviour =
343+
crate::manual_let_else::MatchLintBehaviour::WellKnownTypes),
338344
/// Lint: _CARGO_COMMON_METADATA.
339345
///
340346
/// For internal testing only, ignores the current `publish` settings in the Cargo manifest.

clippy_utils/src/consts.rs

+3-7
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,8 @@ impl Constant {
147147
_ => None,
148148
},
149149
(&Self::Vec(ref l), &Self::Vec(ref r)) => {
150-
let cmp_type = match *cmp_type.kind() {
151-
ty::Array(ty, _) | ty::Slice(ty) => ty,
152-
_ => return None,
150+
let (ty::Array(cmp_type, _) | ty::Slice(cmp_type)) = *cmp_type.kind() else {
151+
return None
153152
};
154153
iter::zip(l, r)
155154
.map(|(li, ri)| Self::partial_cmp(tcx, cmp_type, li, ri))
@@ -401,10 +400,7 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
401400
use self::Constant::{Int, F32, F64};
402401
match *o {
403402
Int(value) => {
404-
let ity = match *ty.kind() {
405-
ty::Int(ity) => ity,
406-
_ => return None,
407-
};
403+
let ty::Int(ity) = *ty.kind() else { return None };
408404
// sign extend
409405
let value = sext(self.lcx.tcx, value, ity);
410406
let value = value.checked_neg()?;

0 commit comments

Comments
 (0)