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::{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}