Skip to content

Commit 5f887d0

Browse files
committed
Add if_chain lints
1 parent 487c2e8 commit 5f887d0

File tree

4 files changed

+353
-5
lines changed

4 files changed

+353
-5
lines changed

clippy_lints/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,8 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
550550
#[cfg(feature = "internal-lints")]
551551
&utils::internal_lints::DEFAULT_LINT,
552552
#[cfg(feature = "internal-lints")]
553+
&utils::internal_lints::IF_CHAIN_STYLE,
554+
#[cfg(feature = "internal-lints")]
553555
&utils::internal_lints::INTERNING_DEFINED_SYMBOL,
554556
#[cfg(feature = "internal-lints")]
555557
&utils::internal_lints::INVALID_PATHS,
@@ -1026,6 +1028,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
10261028
store.register_late_pass(|| box utils::inspector::DeepCodeInspector);
10271029
store.register_late_pass(|| box utils::internal_lints::CollapsibleCalls);
10281030
store.register_late_pass(|| box utils::internal_lints::CompilerLintFunctions::new());
1031+
store.register_late_pass(|| box utils::internal_lints::IfChainStyle);
10291032
store.register_late_pass(|| box utils::internal_lints::InvalidPaths);
10301033
store.register_late_pass(|| box utils::internal_lints::InterningDefinedSymbol::default());
10311034
store.register_late_pass(|| box utils::internal_lints::LintWithoutLintPass::default());
@@ -1442,6 +1445,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
14421445
LintId::of(&utils::internal_lints::COLLAPSIBLE_SPAN_LINT_CALLS),
14431446
LintId::of(&utils::internal_lints::COMPILER_LINT_FUNCTIONS),
14441447
LintId::of(&utils::internal_lints::DEFAULT_LINT),
1448+
LintId::of(&utils::internal_lints::IF_CHAIN_STYLE),
14451449
LintId::of(&utils::internal_lints::INTERNING_DEFINED_SYMBOL),
14461450
LintId::of(&utils::internal_lints::INVALID_PATHS),
14471451
LintId::of(&utils::internal_lints::LINT_WITHOUT_LINT_PASS),

clippy_lints/src/utils/internal_lints.rs

Lines changed: 172 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use crate::consts::{constant_simple, Constant};
2-
use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_sugg};
2+
use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_sugg, span_lint_and_then};
33
use clippy_utils::source::snippet;
44
use clippy_utils::ty::match_type;
5-
use clippy_utils::{is_expn_of, match_def_path, match_qpath, method_calls, path_to_res, paths, run_lints, SpanlessEq};
5+
use clippy_utils::{
6+
is_else_clause, is_expn_of, match_def_path, match_qpath, method_calls, path_to_res, paths, run_lints, SpanlessEq,
7+
};
68
use if_chain::if_chain;
79
use rustc_ast::ast::{Crate as AstCrate, ItemKind, LitKind, ModKind, NodeId};
810
use rustc_ast::visit::FnKind;
@@ -14,15 +16,17 @@ use rustc_hir::def_id::DefId;
1416
use rustc_hir::hir_id::CRATE_HIR_ID;
1517
use rustc_hir::intravisit::{NestedVisitorMap, Visitor};
1618
use rustc_hir::{
17-
BinOpKind, Crate, Expr, ExprKind, HirId, Item, MutTy, Mutability, Node, Path, StmtKind, Ty, TyKind, UnOp,
19+
BinOpKind, Block, Crate, Expr, ExprKind, HirId, Item, Local, MatchSource, MutTy, Mutability, Node, Path, Stmt,
20+
StmtKind, Ty, TyKind, UnOp,
1821
};
19-
use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass};
22+
use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext};
2023
use rustc_middle::hir::map::Map;
2124
use rustc_middle::mir::interpret::ConstValue;
2225
use rustc_middle::ty;
2326
use rustc_session::{declare_lint_pass, declare_tool_lint, impl_lint_pass};
24-
use rustc_span::source_map::{Span, Spanned};
27+
use rustc_span::source_map::Spanned;
2528
use rustc_span::symbol::{Symbol, SymbolStr};
29+
use rustc_span::{BytePos, Span};
2630
use rustc_typeck::hir_ty_to_ty;
2731

2832
use std::borrow::{Borrow, Cow};
@@ -297,6 +301,13 @@ declare_clippy_lint! {
297301
"unnecessary conversion between Symbol and string"
298302
}
299303

304+
declare_clippy_lint! {
305+
/// Finds unidiomatic usage of `if_chain!`
306+
pub IF_CHAIN_STYLE,
307+
internal,
308+
"non-idiomatic `if_chain!` usage"
309+
}
310+
300311
declare_lint_pass!(ClippyLintsInternal => [CLIPPY_LINTS_INTERNAL]);
301312

