feat(natu): ignore imported let bindings

This commit was merged in pull request #122.
This commit is contained in:
2026-10-01 21:54:09 -04:00
parent 3fbb0aca32
commit 1cbdcee8ac
3 changed files with 44 additions and 1 deletions

View File

@@ -199,6 +199,24 @@ in
Ok(()) 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] #[gtest]
fn natu_reports_single_use_binding() -> googletest::Result<()> { fn natu_reports_single_use_binding() -> googletest::Result<()> {
let report = natu::future_sight(vec![SourceFile { let report = natu::future_sight(vec![SourceFile {

View File

@@ -38,7 +38,8 @@ fn is_single_use(graph: &ScopeGraph, let_scope: &LetScope, binding: &Binding) ->
.filter(|reference| graph.resolve(**reference).contains(&binding.declaration)) .filter(|reference| graph.resolve(**reference).contains(&binding.declaration))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
self_references == 0 !binding.is_import
&& self_references == 0
&& external_references.len() == 1 && external_references.len() == 1
&& (binding.is_lambda && (binding.is_lambda
|| !graph.crosses_lambda_scope(*external_references[0], binding.declaration)) || !graph.crosses_lambda_scope(*external_references[0], binding.declaration))

View File

@@ -69,6 +69,7 @@ impl ScopeGraph {
declaration: self.add_declaration(&binding.key.name, scope), declaration: self.add_declaration(&binding.key.name, scope),
name: binding.key.name.clone(), name: binding.key.name.clone(),
references: Vec::new(), references: Vec::new(),
is_import: is_import(&binding.value),
is_lambda: matches!(binding.value, ast::Expr::Lambda { .. }), is_lambda: matches!(binding.value, ast::Expr::Lambda { .. }),
range: binding.range, 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)] #[derive(Debug)]
enum Node { enum Node {
Scope(ScopeKind), Scope(ScopeKind),
@@ -237,6 +260,7 @@ pub(crate) struct Binding {
pub(crate) declaration: NodeId, pub(crate) declaration: NodeId,
pub(crate) name: String, pub(crate) name: String,
pub(crate) references: Vec<NodeId>, pub(crate) references: Vec<NodeId>,
pub(crate) is_import: bool,
pub(crate) is_lambda: bool, pub(crate) is_lambda: bool,
pub(crate) range: ast::SourceRange, pub(crate) range: ast::SourceRange,
} }