diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aee256f8..e02f9cd1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -405,6 +405,7 @@ All notable changes to this project will be documented in this file. [`mutex_integer`]: https://github.com/Manishearth/rust-clippy/wiki#mutex_integer [`needless_bool`]: https://github.com/Manishearth/rust-clippy/wiki#needless_bool [`needless_borrow`]: https://github.com/Manishearth/rust-clippy/wiki#needless_borrow +[`needless_continue`]: https://github.com/Manishearth/rust-clippy/wiki#needless_continue [`needless_lifetimes`]: https://github.com/Manishearth/rust-clippy/wiki#needless_lifetimes [`needless_pass_by_value`]: https://github.com/Manishearth/rust-clippy/wiki#needless_pass_by_value [`needless_range_loop`]: https://github.com/Manishearth/rust-clippy/wiki#needless_range_loop diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index f84b023c3..ef85b0296 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -107,6 +107,7 @@ pub mod mutex_atomic; pub mod needless_bool; pub mod needless_borrow; pub mod needless_pass_by_value; +pub mod needless_continue; pub mod needless_update; pub mod neg_multiply; pub mod new_without_default; @@ -219,6 +220,7 @@ pub fn register_plugins(reg: &mut rustc_plugin::Registry) { reg.register_late_lint_pass(box approx_const::Pass); reg.register_late_lint_pass(box misc::Pass); reg.register_early_lint_pass(box precedence::Precedence); + reg.register_early_lint_pass(box needless_continue::NeedlessContinue); reg.register_late_lint_pass(box eta_reduction::EtaPass); reg.register_late_lint_pass(box identity_op::IdentityOp); reg.register_early_lint_pass(box items_after_statements::ItemsAfterStatements); @@ -461,6 +463,7 @@ pub fn register_plugins(reg: &mut rustc_plugin::Registry) { needless_bool::NEEDLESS_BOOL, needless_borrow::NEEDLESS_BORROW, needless_pass_by_value::NEEDLESS_PASS_BY_VALUE, + needless_continue::NEEDLESS_CONTINUE, needless_update::NEEDLESS_UPDATE, neg_multiply::NEG_MULTIPLY, new_without_default::NEW_WITHOUT_DEFAULT, diff --git a/clippy_lints/src/needless_continue.rs b/clippy_lints/src/needless_continue.rs new file mode 100644 index 000000000..042f45cef --- /dev/null +++ b/clippy_lints/src/needless_continue.rs @@ -0,0 +1,468 @@ +//! Checks for continue statements in loops that are redundant. +//! +//! For example, the lint would catch +//! +//! ``` +//! while condition() { +//! update_condition(); +//! if x { +//! // ... +//! } else { +//! continue; +//! } +//! println!("Hello, world"); +//! } +//! ``` +//! +//! And suggest something like this: +//! +//! ``` +//! while condition() { +//! update_condition(); +//! if x { +//! // ... +//! println!("Hello, world"); +//! } +//! } +//! ``` +//! +//! This lint is **warn** by default. +use std; +use rustc::lint::*; +use syntax::ast; +use syntax::codemap::{original_sp,DUMMY_SP}; + +use utils::{in_macro, span_help_and_lint, snippet_block, snippet}; +use self::LintType::*; + +/// **What it does:** The lint checks for `if`-statements appearing in loops +/// that contain a `continue` statement in either their main blocks or their +/// `else`-blocks, when omitting the `else`-block possibly with some +/// rearrangement of code can make the code easier to understand. +/// +/// **Why is this bad?** Having explicit `else` blocks for `if` statements +/// containing `continue` in their THEN branch adds unnecessary branching and +/// nesting to the code. Having an else block containing just `continue` can +/// also be better written by grouping the statements following the whole `if` +/// statement within the THEN block and omitting the else block completely. +/// +/// **Known problems:** None +/// +/// **Example:** +/// ```rust +/// while condition() { +/// update_condition(); +/// if x { +/// // ... +/// } else { +/// continue; +/// } +/// println!("Hello, world"); +/// } +/// ``` +/// +/// Could be rewritten as +/// +/// ```rust +/// while condition() { +/// update_condition(); +/// if x { +/// // ... +/// println!("Hello, world"); +/// } +/// } +/// ``` +/// +/// As another example, the following code +/// +/// ```rust +/// loop { +/// if waiting() { +/// continue; +/// } else { +/// // Do something useful +/// } +/// } +/// ``` +/// Could be rewritten as +/// +/// ```rust +/// loop { +/// if waiting() { +/// continue; +/// } +/// // Do something useful +/// } +/// ``` +declare_lint! { + pub NEEDLESS_CONTINUE, + Warn, + "`continue` statements that can be replaced by a rearrangement of code" +} + +#[derive(Copy,Clone)] +pub struct NeedlessContinue; + +impl LintPass for NeedlessContinue { + fn get_lints(&self) -> LintArray { + lint_array!(NEEDLESS_CONTINUE) + } +} + +impl EarlyLintPass for NeedlessContinue { + fn check_expr(&mut self, ctx: &EarlyContext, expr: &ast::Expr) { + if !in_macro(ctx, expr.span) { + check_and_warn(ctx, expr); + } + } +} + +/// Given an Expr, returns true if either of the following is true +/// +/// - The Expr is a `continue` node. +/// - The Expr node is a block with the first statement being a `continue`. +/// +fn needless_continue_in_else(else_expr: &ast::Expr) -> bool { + let mut found = false; + match else_expr.node { + ast::ExprKind::Block(ref else_block) => { + found = is_first_block_stmt_continue(else_block); + }, + ast::ExprKind::Continue(_) => { found = true }, + _ => { }, + }; + found +} + +fn is_first_block_stmt_continue(block: &ast::Block) -> bool { + let mut ret = false; + block.stmts.get(0).map(|stmt| { + if_let_chain! {[ + let ast::StmtKind::Semi(ref e) = stmt.node, + let ast::ExprKind::Continue(_) = e.node, + ], { + ret = true; + }} + }).unwrap_or(()); + ret +} + +/// If `expr` is a loop expression (while/while let/for/loop), calls `func` with +/// the AST object representing the loop block of `expr`. +fn with_loop_block(expr: &ast::Expr, mut func: F) where F: FnMut(&ast::Block) { + match expr.node { + ast::ExprKind::While(_, ref loop_block, _) | + ast::ExprKind::WhileLet(_, _, ref loop_block, _) | + ast::ExprKind::ForLoop( _, _, ref loop_block, _) | + ast::ExprKind::Loop(ref loop_block, _) => func(loop_block), + _ => {}, + } +} + +/// If `stmt` is an if expression node with an else branch, calls func with the +/// following: +/// +/// - The if Expr, +/// - The if condition Expr, +/// - The then block of this if Expr, and +/// - The else expr. +/// +fn with_if_expr(stmt: &ast::Stmt, mut func: F) + where F: FnMut(&ast::Expr, &ast::Expr, &ast::Block, &ast::Expr) { + match stmt.node { + ast::StmtKind::Semi(ref e) | + ast::StmtKind::Expr(ref e) => { + if let ast::ExprKind::If(ref cond, ref if_block, Some(ref else_expr)) = e.node { + func(e, cond, if_block, else_expr); + } + }, + _ => { }, + } +} + +/// A type to distinguish between the two distinct cases this lint handles. +enum LintType { + ContinueInsideElseBlock, + ContinueInsideThenBlock, +} + +/// Data we pass around for construction of help messages. +struct LintData<'a> { + if_expr: &'a ast::Expr, // The `if` expr encountered in the above loop. + if_cond: &'a ast::Expr, // The condition expression for the above `if`. + if_block: &'a ast::Block, // The `then` block of the `if` statement. + else_expr: &'a ast::Expr, /* The `else` block of the `if` statement. + Note that we only work with `if` exprs that + have an `else` branch. */ + stmt_idx: usize, /* The 0-based index of the `if` statement in + the containing loop block. */ + block_stmts: &'a [ast::Stmt], // The statements of the loop block. +} + +const MSG_REDUNDANT_ELSE_BLOCK: &'static str = "This else block is redundant.\n"; + +const MSG_ELSE_BLOCK_NOT_NEEDED: &'static str = "There is no need for an explicit `else` block for this `if` expression\n"; + +const DROP_ELSE_BLOCK_AND_MERGE_MSG: &'static str = + "Consider dropping the else clause and merging the code that follows (in the loop) with the if block, like so:\n"; + +const DROP_ELSE_BLOCK_MSG: &'static str = + "Consider dropping the else clause, and moving out the code in the else block, like so:\n"; + + +fn emit_warning<'a>(ctx: &EarlyContext, + data: &'a LintData, + header: &str, + typ: LintType) { + + // snip is the whole *help* message that appears after the warning. + // message is the warning message. + // expr is the expression which the lint warning message refers to. + let (snip, message, expr) = match typ { + ContinueInsideElseBlock => { + (suggestion_snippet_for_continue_inside_else(ctx, data, header), + MSG_REDUNDANT_ELSE_BLOCK, + data.else_expr) + }, + ContinueInsideThenBlock => { + (suggestion_snippet_for_continue_inside_if(ctx, data, header), + MSG_ELSE_BLOCK_NOT_NEEDED, + data.if_expr) + } + }; + span_help_and_lint(ctx, NEEDLESS_CONTINUE, expr.span, message, + &format!("{}", snip)); +} + +fn suggestion_snippet_for_continue_inside_if<'a>(ctx: &EarlyContext, + data: &'a LintData, + header: &str) -> String { + let cond_code = &snippet(ctx, data.if_cond.span, "..").into_owned(); + + let if_code = &format!("if {} {{\n continue;\n}}\n", cond_code); + /* ^^^^--- Four spaces of indentation. */ + // region B + let else_code = &snippet(ctx, data.else_expr.span, "..").into_owned(); + let else_code = erode_block(&else_code); + let else_code = trim_indent(&else_code, false); + + let mut ret = String::from(header); + ret.push_str(&if_code); + ret.push_str(&else_code); + ret.push_str("\n..."); + ret +} + +fn suggestion_snippet_for_continue_inside_else<'a>(ctx: &EarlyContext, + data: &'a LintData, + header: &str) -> String +{ + let cond_code = &snippet(ctx, data.if_cond.span, "..").into_owned(); + let mut if_code = format!("if {} {{\n", cond_code); + + // Region B + let block_code = &snippet(ctx, data.if_block.span, "..").into_owned(); + let block_code = erode_block(block_code); + let block_code = trim_indent(&block_code, false); + let block_code = left_pad_lines_with_spaces(&block_code, 4usize); + + if_code.push_str(&block_code); + + // Region C + // These is the code in the loop block that follows the if/else construction + // we are complaining about. We want to pull all of this code into the + // `then` block of the `if` statement. + let to_annex = data.block_stmts[data.stmt_idx+1..] + .iter() + .map(|stmt| { + original_sp(ctx.sess().codemap(), stmt.span, DUMMY_SP) + }) + .map(|span| snippet_block(ctx, span, "..").into_owned()) + .collect::>().join("\n"); + + let mut ret = String::from(header); + ret.push_str(&align_snippets(&[&if_code, + "\n// Merged code follows...", + &to_annex])); + ret.push_str("\n}\n"); + ret +} + +fn check_and_warn<'a>(ctx: &EarlyContext, expr: &'a ast::Expr) { + with_loop_block(expr, |loop_block| { + for (i, stmt) in loop_block.stmts.iter().enumerate() { + with_if_expr(stmt, |if_expr, cond, then_block, else_expr| { + let data = &LintData { + stmt_idx: i, + if_expr: if_expr, + if_cond: cond, + if_block: then_block, + else_expr: else_expr, + block_stmts: &loop_block.stmts, + }; + if needless_continue_in_else(else_expr) { + emit_warning(ctx, data, DROP_ELSE_BLOCK_AND_MERGE_MSG, ContinueInsideElseBlock); + } else if is_first_block_stmt_continue(then_block) { + emit_warning(ctx, data, DROP_ELSE_BLOCK_MSG, ContinueInsideThenBlock); + } + }); + } + }); +} + +/// Eats at `s` from the end till a closing brace `}` is encountered, and then +/// continues eating till a non-whitespace character is found. +/// e.g., the string +/// +/// " +/// { +/// let x = 5; +/// } +/// " +/// +/// is transformed to +/// +/// " +/// { +/// let x = 5;" +/// +fn erode_from_back(s: &str) -> String { + let mut ret = String::from(s); + while ret.pop().map_or(false, |c| c != '}') { } + while let Some(c) = ret.pop() { + if !c.is_whitespace() { + ret.push(c); + break; + } + } + ret +} + +fn erode_from_front(s: &str) -> String { + s.chars() + .skip_while(|c| c.is_whitespace()) + .skip_while(|c| *c == '{') + .skip_while(|c| *c == '\n') + .collect::() +} + +fn erode_block(s: &str) -> String { + erode_from_back(&erode_from_front(s)) +} + +fn is_all_whitespace(s: &str) -> bool { s.chars().all(|c| c.is_whitespace()) } + +/// Returns true if a string is empty or just spaces. +fn is_null(s: &str) -> bool { s.is_empty() || is_all_whitespace(s) } + +/// Returns the indentation level of a string. It just returns the count of +/// whitespace characters in the string before a non-whitespace character +/// is encountered. +fn indent_level(s: &str) -> usize { + s.chars() + .enumerate() + .find(|&(_, c)| !c.is_whitespace()) + .map_or(0usize, |(i, _)| i) +} + +/// Trims indentation from a snippet such that the line with the minimum +/// indentation has no indentation after the trasformation. +fn trim_indent(s: &str, skip_first_line: bool) -> String { + let min_indent_level = s.lines() + .filter(|line| !is_null(line)) + .skip(skip_first_line as usize) + .map(indent_level) + .min() + .unwrap_or(0usize); + let ret = s.lines().map(|line| { + if is_null(line) { + String::from(line) + } else { + line.chars() + .enumerate() + .skip_while(|&(i, c)| c.is_whitespace() && i < min_indent_level) + .map(|pair| pair.1) + .collect::() + } + }).collect::>(); + ret.join("\n") +} + +/// Add `n` spaces to the left of `s`. +fn left_pad_with_spaces(s: &str, n: usize) -> String { + let mut new_s = std::iter::repeat(' '/* <-space */).take(n).collect::(); + new_s.push_str(s); + new_s +} + +/// Add `n` spaces to the left of each line in `s` and return the result +/// in a new String. +fn left_pad_lines_with_spaces(s: &str, n: usize) -> String { + s.lines() + .map(|line| left_pad_with_spaces(line, n)) + .collect::>() + .join("\n") +} + +/// Remove upto `n` whitespace characters from the beginning of `s`. +fn remove_whitespace_from_left(s: &str, n: usize) -> String { + s.chars() + .enumerate() + .skip_while(|&(i, c)| i < n && c.is_whitespace()) + .map(|(_, c)| c) + .collect::() +} + +/// Aligns two snippets such that the indentation level of the last non-empty, +/// non-space line of the first snippet matches the first non-empty, non-space +/// line of the second. +fn align_two_snippets(s: &str, t: &str) -> String { + // indent level of the last nonempty, non-whitespace line of s. + let target_ilevel = s.lines() + .rev() + .skip_while(|line| line.is_empty() || is_all_whitespace(line)) + .next() + .map_or(0usize, indent_level); + + // We want to align the first nonempty, non-all-whitespace line of t to + // have the same indent level as target_ilevel + let level = t.lines() + .skip_while(|line| line.is_empty() || is_all_whitespace(line)) + .next() + .map_or(0usize, indent_level); + + let add_or_not_remove = target_ilevel > level; /* when true, we add spaces, + otherwise eat. */ + + let delta = if add_or_not_remove { + target_ilevel - level + } else { + level - target_ilevel + }; + + let new_t = t.lines() + .filter(|line| !is_null(line)) + .map(|line| if add_or_not_remove { + left_pad_with_spaces(line, delta) + } else { + remove_whitespace_from_left(line, delta) + }) + .collect::>().join("\n"); + + format!("{}\n{}", s, new_t) +} + +fn align_snippets(xs: &[&str]) -> String { + match xs.len() { + 0 => String::from(""), + _ => { + let mut ret = String::new(); + ret.push_str(xs[0]); + for x in xs.iter().skip(1usize) { + ret = align_two_snippets(&ret, x); + } + ret + } + } +} +