]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/collapsible_if.rs
Rollup merge of #96142 - cjgillot:no-crate-def-index, r=petrochenkov
[rust.git] / clippy_lints / src / collapsible_if.rs
1 //! Checks for if expressions that contain only an if expression.
2 //!
3 //! For example, the lint would catch:
4 //!
5 //! ```rust,ignore
6 //! if x {
7 //!     if y {
8 //!         println!("Hello world");
9 //!     }
10 //! }
11 //! ```
12 //!
13 //! This lint is **warn** by default
14
15 use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
16 use clippy_utils::source::{snippet_block, snippet_block_with_applicability};
17 use clippy_utils::sugg::Sugg;
18 use if_chain::if_chain;
19 use rustc_ast::ast;
20 use rustc_errors::Applicability;
21 use rustc_lint::{EarlyContext, EarlyLintPass};
22 use rustc_session::{declare_lint_pass, declare_tool_lint};
23
24 declare_clippy_lint! {
25     /// ### What it does
26     /// Checks for nested `if` statements which can be collapsed
27     /// by `&&`-combining their conditions.
28     ///
29     /// ### Why is this bad?
30     /// Each `if`-statement adds one level of nesting, which
31     /// makes code look more complex than it really is.
32     ///
33     /// ### Example
34     /// ```rust,ignore
35     /// if x {
36     ///     if y {
37     ///         …
38     ///     }
39     /// }
40     ///
41     /// ```
42     ///
43     /// Should be written:
44     ///
45     /// ```rust,ignore
46     /// if x && y {
47     ///     …
48     /// }
49     /// ```
50     #[clippy::version = "pre 1.29.0"]
51     pub COLLAPSIBLE_IF,
52     style,
53     "nested `if`s that can be collapsed (e.g., `if x { if y { ... } }`"
54 }
55
56 declare_clippy_lint! {
57     /// ### What it does
58     /// Checks for collapsible `else { if ... }` expressions
59     /// that can be collapsed to `else if ...`.
60     ///
61     /// ### Why is this bad?
62     /// Each `if`-statement adds one level of nesting, which
63     /// makes code look more complex than it really is.
64     ///
65     /// ### Example
66     /// ```rust,ignore
67     ///
68     /// if x {
69     ///     …
70     /// } else {
71     ///     if y {
72     ///         …
73     ///     }
74     /// }
75     /// ```
76     ///
77     /// Should be written:
78     ///
79     /// ```rust,ignore
80     /// if x {
81     ///     …
82     /// } else if y {
83     ///     …
84     /// }
85     /// ```
86     #[clippy::version = "1.51.0"]
87     pub COLLAPSIBLE_ELSE_IF,
88     style,
89     "nested `else`-`if` expressions that can be collapsed (e.g., `else { if x { ... } }`)"
90 }
91
92 declare_lint_pass!(CollapsibleIf => [COLLAPSIBLE_IF, COLLAPSIBLE_ELSE_IF]);
93
94 impl EarlyLintPass for CollapsibleIf {
95     fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) {
96         if !expr.span.from_expansion() {
97             check_if(cx, expr);
98         }
99     }
100 }
101
102 fn check_if(cx: &EarlyContext<'_>, expr: &ast::Expr) {
103     if let ast::ExprKind::If(check, then, else_) = &expr.kind {
104         if let Some(else_) = else_ {
105             check_collapsible_maybe_if_let(cx, else_);
106         } else if let ast::ExprKind::Let(..) = check.kind {
107             // Prevent triggering on `if let a = b { if c { .. } }`.
108         } else {
109             check_collapsible_no_if_let(cx, expr, check, then);
110         }
111     }
112 }
113
114 fn block_starts_with_comment(cx: &EarlyContext<'_>, expr: &ast::Block) -> bool {
115     // We trim all opening braces and whitespaces and then check if the next string is a comment.
116     let trimmed_block_text = snippet_block(cx, expr.span, "..", None)
117         .trim_start_matches(|c: char| c.is_whitespace() || c == '{')
118         .to_owned();
119     trimmed_block_text.starts_with("//") || trimmed_block_text.starts_with("/*")
120 }
121
122 fn check_collapsible_maybe_if_let(cx: &EarlyContext<'_>, else_: &ast::Expr) {
123     if_chain! {
124         if let ast::ExprKind::Block(ref block, _) = else_.kind;
125         if !block_starts_with_comment(cx, block);
126         if let Some(else_) = expr_block(block);
127         if else_.attrs.is_empty();
128         if !else_.span.from_expansion();
129         if let ast::ExprKind::If(..) = else_.kind;
130         then {
131             let mut applicability = Applicability::MachineApplicable;
132             span_lint_and_sugg(
133                 cx,
134                 COLLAPSIBLE_ELSE_IF,
135                 block.span,
136                 "this `else { if .. }` block can be collapsed",
137                 "collapse nested if block",
138                 snippet_block_with_applicability(cx, else_.span, "..", Some(block.span), &mut applicability).into_owned(),
139                 applicability,
140             );
141         }
142     }
143 }
144
145 fn check_collapsible_no_if_let(cx: &EarlyContext<'_>, expr: &ast::Expr, check: &ast::Expr, then: &ast::Block) {
146     if_chain! {
147         if !block_starts_with_comment(cx, then);
148         if let Some(inner) = expr_block(then);
149         if inner.attrs.is_empty();
150         if let ast::ExprKind::If(ref check_inner, ref content, None) = inner.kind;
151         // Prevent triggering on `if c { if let a = b { .. } }`.
152         if !matches!(check_inner.kind, ast::ExprKind::Let(..));
153         if expr.span.ctxt() == inner.span.ctxt();
154         then {
155             span_lint_and_then(cx, COLLAPSIBLE_IF, expr.span, "this `if` statement can be collapsed", |diag| {
156                 let lhs = Sugg::ast(cx, check, "..");
157                 let rhs = Sugg::ast(cx, check_inner, "..");
158                 diag.span_suggestion(
159                     expr.span,
160                     "collapse nested if block",
161                     format!(
162                         "if {} {}",
163                         lhs.and(&rhs),
164                         snippet_block(cx, content.span, "..", Some(expr.span)),
165                     ),
166                     Applicability::MachineApplicable, // snippet
167                 );
168             });
169         }
170     }
171 }
172
173 /// If the block contains only one expression, return it.
174 fn expr_block(block: &ast::Block) -> Option<&ast::Expr> {
175     let mut it = block.stmts.iter();
176
177     if let (Some(stmt), None) = (it.next(), it.next()) {
178         match stmt.kind {
179             ast::StmtKind::Expr(ref expr) | ast::StmtKind::Semi(ref expr) => Some(expr),
180             _ => None,
181         }
182     } else {
183         None
184     }
185 }