From 93d097eb12fc0b7e34187d2cac19b0059fa18ed4 Mon Sep 17 00:00:00 2001 From: Oliver Schneider <git-spam-no-reply9815368754983@oli-obk.de> Date: Wed, 23 Mar 2016 12:19:13 +0100 Subject: [PATCH] better simplification --- Cargo.toml | 1 + README.md | 3 +- src/booleans.rs | 155 ++++++++++++++++++++ src/lib.rs | 8 + tests/compile-fail/block_in_if_condition.rs | 2 +- tests/compile-fail/eq_op.rs | 2 + 6 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 src/booleans.rs diff --git a/Cargo.toml b/Cargo.toml index 3bb7a82f6e2..93886e214e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ regex_macros = { version = "0.1.33", optional = true } semver = "0.2.1" toml = "0.1" unicode-normalization = "0.1" +quine-mc_cluskey = "0.2" [dev-dependencies] compiletest_rs = "0.1.0" diff --git a/README.md b/README.md index 43caa80543a..cd8b0257c6e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Table of contents: * [License](#license) ##Lints -There are 137 lints included in this crate: +There are 138 lints included in this crate: name | default | meaning ---------------------------------------------------------------------------------------------------------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ @@ -97,6 +97,7 @@ name [new_without_default](https://github.com/Manishearth/rust-clippy/wiki#new_without_default) | warn | `fn new() -> Self` method without `Default` implementation [no_effect](https://github.com/Manishearth/rust-clippy/wiki#no_effect) | warn | statements with no effect [non_ascii_literal](https://github.com/Manishearth/rust-clippy/wiki#non_ascii_literal) | allow | using any literal non-ASCII chars in a string literal; suggests using the \\u escape instead +[nonminimal_bool](https://github.com/Manishearth/rust-clippy/wiki#nonminimal_bool) | warn | checks for boolean expressions that can be written more concisely [nonsensical_open_options](https://github.com/Manishearth/rust-clippy/wiki#nonsensical_open_options) | warn | nonsensical combination of options for opening a file [ok_expect](https://github.com/Manishearth/rust-clippy/wiki#ok_expect) | warn | using `ok().expect()`, which gives worse error messages than calling `expect` directly on the Result [option_map_unwrap_or](https://github.com/Manishearth/rust-clippy/wiki#option_map_unwrap_or) | warn | using `Option.map(f).unwrap_or(a)`, which is more succinctly expressed as `map_or(a, f)` diff --git a/src/booleans.rs b/src/booleans.rs new file mode 100644 index 00000000000..90e4dee9769 --- /dev/null +++ b/src/booleans.rs @@ -0,0 +1,155 @@ +use rustc::lint::*; +use rustc_front::hir::*; +use rustc_front::intravisit::*; +use syntax::ast::LitKind; +use utils::{span_lint_and_then, in_macro, snippet_opt}; + +/// **What it does:** This lint checks for boolean expressions that can be written more concisely +/// +/// **Why is this bad?** Readability of boolean expressions suffers from unnecesessary duplication +/// +/// **Known problems:** None +/// +/// **Example:** `if a && b || a` should be `if a` +declare_lint! { + pub NONMINIMAL_BOOL, Warn, + "checks for boolean expressions that can be written more concisely" +} + +#[derive(Copy,Clone)] +pub struct NonminimalBool; + +impl LintPass for NonminimalBool { + fn get_lints(&self) -> LintArray { + lint_array!(NONMINIMAL_BOOL) + } +} + +impl LateLintPass for NonminimalBool { + fn check_crate(&mut self, cx: &LateContext, krate: &Crate) { + krate.visit_all_items(&mut NonminimalBoolVisitor(cx)) + } +} + +struct NonminimalBoolVisitor<'a, 'tcx: 'a>(&'a LateContext<'a, 'tcx>); + +use quine_mc_cluskey::Bool; +struct Hir2Qmm<'tcx>(Vec<&'tcx Expr>); + +impl<'tcx> Hir2Qmm<'tcx> { + fn extract(&mut self, op: BinOp_, a: &[&'tcx Expr], mut v: Vec<Bool>) -> Result<Vec<Bool>, String> { + for a in a { + if let ExprBinary(binop, ref lhs, ref rhs) = a.node { + if binop.node == op { + v = self.extract(op, &[lhs, rhs], v)?; + continue; + } + } + v.push(self.run(a)?); + } + Ok(v) + } + + fn run(&mut self, e: &'tcx Expr) -> Result<Bool, String> { + match e.node { + ExprUnary(UnNot, ref inner) => return Ok(Bool::Not(box self.run(inner)?)), + ExprBinary(binop, ref lhs, ref rhs) => { + match binop.node { + BiOr => return Ok(Bool::Or(self.extract(BiOr, &[lhs, rhs], Vec::new())?)), + BiAnd => return Ok(Bool::And(self.extract(BiAnd, &[lhs, rhs], Vec::new())?)), + _ => {}, + } + }, + ExprLit(ref lit) => { + match lit.node { + LitKind::Bool(true) => return Ok(Bool::True), + LitKind::Bool(false) => return Ok(Bool::False), + _ => {}, + } + }, + _ => {}, + } + let n = self.0.len(); + self.0.push(e); + if n < 32 { + #[allow(cast_possible_truncation)] + Ok(Bool::Term(n as u8)) + } else { + Err("too many literals".to_owned()) + } + } +} + +fn suggest(cx: &LateContext, suggestion: &Bool, terminals: &[&Expr]) -> String { + fn recurse(cx: &LateContext, suggestion: &Bool, terminals: &[&Expr], mut s: String) -> String { + use quine_mc_cluskey::Bool::*; + match *suggestion { + True => { + s.extend("true".chars()); + s + }, + False => { + s.extend("false".chars()); + s + }, + Not(ref inner) => { + s.push('!'); + recurse(cx, inner, terminals, s) + }, + And(ref v) => { + s = recurse(cx, &v[0], terminals, s); + for inner in &v[1..] { + s.extend(" && ".chars()); + s = recurse(cx, inner, terminals, s); + } + s + }, + Or(ref v) => { + s = recurse(cx, &v[0], terminals, s); + for inner in &v[1..] { + s.extend(" || ".chars()); + s = recurse(cx, inner, terminals, s); + } + s + }, + Term(n) => { + s.extend(snippet_opt(cx, terminals[n as usize].span).expect("don't try to improve booleans created by macros").chars()); + s + } + } + } + recurse(cx, suggestion, terminals, String::new()) +} + +impl<'a, 'tcx> NonminimalBoolVisitor<'a, 'tcx> { + fn bool_expr(&self, e: &Expr) { + let mut h2q = Hir2Qmm(Vec::new()); + if let Ok(expr) = h2q.run(e) { + let simplified = expr.simplify(); + if !simplified.iter().any(|s| *s == expr) { + span_lint_and_then(self.0, NONMINIMAL_BOOL, e.span, "this boolean expression can be simplified", |db| { + for suggestion in &simplified { + db.span_suggestion(e.span, "try", suggest(self.0, suggestion, &h2q.0)); + } + }); + } + } + } +} + +impl<'a, 'v, 'tcx> Visitor<'v> for NonminimalBoolVisitor<'a, 'tcx> { + fn visit_expr(&mut self, e: &'v Expr) { + if in_macro(self.0, e.span) { return } + match e.node { + ExprBinary(binop, _, _) if binop.node == BiOr || binop.node == BiAnd => self.bool_expr(e), + ExprUnary(UnNot, ref inner) => { + if self.0.tcx.node_types()[&inner.id].is_bool() { + self.bool_expr(e); + } else { + walk_expr(self, e); + } + }, + _ => walk_expr(self, e), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 0a51385f6f2..575e318e7ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,8 @@ #![feature(iter_arith)] #![feature(custom_attribute)] #![feature(slice_patterns)] +#![feature(question_mark)] +#![feature(stmt_expr_attributes)] #![allow(indexing_slicing, shadow_reuse, unknown_lints)] // this only exists to allow the "dogfood" integration test to work @@ -35,6 +37,9 @@ extern crate semver; // for regex checking extern crate regex_syntax; +// for finding minimal boolean expressions +extern crate quine_mc_cluskey; + extern crate rustc_plugin; extern crate rustc_const_eval; use rustc_plugin::Registry; @@ -50,6 +55,7 @@ pub mod attrs; pub mod bit_mask; pub mod blacklisted_name; pub mod block_in_if_condition; +pub mod booleans; pub mod collapsible_if; pub mod copies; pub mod cyclomatic_complexity; @@ -149,6 +155,7 @@ pub fn plugin_registrar(reg: &mut Registry) { // end deprecated lints, do not remove this comment, it’s used in `update_lints` reg.register_late_lint_pass(box types::TypePass); + reg.register_late_lint_pass(box booleans::NonminimalBool); reg.register_late_lint_pass(box misc::TopLevelRefPass); reg.register_late_lint_pass(box misc::CmpNan); reg.register_late_lint_pass(box eq_op::EqOp); @@ -260,6 +267,7 @@ pub fn plugin_registrar(reg: &mut Registry) { blacklisted_name::BLACKLISTED_NAME, block_in_if_condition::BLOCK_IN_IF_CONDITION_EXPR, block_in_if_condition::BLOCK_IN_IF_CONDITION_STMT, + booleans::NONMINIMAL_BOOL, collapsible_if::COLLAPSIBLE_IF, copies::IF_SAME_THEN_ELSE, copies::IFS_SAME_COND, diff --git a/tests/compile-fail/block_in_if_condition.rs b/tests/compile-fail/block_in_if_condition.rs index 5158e74c1e0..19fa949fb84 100644 --- a/tests/compile-fail/block_in_if_condition.rs +++ b/tests/compile-fail/block_in_if_condition.rs @@ -67,7 +67,7 @@ fn pred_test() { fn condition_is_normal() -> i32 { let x = 3; - if true && x == 3 { + if true && x == 3 { //~ WARN this boolean expression can be simplified 6 } else { 10 diff --git a/tests/compile-fail/eq_op.rs b/tests/compile-fail/eq_op.rs index 487185516bf..bd76695e6ad 100644 --- a/tests/compile-fail/eq_op.rs +++ b/tests/compile-fail/eq_op.rs @@ -38,7 +38,9 @@ fn main() { 1 - 1; //~ERROR equal expressions 1 / 1; //~ERROR equal expressions true && true; //~ERROR equal expressions + //~|WARN this boolean expression can be simplified true || true; //~ERROR equal expressions + //~|WARN this boolean expression can be simplified let mut a = vec![1]; a == a; //~ERROR equal expressions