Lints and suggestions for the Nix programming language
at main 128 lines 3.9 kB view raw
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}