]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/collapsible_if.rs
Move some utils to clippy_utils::source module
[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::source::{snippet_block, snippet_block_with_applicability};
16 use if_chain::if_chain;
17 use rustc_ast::ast;
18 use rustc_errors::Applicability;
19 use rustc_lint::{EarlyContext, EarlyLintPass};
20 use rustc_session::{declare_lint_pass, declare_tool_lint};
21
22 use crate::utils::sugg::Sugg;
23 use crate::utils::{span_lint_and_sugg, span_lint_and_then};
24
25 declare_clippy_lint! {
26     /// **What it does:** Checks for nested `if` statements which can be collapsed
27     /// by `&&`-combining their conditions.
28     ///
29     /// **Why is this bad?** Each `if`-statement adds one level of nesting, which
30     /// makes code look more complex than it really is.
31     ///
32     /// **Known problems:** None.
33     ///
34     /// **Example:**
35     /// ```rust,ignore
36     /// if x {
37     ///     if y {
38     ///         …
39     ///     }
40     /// }
41     ///
42     /// ```
43     ///
44     /// Should be written:
45     ///
46     /// ```rust.ignore
47     /// if x && y {
48     ///     …
49     /// }
50     /// ```
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:** Checks for collapsible `else { if ... }` expressions
58     /// that can be collapsed to `else if ...`.
59     ///
60     /// **Why is this bad?** Each `if`-statement adds one level of nesting, which
61     /// makes code look more complex than it really is.
62     ///
63     /// **Known problems:** None.
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     pub COLLAPSIBLE_ELSE_IF,
87     style,
88     "nested `else`-`if` expressions that can be collapsed (e.g., `else { if x { ... } }`)"
89 }
90
91 declare_lint_pass!(CollapsibleIf => [COLLAPSIBLE_IF, COLLAPSIBLE_ELSE_IF]);
92
93 impl EarlyLintPass for CollapsibleIf {
94     fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) {
95         if !expr.span.from_expansion() {
96             check_if(cx, expr)
97         }
98     }
99 }
100
101 fn check_if(cx: &EarlyContext<'_>, expr: &ast::Expr) {
102     if let ast::ExprKind::If(check, then, else_) = &expr.kind {
103         if let Some(else_) = else_ {
104             check_collapsible_maybe_if_let(cx, else_);
105         } else if let ast::ExprKind::Let(..) = check.kind {
106             // Prevent triggering on `if let a = b { if c { .. } }`.
107         } else {
108             check_collapsible_no_if_let(cx, expr, check, then);
109         }
110     }
111 }
112
113 fn block_starts_with_comment(cx: &EarlyContext<'_>, expr: &ast::Block) -> bool {
114     // We trim all opening braces and whitespaces and then check if the next string is a comment.
115     let trimmed_block_text = snippet_block(cx, expr.span, "..", None)
116         .trim_start_matches(|c: char| c.is_whitespace() || c == '{')
117         .to_owned();
118     trimmed_block_text.starts_with("//") || trimmed_block_text.starts_with("/*")
119 }
120
121 fn check_collapsible_maybe_if_let(cx: &EarlyContext<'_>, else_: &ast::Expr) {
122     if_chain! {
123         if let ast::ExprKind::Block(ref block, _) = else_.kind;
124         if !block_starts_with_comment(cx, block);
125         if let Some(else_) = expr_block(block);
126         if else_.attrs.is_empty();
127         if !else_.span.from_expansion();
128         if let ast::ExprKind::If(..) = else_.kind;
129         then {
130             let mut applicability = Applicability::MachineApplicable;
131             span_lint_and_sugg(
132                 cx,
133                 COLLAPSIBLE_ELSE_IF,
134                 block.span,
135                 "this `else { if .. }` block can be collapsed",
136                 "collapse nested if block",
137                 snippet_block_with_applicability(cx, else_.span, "..", Some(block.span), &mut applicability).into_owned(),
138                 applicability,
139             );
140         }
141     }
142 }
143
144 fn check_collapsible_no_if_let(cx: &EarlyContext<'_>, expr: &ast::Expr, check: &ast::Expr, then: &ast::Block) {
145     if_chain! {
146         if !block_starts_with_comment(cx, then);
147         if let Some(inner) = expr_block(then);
148         if inner.attrs.is_empty();
149         if let ast::ExprKind::If(ref check_inner, ref content, None) = inner.kind;
150         // Prevent triggering on `if c { if let a = b { .. } }`.
151         if !matches!(check_inner.kind, ast::ExprKind::Let(..));
152         if expr.span.ctxt() == inner.span.ctxt();
153         then {
154             span_lint_and_then(cx, COLLAPSIBLE_IF, expr.span, "this `if` statement can be collapsed", |diag| {
155                 let lhs = Sugg::ast(cx, check, "..");
156                 let rhs = Sugg::ast(cx, check_inner, "..");
157                 diag.span_suggestion(
158                     expr.span,
159                     "collapse nested if block",
160                     format!(
161                         "if {} {}",
162                         lhs.and(&rhs),
163                         snippet_block(cx, content.span, "..", Some(expr.span)),
164                     ),
165                     Applicability::MachineApplicable, // snippet
166                 );
167             });
168         }
169     }
170 }
171
172 /// If the block contains only one expression, return it.
173 fn expr_block(block: &ast::Block) -> Option<&ast::Expr> {
174     let mut it = block.stmts.iter();
175
176     if let (Some(stmt), None) = (it.next(), it.next()) {
177         match stmt.kind {
178             ast::StmtKind::Expr(ref expr) | ast::StmtKind::Semi(ref expr) => Some(expr),
179             _ => None,
180         }
181     } else {
182         None
183     }
184 }