From 1cbdcee8ac8b966d846b1b07f3dd18b9546fb684 Mon Sep 17 00:00:00 2001 From: alexion Date: Thu, 1 Oct 2026 21:54:09 -0400 Subject: [PATCH] feat(natu): ignore imported let bindings --- src/natu/natu_test.rs | 18 ++++++++++++++++++ src/natu/rules/single_use_binding.rs | 3 ++- src/natu/scope_graph.rs | 24 ++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/natu/natu_test.rs b/src/natu/natu_test.rs index ccd02fa..475afc6 100644 --- a/src/natu/natu_test.rs +++ b/src/natu/natu_test.rs @@ -199,6 +199,24 @@ in Ok(()) } +#[gtest] +fn natu_accepts_single_use_import_binding() -> googletest::Result<()> { + let report = natu::future_sight(vec![SourceFile { + path: PathBuf::from("testdata/import_binding.nix"), + source: r#" +let + imported = import ./module.nix { }; +in +imported +"# + .into(), + }]) + .or_fail()?; + + expect_that!(report.is_clean(), is_true()); + Ok(()) +} + #[gtest] fn natu_reports_single_use_binding() -> googletest::Result<()> { let report = natu::future_sight(vec![SourceFile { diff --git a/src/natu/rules/single_use_binding.rs b/src/natu/rules/single_use_binding.rs index adf03e9..54aaedd 100644 --- a/src/natu/rules/single_use_binding.rs +++ b/src/natu/rules/single_use_binding.rs @@ -38,7 +38,8 @@ fn is_single_use(graph: &ScopeGraph, let_scope: &LetScope, binding: &Binding) -> .filter(|reference| graph.resolve(**reference).contains(&binding.declaration)) .collect::>(); - self_references == 0 + !binding.is_import + && self_references == 0 && external_references.len() == 1 && (binding.is_lambda || !graph.crosses_lambda_scope(*external_references[0], binding.declaration)) diff --git a/src/natu/scope_graph.rs b/src/natu/scope_graph.rs index 7ad0526..59c3925 100644 --- a/src/natu/scope_graph.rs +++ b/src/natu/scope_graph.rs @@ -69,6 +69,7 @@ impl ScopeGraph { declaration: self.add_declaration(&binding.key.name, scope), name: binding.key.name.clone(), references: Vec::new(), + is_import: is_import(&binding.value), is_lambda: matches!(binding.value, ast::Expr::Lambda { .. }), range: binding.range, }) @@ -191,6 +192,28 @@ impl ScopeGraph { } } +fn is_import(expr: &ast::Expr) -> bool { + match unwrap_expr(expr) { + ast::Expr::Apply(application) => is_import_callee(&application.lambda), + _ => false, + } +} + +fn is_import_callee(expr: &ast::Expr) -> bool { + match unwrap_expr(expr) { + ast::Expr::Apply(application) => is_import_callee(&application.lambda), + ast::Expr::Ident(ident) => ident.name == "import", + _ => false, + } +} + +fn unwrap_expr(expr: &ast::Expr) -> &ast::Expr { + match expr { + ast::Expr::Other(expressions) if expressions.len() == 1 => unwrap_expr(&expressions[0]), + _ => expr, + } +} + #[derive(Debug)] enum Node { Scope(ScopeKind), @@ -237,6 +260,7 @@ pub(crate) struct Binding { pub(crate) declaration: NodeId, pub(crate) name: String, pub(crate) references: Vec, + pub(crate) is_import: bool, pub(crate) is_lambda: bool, pub(crate) range: ast::SourceRange, }