fix(natu): preserve NixOS cfg bindings

This commit was merged in pull request #104.
This commit is contained in:
2026-09-26 23:24:54 -04:00
parent 5e5c3e2534
commit ac5cc8d659
4 changed files with 49 additions and 25 deletions

View File

@@ -111,7 +111,7 @@ pub fn future_sight(files: Vec<SourceFile>) -> status::Result<Report> {
} }
let graph = ScopeGraph::from_file(&parsed); let graph = ScopeGraph::from_file(&parsed);
diagnostics.extend(rules::run_rules(&parsed, &graph)); diagnostics.extend(rules::run_rules(&parsed, &graph, &file.path));
} }
Ok(Report { Ok(Report {

View File

@@ -65,6 +65,18 @@ fn natu_reports_out_of_order_let_bindings() -> googletest::Result<()> {
Ok(()) 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] #[gtest]
fn natu_reports_unquoted_uri() -> googletest::Result<()> { fn natu_reports_unquoted_uri() -> googletest::Result<()> {
let report = natu::future_sight(vec![fixture_source("testdata/lint_findings.nix")?]).or_fail()?; let report = natu::future_sight(vec![fixture_source("testdata/lint_findings.nix")?]).or_fail()?;

View File

@@ -1,3 +1,5 @@
use std::path::Path;
use crate::FileId; use crate::FileId;
use crate::ast; use crate::ast;
use crate::rules::manual_inherit::ManualInherit; use crate::rules::manual_inherit::ManualInherit;
@@ -59,7 +61,7 @@ pub trait TokenRule {
fn check(token: &Self::Token) -> Option<&'static str>; fn check(token: &Self::Token) -> Option<&'static str>;
} }
pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph) -> Vec<Diagnostic> { pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph, path: &Path) -> Vec<Diagnostic> {
let node_diagnostics = file let node_diagnostics = file
.nodes .nodes
.iter() .iter()
@@ -76,13 +78,17 @@ pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph) -> Vec<Diagnostic>
.flat_map(|token| [run_token_rule::<UnquotedUri>(token)]) .flat_map(|token| [run_token_rule::<UnquotedUri>(token)])
.flatten(); .flatten();
let let_order_diagnostics = let_order::check(graph).into_iter().map(|finding| Finding { let ignore_cfg =
rule_id: "let_order", path.starts_with("configs/nixos") || path.to_string_lossy().contains("/configs/nixos/");
range: finding.range, let let_order_diagnostics = let_order::check(graph, ignore_cfg)
message: "Let bindings must follow dependency order", .into_iter()
binding: None, .map(|finding| Finding {
expected_order: Some(finding.expected_order), rule_id: "let_order",
}); range: finding.range,
message: "Let bindings must follow dependency order",
binding: None,
expected_order: Some(finding.expected_order),
});
node_diagnostics node_diagnostics
.chain(token_diagnostics) .chain(token_diagnostics)

View File

@@ -8,36 +8,42 @@ pub struct Finding {
pub expected_order: Vec<String>, pub expected_order: Vec<String>,
} }
pub fn check(graph: &ScopeGraph) -> Vec<Finding> { pub fn check(graph: &ScopeGraph, ignore_cfg: bool) -> Vec<Finding> {
graph graph
.let_scopes() .let_scopes()
.iter() .iter()
.flat_map(|let_scope| { .flat_map(|let_scope| {
let expected = expected_order(graph, let_scope); let expected = expected_order(graph, let_scope)
let is_ordered = expected.iter().enumerate().all(|(position, declaration)| { .into_iter()
let_scope .filter(|declaration| !ignore_cfg || binding_name(let_scope, *declaration) != "cfg")
.bindings .collect::<Vec<_>>();
.get(position) let actual = let_scope
.is_some_and(|binding| binding.declaration == *declaration) .bindings
}); .iter()
.filter(|binding| !ignore_cfg || binding.name != "cfg")
.map(|binding| binding.declaration)
.collect::<Vec<_>>();
(!is_ordered).then(|| Finding { (!actual.starts_with(&expected)).then(|| Finding {
range: let_scope.range, range: let_scope.range,
expected_order: expected expected_order: expected
.iter() .iter()
.filter_map(|declaration| { .map(|declaration| binding_name(let_scope, *declaration).to_owned())
let_scope
.bindings
.iter()
.find(|binding| binding.declaration == *declaration)
.map(|binding| binding.name.clone())
})
.collect(), .collect(),
}) })
}) })
.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<NodeId> { fn expected_order(graph: &ScopeGraph, let_scope: &LetScope) -> Vec<NodeId> {
let mut order = Vec::new(); let mut order = Vec::new();
visit_references(graph, &let_scope.entry_references, let_scope, &mut order); visit_references(graph, &let_scope.entry_references, let_scope, &mut order);