302313
impl EarlyLintPass for ClippyLintsInternal {
@@ -1063,3 +1074,159 @@ impl<'tcx> SymbolStrExpr<'tcx> {
10631074
}
10641075
}
10651076
}
1077+
1078+
declare_lint_pass!(IfChainStyle => [IF_CHAIN_STYLE]);
1079+
1080+
impl<'tcx> LateLintPass<'tcx> for IfChainStyle {
1081+
fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx hir::Block<'_>) {
1082+
let (local, after, if_chain_span) = if_chain! {
1083+
if let [Stmt { kind: StmtKind::Local(local), .. }, after @ ..] = block.stmts;
1084+
if let Some(if_chain_span) = is_expn_of(block.span, "if_chain");
1085+
then { (local, after, if_chain_span) } else { return }
1086+
};
1087+
if is_first_if_chain_expr(cx, block.hir_id, if_chain_span) {
1088+
span_lint(
1089+
cx,
1090+
IF_CHAIN_STYLE,
1091+
if_chain_local_span(cx, local, if_chain_span),
1092+
"`let` expression should be above the `if_chain!`",
1093+
);
1094+
} else if local.span.ctxt() == block.span.ctxt() && is_if_chain_then(after, block.expr, if_chain_span) {
1095+
span_lint(
1096+
cx,
1097+
IF_CHAIN_STYLE,
1098+
if_chain_local_span(cx, local, if_chain_span),
1099+
"`let` expression should be inside `then { .. }`",
1100+
)
1101+
}
1102+
}
1103+
1104+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) {
1105+
let (cond, then, els) = match expr.kind {
1106+
ExprKind::If(cond, then, els) => (Some(cond), then, els.is_some()),
1107+
ExprKind::Match(
1108+
_,
1109+
[arm, ..],
1110+
MatchSource::IfLetDesugar {
1111+
contains_else_clause: els,
1112+
},
1113+
) => (None, arm.body, els),
1114+
_ => return,
1115+
};
1116+
let then_block = match then.kind {
1117+
ExprKind::Block(block, _) => block,
1118+
_ => return,
1119+
};
1120+
let if_chain_span = is_expn_of(expr.span, "if_chain");
1121+
if !els {
1122+
check_nested_if_chains(cx, expr, then_block, if_chain_span);
1123+
}
1124+
let if_chain_span = match if_chain_span {
1125+
None => return,
1126+
Some(span) => span,
1127+
};
1128+
// check for `if a && b;`
1129+
if_chain! {
1130+
if let Some(cond) = cond;
1131+
if let ExprKind::Binary(op, _, _) = cond.kind;
1132+
if op.node == BinOpKind::And;
1133+
if cx.sess().source_map().is_multiline(cond.span);
1134+
then {
1135+
span_lint(cx, IF_CHAIN_STYLE, cond.span, "`if a && b;` should be `if a; if b;`");
1136+
}
1137+
}
1138+
if is_first_if_chain_expr(cx, expr.hir_id, if_chain_span)
1139+
&& is_if_chain_then(then_block.stmts, then_block.expr, if_chain_span)
1140+
{
1141+
span_lint(cx, IF_CHAIN_STYLE, expr.span, "`if_chain!` only has one `if`")
1142+
}
1143+
}
1144+
}
1145+
1146+
fn check_nested_if_chains(
1147+
cx: &LateContext<'_>,
1148+
if_expr: &Expr<'_>,
1149+
then_block: &Block<'_>,
1150+
if_chain_span: Option<Span>,
1151+
) {
1152+
#[rustfmt::skip]
1153+
let (head, tail) = match *then_block {
1154+
Block { stmts, expr: Some(tail), .. } => (stmts, tail),
1155+
Block {
1156+
stmts: &[
1157+
ref head @ ..,
1158+
Stmt { kind: StmtKind::Expr(tail) | StmtKind::Semi(tail), .. }
1159+
],
1160+
..
1161+
} => (head, tail),
1162+
_ => return,
1163+
};
1164+
if_chain! {
1165+
if matches!(tail.kind,
1166+
ExprKind::If(_, _, None)
1167+
| ExprKind::Match(.., MatchSource::IfLetDesugar { contains_else_clause: false }));
1168+
let sm = cx.sess().source_map();
1169+
if head
1170+
.iter()
1171+
.all(|stmt| matches!(stmt.kind, StmtKind::Local(..)) && !sm.is_multiline(stmt.span));
1172+
if if_chain_span.is_some() || !is_else_clause(cx.tcx, if_expr);
1173+
then {} else { return }
1174+
}
1175+
let (span, msg) = match (if_chain_span, is_expn_of(tail.span, "if_chain")) {
1176+
(None, Some(_)) => (if_expr.span, "this `if` can be part of the inner `if_chain!`"),
1177+
(Some(_), None) => (tail.span, "this `if` can be part of the outer `if_chain!`"),
1178+
(Some(a), Some(b)) if a != b => (b, "this `if_chain!` can be merged with the outer `if_chain!`"),
1179+
_ => return,
1180+
};
1181+
span_lint_and_then(cx, IF_CHAIN_STYLE, span, msg, |diag| {
1182+
let (span, msg) = match head {
1183+
[] => return,
1184+
[stmt] => (stmt.span, "this `let` statement can also be in the `if_chain!`"),
1185+
[a, .., b] => (
1186+
a.span.to(b.span),
1187+
"these `let` statements can also be in the `if_chain!`",
1188+
),
1189+
};
1190+
diag.span_help(span, msg);
1191+
});
1192+
}
1193+
1194+
fn is_first_if_chain_expr(cx: &LateContext<'_>, hir_id: HirId, if_chain_span: Span) -> bool {
1195+
cx.tcx
1196+
.hir()
1197+
.parent_iter(hir_id)
1198+
.find(|(_, node)| {
1199+
#[rustfmt::skip]
1200+
!matches!(node, Node::Expr(Expr { kind: ExprKind::Block(..), .. }) | Node::Stmt(_))
1201+
})
1202+
.map_or(false, |(id, _)| {
1203+
is_expn_of(cx.tcx.hir().span(id), "if_chain") != Some(if_chain_span)
1204+
})
1205+
}
1206+
1207+
/// Checks a trailing slice of statements and expression of a `Block` to see if they are part
1208+
/// of the `then {..}` portion of an `if_chain!`
1209+
fn is_if_chain_then(stmts: &[Stmt<'_>], expr: Option<&Expr<'_>>, if_chain_span: Span) -> bool {
1210+
let span = if let [stmt, ..] = stmts {
1211+
stmt.span
1212+
} else if let Some(expr) = expr {
1213+
expr.span
1214+
} else {
1215+
// empty `then {}`
1216+
return true;
1217+
};
1218+
is_expn_of(span, "if_chain").map_or(true, |span| span != if_chain_span)
1219+
}
1220+
1221+
/// Creates a `Span` for `let x = ..;` in an `if_chain!` call.
1222+
fn if_chain_local_span(cx: &LateContext<'_>, local: &Local<'_>, if_chain_span: Span) -> Span {
1223+
let mut span = local.pat.span;
1224+
if let Some(init) = local.init {
1225+
span = span.to(init.span);
1226+
}
1227+
span.adjust(if_chain_span.ctxt().outer_expn());
1228+
let sm = cx.sess().source_map();
1229+
let span = sm.span_extend_to_prev_str(span, "let", false);
1230+
let span = sm.span_extend_to_next_char(span, ';', false);
1231+
Span::new(span.lo() - BytePos(3), span.hi() + BytePos(1), span.ctxt())
1232+
}

tests/ui-internal/if_chain_style.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#![warn(clippy::if_chain_style)]
2+
#![allow(clippy::no_effect)]
3+
4+
extern crate if_chain;
5+
6+
use if_chain::if_chain;
7+
8+
fn main() {
9+
if true {
10+
let x = "";
11+
// `if_chain!` inside `if`
12+
if_chain! {
13+
if true;
14+
if true;
15+
then {}
16+
}
17+
}
18+
if_chain! {
19+
if true
20+
// multi-line AND'ed conditions
21+
&& false;
22+
if let Some(1) = Some(1);
23+
// `let` before `then`
24+
let x = "";
25+
then {
26+
();
27+
}
28+
}
29+
if_chain! {
30+
// single `if` condition
31+
if true;
32+
then {
33+
let x = "";
34+
// nested if
35+
if true {}
36+
}
37+
}
38+
if_chain! {
39+
// starts with `let ..`
40+
let x = "";
41+
if let Some(1) = Some(1);
42+
then {
43+
let x = "";
44+
let x = "";
45+
// nested if_chain!
46+
if_chain! {
47+
if true;
48+
if true;
49+
then {}
50+
}
51+
}
52+
}
53+
}
54+
55+
fn negative() {
56+
if true {
57+
();
58+
if_chain! {
59+
if true;
60+
if true;
61+
then { (); }
62+
}
63+
}
64+
if_chain! {
65+
if true;
66+
let x = "";
67+
if true;
68+
then { (); }
69+
}
70+
if_chain! {
71+
if true;
72+
if true;
73+
then {
74+
if true { 1 } else { 2 }
75+
} else {
76+
3
77+
}
78+
};
79+
if true {
80+
if_chain! {
81+
if true;
82+
if true;
83+
then {}
84+
}
85+
} else if false {
86+
if_chain! {
87+
if true;
88+
if false;
89+
then {}
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)