Lints and suggestions for the Nix programming language
at main 99 lines 2.6 kB view raw
1use crate::{Metadata, Report, Rule, Suggestion}; 2 3use macros::lint; 4use rnix::{ 5 NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, 6 ast::{Expr, Ident, Lambda, Param}, 7}; 8use rowan::ast::AstNode as _; 9 10/// ## What it does 11/// Checks for eta-reducible functions, i.e.: converts lambda 12/// expressions into free standing functions where applicable. 13/// 14/// ## Why is this bad? 15/// Oftentimes, eta-reduction results in code that is more natural 16/// to read. 17/// 18/// ## Example 19/// 20/// ```nix 21/// let 22/// double = i: 2 * i; 23/// in 24/// map (x: double x) [ 1 2 3 ] 25/// ``` 26/// 27/// The lambda passed to the `map` function is eta-reducible, and the 28/// result reads more naturally: 29/// 30/// ```nix 31/// let 32/// double = i: 2 * i; 33/// in 34/// map double [ 1 2 3 ] 35/// ``` 36#[lint( 37 name = "eta_reduction", 38 note = "This function expression is eta reducible", 39 code = 7, 40 match_with = SyntaxKind::NODE_LAMBDA 41)] 42struct EtaReduction; 43 44impl Rule for EtaReduction { 45 fn validate(&self, node: &SyntaxElement) -> Option<Report> { 46 let NodeOrToken::Node(node) = node else { 47 return None; 48 }; 49 50 let lambda_expr = Lambda::cast(node.clone())?; 51 52 let Some(Param::IdentParam(ident_param)) = lambda_expr.param() else { 53 return None; 54 }; 55 56 let ident = ident_param.ident()?; 57 58 let Some(Expr::Apply(body)) = lambda_expr.body() else { 59 return None; 60 }; 61 62 let Some(Expr::Ident(body_ident)) = body.argument() else { 63 return None; 64 }; 65 66 if ident.to_string() != body_ident.to_string() { 67 return None; 68 } 69 70 let lambda_node = body.lambda()?; 71 72 if mentions_ident(&ident, lambda_node.syntax()) { 73 return None; 74 } 75 76 // lambda body should be no more than a single Ident to 77 // retain code readability 78 let Expr::Ident(_) = lambda_node else { 79 return None; 80 }; 81 82 let at = node.text_range(); 83 let replacement = body.lambda()?; 84 let message = format!("Found eta-reduction: `{}`", replacement.syntax().text()); 85 Some(self.report().suggest( 86 at, 87 message, 88 Suggestion::with_replacement(at, replacement.syntax().clone()), 89 )) 90 } 91} 92 93fn mentions_ident(ident: &Ident, node: &SyntaxNode) -> bool { 94 if let Some(node_ident) = Ident::cast(node.clone()) { 95 node_ident.to_string() == ident.to_string() 96 } else { 97 node.children().any(|child| mentions_ident(ident, &child)) 98 } 99}