Lints and suggestions for the Nix programming language
1use crate::{Metadata, Report, Rule, Suggestion, make};
2
3use macros::lint;
4use rnix::{
5 NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode,
6 ast::{BinOp, BinOpKind, Ident},
7};
8use rowan::ast::AstNode as _;
9
10/// ## What it does
11/// Checks for expressions of the form `x == true`, `x != true` and
12/// suggests using the variable directly.
13///
14/// ## Why is this bad?
15/// Unnecessary code.
16///
17/// ## Example
18/// Instead of checking the value of `x`:
19///
20/// ```nix
21/// if x == true then 0 else 1
22/// ```
23///
24/// Use `x` directly:
25///
26/// ```nix
27/// if x then 0 else 1
28/// ```
29#[lint(
30 name = "bool_comparison",
31 note = "Unnecessary comparison with boolean",
32 code = 1,
33 match_with = SyntaxKind::NODE_BIN_OP
34)]
35struct BoolComparison;
36
37impl Rule for BoolComparison {
38 fn validate(&self, node: &SyntaxElement) -> Option<Report> {
39 let NodeOrToken::Node(node) = node else {
40 return None;
41 };
42 let bin_expr = BinOp::cast(node.clone())?;
43 let (lhs, rhs) = (bin_expr.lhs()?, bin_expr.rhs()?);
44 let (lhs, rhs) = (lhs.syntax(), rhs.syntax());
45 let op = EqualityBinOpKind::try_from(bin_expr.operator()?)?;
46
47 let (bool_side, non_bool_side): (NixBoolean, &SyntaxNode) =
48 match (boolean_ident(lhs), boolean_ident(rhs)) {
49 (None, None) => return None,
50 (None, Some(bool)) => (bool, lhs),
51 (Some(bool), _) => (bool, rhs),
52 };
53
54 let replacement = match (&bool_side, op) {
55 (NixBoolean::True, EqualityBinOpKind::Equal)
56 | (NixBoolean::False, EqualityBinOpKind::NotEqual) => {
57 // `a == true`, `a != false` replace with just `a`
58 non_bool_side.clone()
59 }
60 (NixBoolean::True, EqualityBinOpKind::NotEqual)
61 | (NixBoolean::False, EqualityBinOpKind::Equal) => {
62 // `a != true`, `a == false` replace with `!a`
63 match non_bool_side.kind() {
64 SyntaxKind::NODE_APPLY
65 | SyntaxKind::NODE_PAREN
66 | SyntaxKind::NODE_IDENT
67 | SyntaxKind::NODE_HAS_ATTR => {
68 // do not parenthsize the replacement
69 make::unary_not(non_bool_side).syntax().clone()
70 }
71 _ => {
72 let parens = make::parenthesize(non_bool_side);
73 make::unary_not(parens.syntax()).syntax().clone()
74 }
75 }
76 }
77 };
78 let at = node.text_range();
79 Some(self.report().suggest(
80 at,
81 format!("Comparing `{non_bool_side}` with boolean literal `{bool_side}`"),
82 Suggestion::with_replacement(at, replacement),
83 ))
84 }
85}
86
87enum NixBoolean {
88 True,
89 False,
90}
91
92#[derive(Debug)]
93enum EqualityBinOpKind {
94 NotEqual,
95 Equal,
96}
97
98impl EqualityBinOpKind {
99 /// Try to create from a `BinOpKind`
100 ///
101 /// Returns an option, not a Result
102 fn try_from(bin_op_kind: BinOpKind) -> Option<Self> {
103 match bin_op_kind {
104 BinOpKind::Equal => Some(Self::Equal),
105 BinOpKind::NotEqual => Some(Self::NotEqual),
106 _ => None,
107 }
108 }
109}
110
111impl std::fmt::Display for NixBoolean {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 let s = match self {
114 Self::True => "true",
115 Self::False => "false",
116 };
117 write!(f, "{s}")
118 }
119}
120
121// not entirely accurate, underhanded nix programmers might write `true = false`
122fn boolean_ident(node: &SyntaxNode) -> Option<NixBoolean> {
123 Ident::cast(node.clone()).and_then(|ident_expr| match ident_expr.to_string().as_str() {
124 "true" => Some(NixBoolean::True),
125 "false" => Some(NixBoolean::False),
126 _ => None,
127 })
128}