| //! Checks for if expressions that contain only an if expression. |
| //! |
| //! For example, the lint would catch: |
| //! |
| //! ```rust,ignore |
| //! if x { |
| //! if y { |
| //! println!("Hello world"); |
| //! } |
| //! } |
| //! ``` |
| //! |
| //! This lint is **warn** by default |
| |
| use if_chain::if_chain; |
| use rustc::lint::{EarlyContext, EarlyLintPass, LintArray, LintPass}; |
| use rustc::{declare_lint_pass, declare_tool_lint}; |
| use syntax::ast; |
| |
| use crate::utils::sugg::Sugg; |
| use crate::utils::{snippet_block, snippet_block_with_applicability, span_lint_and_sugg, span_lint_and_then}; |
| use rustc_errors::Applicability; |
| |
| declare_clippy_lint! { |
| /// **What it does:** Checks for nested `if` statements which can be collapsed |
| /// by `&&`-combining their conditions and for `else { if ... }` expressions |
| /// that |
| /// can be collapsed to `else if ...`. |
| /// |
| /// **Why is this bad?** Each `if`-statement adds one level of nesting, which |
| /// makes code look more complex than it really is. |
| /// |
| /// **Known problems:** None. |
| /// |
| /// **Example:** |
| /// ```rust,ignore |
| /// if x { |
| /// if y { |
| /// … |
| /// } |
| /// } |
| /// |
| /// // or |
| /// |
| /// if x { |
| /// … |
| /// } else { |
| /// if y { |
| /// … |
| /// } |
| /// } |
| /// ``` |
| /// |
| /// Should be written: |
| /// |
| /// ```rust.ignore |
| /// if x && y { |
| /// … |
| /// } |
| /// |
| /// // or |
| /// |
| /// if x { |
| /// … |
| /// } else if y { |
| /// … |
| /// } |
| /// ``` |
| pub COLLAPSIBLE_IF, |
| style, |
| "`if`s that can be collapsed (e.g., `if x { if y { ... } }` and `else { if x { ... } }`)" |
| } |
| |
| declare_lint_pass!(CollapsibleIf => [COLLAPSIBLE_IF]); |
| |
| impl EarlyLintPass for CollapsibleIf { |
| fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) { |
| if !expr.span.from_expansion() { |
| check_if(cx, expr) |
| } |
| } |
| } |
| |
| fn check_if(cx: &EarlyContext<'_>, expr: &ast::Expr) { |
| if let ast::ExprKind::If(check, then, else_) = &expr.kind { |
| if let Some(else_) = else_ { |
| check_collapsible_maybe_if_let(cx, else_); |
| } else if let ast::ExprKind::Let(..) = check.kind { |
| // Prevent triggering on `if let a = b { if c { .. } }`. |
| } else { |
| check_collapsible_no_if_let(cx, expr, check, then); |
| } |
| } |
| } |
| |
| fn block_starts_with_comment(cx: &EarlyContext<'_>, expr: &ast::Block) -> bool { |
| // We trim all opening braces and whitespaces and then check if the next string is a comment. |
| let trimmed_block_text = snippet_block(cx, expr.span, "..") |
| .trim_start_matches(|c: char| c.is_whitespace() || c == '{') |
| .to_owned(); |
| trimmed_block_text.starts_with("//") || trimmed_block_text.starts_with("/*") |
| } |
| |
| fn check_collapsible_maybe_if_let(cx: &EarlyContext<'_>, else_: &ast::Expr) { |
| if_chain! { |
| if let ast::ExprKind::Block(ref block, _) = else_.kind; |
| if !block_starts_with_comment(cx, block); |
| if let Some(else_) = expr_block(block); |
| if !else_.span.from_expansion(); |
| if let ast::ExprKind::If(..) = else_.kind; |
| then { |
| let mut applicability = Applicability::MachineApplicable; |
| span_lint_and_sugg( |
| cx, |
| COLLAPSIBLE_IF, |
| block.span, |
| "this `else { if .. }` block can be collapsed", |
| "try", |
| snippet_block_with_applicability(cx, else_.span, "..", &mut applicability).into_owned(), |
| applicability, |
| ); |
| } |
| } |
| } |
| |
| fn check_collapsible_no_if_let(cx: &EarlyContext<'_>, expr: &ast::Expr, check: &ast::Expr, then: &ast::Block) { |
| if_chain! { |
| if !block_starts_with_comment(cx, then); |
| if let Some(inner) = expr_block(then); |
| if let ast::ExprKind::If(ref check_inner, ref content, None) = inner.kind; |
| then { |
| if let ast::ExprKind::Let(..) = check_inner.kind { |
| // Prevent triggering on `if c { if let a = b { .. } }`. |
| return; |
| } |
| |
| if expr.span.ctxt() != inner.span.ctxt() { |
| return; |
| } |
| span_lint_and_then(cx, COLLAPSIBLE_IF, expr.span, "this if statement can be collapsed", |db| { |
| let lhs = Sugg::ast(cx, check, ".."); |
| let rhs = Sugg::ast(cx, check_inner, ".."); |
| db.span_suggestion( |
| expr.span, |
| "try", |
| format!( |
| "if {} {}", |
| lhs.and(&rhs), |
| snippet_block(cx, content.span, ".."), |
| ), |
| Applicability::MachineApplicable, // snippet |
| ); |
| }); |
| } |
| } |
| } |
| |
| /// If the block contains only one expression, return it. |
| fn expr_block(block: &ast::Block) -> Option<&ast::Expr> { |
| let mut it = block.stmts.iter(); |
| |
| if let (Some(stmt), None) = (it.next(), it.next()) { |
| match stmt.kind { |
| ast::StmtKind::Expr(ref expr) | ast::StmtKind::Semi(ref expr) => Some(expr), |
| _ => None, |
| } |
| } else { |
| None |
| } |
| } |