Lints and suggestions for the Nix programming language
at main 95 lines 2.4 kB view raw
1use crate::{Metadata, Report, Rule, Suggestion, make}; 2 3use macros::lint; 4use rnix::{ 5 NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, 6 ast::{AttrSet, Entry, HasEntry as _, Lambda, Param}, 7}; 8use rowan::ast::AstNode as _; 9 10/// ## What it does 11/// Checks for an empty variadic pattern: `{...}`, in a function 12/// argument. 13/// 14/// ## Why is this bad? 15/// The intention with empty patterns is not instantly obvious. Prefer 16/// an underscore identifier instead, to indicate that the argument 17/// is being ignored. 18/// 19/// ## Example 20/// 21/// ```nix 22/// client = { ... }: { 23/// services.irmaseal-pkg.enable = true; 24/// }; 25/// ``` 26/// 27/// Replace the empty variadic pattern with `_` to indicate that you 28/// intend to ignore the argument: 29/// 30/// ```nix 31/// client = _: { 32/// services.irmaseal-pkg.enable = true; 33/// }; 34/// ``` 35#[lint( 36 name = "empty_pattern", 37 note = "Found empty pattern in function argument", 38 code = 10, 39 match_with = SyntaxKind::NODE_LAMBDA 40)] 41struct EmptyPattern; 42 43impl Rule for EmptyPattern { 44 fn validate(&self, node: &SyntaxElement) -> Option<Report> { 45 let NodeOrToken::Node(node) = node else { 46 return None; 47 }; 48 49 let lambda_expr = Lambda::cast(node.clone())?; 50 51 let Some(Param::Pattern(pattern)) = lambda_expr.param() else { 52 return None; 53 }; 54 55 // no patterns within `{ }` 56 if pattern.pat_entries().count() != 0 { 57 return None; 58 } 59 60 // pattern is not bound 61 if pattern.pat_bind().is_some() { 62 return None; 63 } 64 65 if is_module(lambda_expr.body()?.syntax()) { 66 return None; 67 } 68 69 Some(self.report().suggest( 70 pattern.syntax().text_range(), 71 "This pattern is empty, use `_` instead", 72 Suggestion::with_replacement( 73 pattern.syntax().text_range(), 74 make::ident("_").syntax().clone(), 75 ), 76 )) 77 } 78} 79 80fn is_module(body: &SyntaxNode) -> bool { 81 let Some(attr_set) = AttrSet::cast(body.clone()) else { 82 return false; 83 }; 84 85 attr_set 86 .entries() 87 .filter_map(|e| { 88 let Entry::AttrpathValue(attrpath_value) = e else { 89 return None; 90 }; 91 92 attrpath_value.attrpath() 93 }) 94 .any(|k| k.to_string() == "imports") 95}