feat(natu): lint redundant parameter forwarding

This commit was merged in pull request #114.
This commit is contained in:
2026-09-30 00:19:27 -04:00
parent becbb50491
commit 2265dfc558
5 changed files with 320 additions and 9 deletions

View File

@@ -0,0 +1,244 @@
use crate::ast;
pub struct Finding {
pub range: ast::SourceRange,
}
pub fn check(expr: &ast::Expr) -> Vec<Finding> {
let mut findings = Vec::new();
check_expr(expr, &[], &mut findings);
findings
}
fn check_expr(expr: &ast::Expr, outer_parameters: &[ast::Ident], findings: &mut Vec<Finding>) {
match expr {
ast::Expr::Let { bindings, body, .. } => {
check_let(bindings, body, outer_parameters, findings);
for binding in bindings {
check_expr(&binding.value, outer_parameters, findings);
}
check_expr(body, outer_parameters, findings);
}
ast::Expr::AttrSet(bindings) => {
for binding in bindings {
check_expr(&binding.value, outer_parameters, findings);
}
}
ast::Expr::Lambda { parameters, body } => check_expr(body, parameters, findings),
ast::Expr::Apply(application) => {
check_expr(&application.lambda, outer_parameters, findings);
check_expr(&application.argument, outer_parameters, findings);
}
ast::Expr::Other(expressions) => {
for expression in expressions {
check_expr(expression, outer_parameters, findings);
}
}
ast::Expr::Ident(_) | ast::Expr::Select(_) => { }
}
}
fn check_let(
bindings: &[ast::Binding],
let_body: &ast::Expr,
outer_parameters: &[ast::Ident],
findings: &mut Vec<Finding>,
) {
for binding in bindings {
let parameters = curried_parameters(&binding.value);
for (parameter_index, parameter) in parameters.iter().enumerate() {
if !outer_parameters
.iter()
.any(|outer_parameter| outer_parameter.name == parameter.name)
{
continue;
}
let usage = helper_usage(
bindings,
let_body,
&binding.key.name,
&parameter.name,
parameter_index,
);
if usage.calls > 0 && !usage.invalid {
findings.push(Finding {
range: binding.range,
});
break;
}
}
}
}
#[derive(Default)]
struct Usage {
calls: usize,
invalid: bool,
}
fn helper_usage(
bindings: &[ast::Binding],
body: &ast::Expr,
helper_name: &str,
parameter_name: &str,
parameter_index: usize,
) -> Usage {
let mut usage = Usage::default();
for binding in bindings {
inspect_usage(
&binding.value,
helper_name,
parameter_name,
parameter_index,
&mut usage,
);
}
inspect_usage(body, helper_name, parameter_name, parameter_index, &mut usage);
usage
}
fn inspect_usage(
expr: &ast::Expr,
helper_name: &str,
parameter_name: &str,
parameter_index: usize,
usage: &mut Usage,
) {
match expr {
ast::Expr::Ident(ident) => {
if ident.name == helper_name {
usage.invalid = true;
}
}
ast::Expr::Select(_) => { }
ast::Expr::Apply(_) => {
let (lambda, arguments) = application_parts(expr);
if is_ident(lambda, helper_name) {
let Some(argument) = arguments.get(parameter_index) else {
usage.invalid = true;
return;
};
if is_forwarded_parameter(argument, parameter_name) {
usage.calls += 1;
} else {
usage.invalid = true;
}
for argument in arguments {
inspect_usage(
argument,
helper_name,
parameter_name,
parameter_index,
usage,
);
}
} else {
for argument in arguments {
inspect_usage(
argument,
helper_name,
parameter_name,
parameter_index,
usage,
);
}
inspect_usage(lambda, helper_name, parameter_name, parameter_index, usage);
}
}
ast::Expr::Let { bindings, body, .. } => {
if bindings.iter().any(|binding| {
binding.key.name == helper_name || binding.key.name == parameter_name
}) {
return;
}
for binding in bindings {
inspect_usage(
&binding.value,
helper_name,
parameter_name,
parameter_index,
usage,
);
}
inspect_usage(body, helper_name, parameter_name, parameter_index, usage);
}
ast::Expr::AttrSet(bindings) => {
for binding in bindings {
inspect_usage(
&binding.value,
helper_name,
parameter_name,
parameter_index,
usage,
);
}
}
ast::Expr::Lambda { parameters, body } => {
if parameters.iter().any(|parameter| {
parameter.name == helper_name || parameter.name == parameter_name
}) {
return;
}
inspect_usage(body, helper_name, parameter_name, parameter_index, usage);
}
ast::Expr::Other(expressions) => {
for expression in expressions {
inspect_usage(
expression,
helper_name,
parameter_name,
parameter_index,
usage,
);
}
}
}
}
fn curried_parameters(expr: &ast::Expr) -> Vec<&ast::Ident> {
let mut parameters = Vec::new();
let mut body = expr;
while let ast::Expr::Lambda {
parameters: lambda_parameters,
body: lambda_body,
} = body
{
parameters.extend(lambda_parameters);
body = lambda_body;
}
parameters
}
fn application_parts(expr: &ast::Expr) -> (&ast::Expr, Vec<&ast::Expr>) {
let mut arguments = Vec::new();
let mut lambda = unwrap_expr(expr);
while let ast::Expr::Apply(application) = lambda {
arguments.push(&*application.argument);
lambda = unwrap_expr(&application.lambda);
}
arguments.reverse();
(lambda, arguments)
}
fn is_ident(expr: &ast::Expr, name: &str) -> bool {
matches!(unwrap_expr(expr), ast::Expr::Ident(ident) if ident.name == name)
}
fn is_forwarded_parameter(expr: &ast::Expr, parameter_name: &str) -> bool {
match unwrap_expr(expr) {
ast::Expr::Ident(ident) => ident.name == parameter_name,
ast::Expr::AttrSet(bindings) => matches!(
bindings.as_slice(),
[binding] if binding.key.name == parameter_name && is_ident(&binding.value, parameter_name)
),
_ => false,
}
}
fn unwrap_expr(expr: &ast::Expr) -> &ast::Expr {
match expr {
ast::Expr::Other(expressions) if expressions.len() == 1 => unwrap_expr(&expressions[0]),
_ => expr,
}
}