]> git.lizzy.rs Git - rust.git/commitdiff
Needless continue: This is a complete rewrite of this lint as an early-pass lint.
authorYati Sagade <yati.sagade@gmail.com>
Sun, 21 Aug 2016 21:18:58 +0000 (23:18 +0200)
committerYati Sagade <yati.sagade@gmail.com>
Sat, 8 Apr 2017 21:30:34 +0000 (23:30 +0200)
The previous version had troubles computing suggestions without macro expansions
creeping in. This fixes it by using original_sp, which works on AST nodes.

CHANGELOG.md
clippy_lints/src/lib.rs
clippy_lints/src/needless_continue.rs [new file with mode: 0644]

index 1aee256f8bbaecb9f2d377d4a689322f07524663..e02f9cd1b7b68511b81a00a0e228ce30d777842f 100644 (file)
@@ -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
index f84b023c3d8cfaebf33184c0f2eafd6142b7568a..ef85b029617dc642769bbe0277e5adafa40f191d 100644 (file)
@@ -107,6 +107,7 @@ macro_rules! declare_restriction_lint {
 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 (file)
index 0000000..042f45c
--- /dev/null
@@ -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<F>(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<F>(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::<Vec<_>>().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::<String>()
+}
+
+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::<String>()
+        }
+    }).collect::<Vec<String>>();
+    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::<String>();
+    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::<Vec<_>>()
+     .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::<String>()
+}
+
+/// 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::<Vec<_>>().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
+        }
+    }
+}
+