Lints and suggestions for the Nix programming language
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}