]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/collapsible_if.rs
Auto merge of #4082 - Manishearth:macro-check-split, r=oli-obk
[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 if_chain::if_chain;
16 use rustc::lint::{EarlyContext, EarlyLintPass, LintArray, LintPass};
17 use rustc::{declare_lint_pass, declare_tool_lint};
18 use syntax::ast;
19
20 use crate::utils::sugg::Sugg;
21 use crate::utils::{
22     in_macro_or_desugar, snippet_block, snippet_block_with_applicability, span_lint_and_sugg, span_lint_and_then,
23 };
24 use rustc_errors::Applicability;
25
26 declare_clippy_lint! {
27     /// **What it does:** Checks for nested `if` statements which can be collapsed
28     /// by `&&`-combining their conditions and for `else { if ... }` expressions
29     /// that
30     /// can be collapsed to `else if ...`.
31     ///
32     /// **Why is this bad?** Each `if`-statement adds one level of nesting, which
33     /// makes code look more complex than it really is.
34     ///
35     /// **Known problems:** None.
36     ///
37     /// **Example:**
38     /// ```rust,ignore
39     /// if x {
40     ///     if y {
41     ///         …
42     ///     }
43     /// }
44     ///
45     /// // or
46     ///
47     /// if x {
48     ///     …
49     /// } else {
50     ///     if y {
51     ///         …
52     ///     }
53     /// }
54     /// ```
55     ///
56     /// Should be written:
57     ///
58     /// ```rust.ignore
59     /// if x && y {
60     ///     …
61     /// }
62     ///
63     /// // or
64     ///
65     /// if x {
66     ///     …
67     /// } else if y {
68     ///     …
69     /// }
70     /// ```
71     pub COLLAPSIBLE_IF,
72     style,
73     "`if`s that can be collapsed (e.g., `if x { if y { ... } }` and `else { if x { ... } }`)"
74 }
75
76 declare_lint_pass!(CollapsibleIf => [COLLAPSIBLE_IF]);
77
78 impl EarlyLintPass for CollapsibleIf {
79     fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) {
80         if !in_macro_or_desugar(expr.span) {
81             check_if(cx, expr)
82         }
83     }
84 }
85
86 fn check_if(cx: &EarlyContext<'_>, expr: &ast::Expr) {
87     match expr.node {
88         ast::ExprKind::If(ref check, ref then, ref else_) => {
89             if let Some(ref else_) = *else_ {
90                 check_collapsible_maybe_if_let(cx, else_);
91             } else {
92                 check_collapsible_no_if_let(cx, expr, check, then);
93             }
94         },
95         ast::ExprKind::IfLet(_, _, _, Some(ref else_)) => {
96             check_collapsible_maybe_if_let(cx, else_);
97         },
98         _ => (),
99     }
100 }
101
102 fn block_starts_with_comment(cx: &EarlyContext<'_>, expr: &ast::Block) -> bool {
103     // We trim all opening braces and whitespaces and then check if the next string is a comment.
104     let trimmed_block_text = snippet_block(cx, expr.span, "..")
105         .trim_start_matches(|c: char| c.is_whitespace() || c == '{')
106         .to_owned();
107     trimmed_block_text.starts_with("//") || trimmed_block_text.starts_with("/*")
108 }
109
110 fn check_collapsible_maybe_if_let(cx: &EarlyContext<'_>, else_: &ast::Expr) {
111     if_chain! {
112         if let ast::ExprKind::Block(ref block, _) = else_.node;
113         if !block_starts_with_comment(cx, block);
114         if let Some(else_) = expr_block(block);
115         if !in_macro_or_desugar(else_.span);
116         then {
117             match else_.node {
118                 ast::ExprKind::If(..) | ast::ExprKind::IfLet(..) => {
119                     let mut applicability = Applicability::MachineApplicable;
120                     span_lint_and_sugg(
121                         cx,
122                         COLLAPSIBLE_IF,
123                         block.span,
124                         "this `else { if .. }` block can be collapsed",
125                         "try",
126                         snippet_block_with_applicability(cx, else_.span, "..", &mut applicability).into_owned(),
127                         applicability,
128                     );
129                 }
130                 _ => (),
131             }
132         }
133     }
134 }
135
136 fn check_collapsible_no_if_let(cx: &EarlyContext<'_>, expr: &ast::Expr, check: &ast::Expr, then: &ast::Block) {
137     if_chain! {
138         if !block_starts_with_comment(cx, then);
139         if let Some(inner) = expr_block(then);
140         if let ast::ExprKind::If(ref check_inner, ref content, None) = inner.node;
141         then {
142             if expr.span.ctxt() != inner.span.ctxt() {
143                 return;
144             }
145             span_lint_and_then(cx, COLLAPSIBLE_IF, expr.span, "this if statement can be collapsed", |db| {
146                 let lhs = Sugg::ast(cx, check, "..");
147                 let rhs = Sugg::ast(cx, check_inner, "..");
148                 db.span_suggestion(
149                     expr.span,
150                     "try",
151                     format!(
152                         "if {} {}",
153                         lhs.and(&rhs),
154                         snippet_block(cx, content.span, ".."),
155                     ),
156                     Applicability::MachineApplicable, // snippet
157                 );
158             });
159         }
160     }
161 }
162
163 /// If the block contains only one expression, return it.
164 fn expr_block(block: &ast::Block) -> Option<&ast::Expr> {
165     let mut it = block.stmts.iter();
166
167     if let (Some(stmt), None) = (it.next(), it.next()) {
168         match stmt.node {
169             ast::StmtKind::Expr(ref expr) | ast::StmtKind::Semi(ref expr) => Some(expr),
170             _ => None,
171         }
172     } else {
173         None
174     }
175 }