From ac5cc8d65916a0ac87a59876cb30103544995014 Mon Sep 17 00:00:00 2001 From: alexion Date: Sat, 26 Sep 2026 23:24:54 -0400 Subject: [PATCH] fix(natu): preserve NixOS cfg bindings --- src/natu/natu.rs | 2 +- src/natu/natu_test.rs | 12 ++++++++++++ src/natu/rules.rs | 22 +++++++++++++-------- src/natu/rules/let_order.rs | 38 +++++++++++++++++++++---------------- 4 files changed, 49 insertions(+), 25 deletions(-) diff --git a/src/natu/natu.rs b/src/natu/natu.rs index 94ea9b2..a4df7ba 100644 --- a/src/natu/natu.rs +++ b/src/natu/natu.rs @@ -111,7 +111,7 @@ pub fn future_sight(files: Vec) -> status::Result { } let graph = ScopeGraph::from_file(&parsed); - diagnostics.extend(rules::run_rules(&parsed, &graph)); + diagnostics.extend(rules::run_rules(&parsed, &graph, &file.path)); } Ok(Report { diff --git a/src/natu/natu_test.rs b/src/natu/natu_test.rs index bc7c81e..3f2f47f 100644 --- a/src/natu/natu_test.rs +++ b/src/natu/natu_test.rs @@ -65,6 +65,18 @@ fn natu_reports_out_of_order_let_bindings() -> googletest::Result<()> { Ok(()) } +#[gtest] +fn natu_preserves_nixos_cfg_bindings() -> googletest::Result<()> { + let report = natu::future_sight(vec![SourceFile { + path: PathBuf::from("configs/nixos/test.nix"), + source: "{ config, ... }: let\n cfg = config.example;\n value = cfg;\nin value\n".into(), + }]) + .or_fail()?; + + expect_that!(report.is_clean(), is_true()); + Ok(()) +} + #[gtest] fn natu_reports_unquoted_uri() -> googletest::Result<()> { let report = natu::future_sight(vec![fixture_source("testdata/lint_findings.nix")?]).or_fail()?; diff --git a/src/natu/rules.rs b/src/natu/rules.rs index 7d0e6b8..cf8183d 100644 --- a/src/natu/rules.rs +++ b/src/natu/rules.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use crate::FileId; use crate::ast; use crate::rules::manual_inherit::ManualInherit; @@ -59,7 +61,7 @@ pub trait TokenRule { fn check(token: &Self::Token) -> Option<&'static str>; } -pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph) -> Vec { +pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph, path: &Path) -> Vec { let node_diagnostics = file .nodes .iter() @@ -76,13 +78,17 @@ pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph) -> Vec .flat_map(|token| [run_token_rule::(token)]) .flatten(); - let let_order_diagnostics = let_order::check(graph).into_iter().map(|finding| Finding { - rule_id: "let_order", - range: finding.range, - message: "Let bindings must follow dependency order", - binding: None, - expected_order: Some(finding.expected_order), - }); + let ignore_cfg = + path.starts_with("configs/nixos") || path.to_string_lossy().contains("/configs/nixos/"); + let let_order_diagnostics = let_order::check(graph, ignore_cfg) + .into_iter() + .map(|finding| Finding { + rule_id: "let_order", + range: finding.range, + message: "Let bindings must follow dependency order", + binding: None, + expected_order: Some(finding.expected_order), + }); node_diagnostics .chain(token_diagnostics) diff --git a/src/natu/rules/let_order.rs b/src/natu/rules/let_order.rs index 92dc6d6..ae8e77d 100644 --- a/src/natu/rules/let_order.rs +++ b/src/natu/rules/let_order.rs @@ -8,36 +8,42 @@ pub struct Finding { pub expected_order: Vec, } -pub fn check(graph: &ScopeGraph) -> Vec { +pub fn check(graph: &ScopeGraph, ignore_cfg: bool) -> Vec { graph .let_scopes() .iter() .flat_map(|let_scope| { - let expected = expected_order(graph, let_scope); - let is_ordered = expected.iter().enumerate().all(|(position, declaration)| { - let_scope - .bindings - .get(position) - .is_some_and(|binding| binding.declaration == *declaration) - }); + let expected = expected_order(graph, let_scope) + .into_iter() + .filter(|declaration| !ignore_cfg || binding_name(let_scope, *declaration) != "cfg") + .collect::>(); + let actual = let_scope + .bindings + .iter() + .filter(|binding| !ignore_cfg || binding.name != "cfg") + .map(|binding| binding.declaration) + .collect::>(); - (!is_ordered).then(|| Finding { + (!actual.starts_with(&expected)).then(|| Finding { range: let_scope.range, expected_order: expected .iter() - .filter_map(|declaration| { - let_scope - .bindings - .iter() - .find(|binding| binding.declaration == *declaration) - .map(|binding| binding.name.clone()) - }) + .map(|declaration| binding_name(let_scope, *declaration).to_owned()) .collect(), }) }) .collect() } +fn binding_name(let_scope: &LetScope, declaration: NodeId) -> &str { + &let_scope + .bindings + .iter() + .find(|binding| binding.declaration == declaration) + .expect("let declaration") + .name +} + fn expected_order(graph: &ScopeGraph, let_scope: &LetScope) -> Vec { let mut order = Vec::new(); visit_references(graph, &let_scope.entry_references, let_scope, &mut order); -- 2.47.3