aboutsummaryrefslogtreecommitdiff
path: root/lib/src/lints/bool_comparison.rs
blob: ef7f5d203b8021d6db8d9aa79637d4371da6befd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
use crate::{make, session::SessionInfo, Metadata, Report, Rule, Suggestion};

use if_chain::if_chain;
use macros::lint;
use rnix::{
    types::{BinOp, BinOpKind, Ident, TokenWrapper, TypedNode},
    NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode,
};

/// ## What it does
/// Checks for expressions of the form `x == true`, `x != true` and
/// suggests using the variable directly.
///
/// ## Why is this bad?
/// Unnecessary code.
///
/// ## Example
/// Instead of checking the value of `x`:
///
/// ```nix
/// if x == true then 0 else 1
/// ```
///
/// Use `x` directly:
///
/// ```nix
/// if x then 0 else 1
/// ```
#[lint(
    name = "bool_comparison",
    note = "Unnecessary comparison with boolean",
    code = 1,
    match_with = SyntaxKind::NODE_BIN_OP
)]
struct BoolComparison;

impl Rule for BoolComparison {
    fn validate(&self, node: &SyntaxElement, _sess: &SessionInfo) -> Option<Report> {
        if_chain! {
            if let NodeOrToken::Node(node) = node;
            if let Some(bin_expr) = BinOp::cast(node.clone());
            if let Some(lhs) = bin_expr.lhs();
            if let Some(rhs) = bin_expr.rhs();

            if let op@(BinOpKind::Equal | BinOpKind::NotEqual) = bin_expr.operator();
            let (non_bool_side, bool_side) = if boolean_ident(&lhs).is_some() {
                (rhs, lhs)
            } else if boolean_ident(&rhs).is_some() {
                (lhs, rhs)
            } else {
                return None
            };
            then {
                let at = node.text_range();
                let replacement = {
                    match (boolean_ident(&bool_side).unwrap(), op == BinOpKind::Equal) {
                        (NixBoolean::True, true) | (NixBoolean::False, false) => {
                            // `a == true`, `a != false` replace with just `a`
                            non_bool_side.clone()
                        },
                        (NixBoolean::True, false) | (NixBoolean::False, true) => {
                            // `a != true`, `a == false` replace with `!a`
                            match non_bool_side.kind() {
                                SyntaxKind::NODE_APPLY
                                    | SyntaxKind::NODE_PAREN
                                    | SyntaxKind::NODE_IDENT => {
                                    // do not parenthsize the replacement
                                    make::unary_not(&non_bool_side).node().clone()
                                },
                                SyntaxKind::NODE_BIN_OP => {
                                    let inner = BinOp::cast(non_bool_side.clone()).unwrap();
                                    // `!a ? b`, no paren required
                                    if inner.operator() == BinOpKind::IsSet {
                                        make::unary_not(&non_bool_side).node().clone()
                                    } else {
                                        let parens = make::parenthesize(&non_bool_side);
                                        make::unary_not(parens.node()).node().clone()
                                    }
                                },
                                _ => {
                                    let parens = make::parenthesize(&non_bool_side);
                                    make::unary_not(parens.node()).node().clone()
                                }
                            }
                        },
                    }
                };
                let message = format!(
                    "Comparing `{}` with boolean literal `{}`",
                    non_bool_side,
                    bool_side
                );
                Some(self.report().suggest(at, message, Suggestion::new(at, replacement)))
            } else {
                None
            }
        }
    }
}

enum NixBoolean {
    True,
    False,
}

// not entirely accurate, underhanded nix programmers might write `true = false`
fn boolean_ident(node: &SyntaxNode) -> Option<NixBoolean> {
    Ident::cast(node.clone())
        .map(|ident_expr| match ident_expr.as_str() {
            "true" => Some(NixBoolean::True),
            "false" => Some(NixBoolean::False),
            _ => None,
        })
        .flatten()
}