feat(natu): report expected let order

This commit is contained in:
2026-09-26 23:14:08 -04:00
parent 09a2093f90
commit 90b83d6f5d
4 changed files with 66 additions and 22 deletions

View File

@@ -1,3 +1,5 @@
use std::collections::HashSet;
mod ast;
mod file_store;
mod parser;
@@ -23,7 +25,34 @@ impl Report {
impl std::fmt::Display for Report {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut reported_let_order_files = HashSet::new();
for diagnostic in &self.diagnostics {
if diagnostic.finding.rule_id == "let_order" {
if !reported_let_order_files.insert(diagnostic.file_id) {
continue;
}
let file = self.files.get(diagnostic.file_id);
writeln!(
formatter,
"{} [let_order] Let bindings must follow dependency order",
file.path.display()
)?;
for finding in self.diagnostics.iter().filter(|candidate| {
candidate.file_id == diagnostic.file_id
&& candidate.finding.rule_id == "let_order"
}) {
let (start, _) = line_range(&file.source, finding.finding.range);
let expected_order =
finding.finding.expected_order.as_ref().expect("let order");
writeln!(formatter, " line {start}: {}", expected_order.join(", "))?;
}
continue;
}
let file = self.files.get(diagnostic.file_id);
let (start, end) = line_range(&file.source, diagnostic.finding.range);
let location = if start == end {

View File

@@ -50,7 +50,18 @@ fn natu_reports_out_of_order_let_bindings() -> googletest::Result<()> {
.iter()
.filter(|diagnostic| diagnostic.finding.rule_id == "let_order")
.count();
expect_that!(let_order_count, eq(4));
expect_that!(let_order_count, eq(2));
expect_that!(
report.to_string(),
eq(concat!(
"testdata/lint_findings.nix:11 [manual_inherit] Use `inherit` for this assignment\n",
"testdata/lint_findings.nix:12 [manual_inherit_from] Use `inherit` for this selected attribute\n",
"testdata/lint_findings.nix:13 [unquoted_uri] URI literals should be quoted\n",
"testdata/lint_findings.nix [let_order] Let bindings must follow dependency order\n",
" line 5: nestedSecond, nestedFirst\n",
" line 2: first, second, nested\n",
))
);
Ok(())
}

View File

@@ -22,6 +22,7 @@ pub struct Finding {
pub range: ast::SourceRange,
pub message: &'static str,
pub binding: Option<String>,
pub expected_order: Option<Vec<String>>,
}
impl std::fmt::Display for Diagnostic {
@@ -79,7 +80,8 @@ pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph) -> Vec<Diagnostic>
rule_id: "let_order",
range: finding.range,
message: "Let bindings must follow dependency order",
binding: Some(finding.binding),
binding: None,
expected_order: Some(finding.expected_order),
});
node_diagnostics
@@ -104,6 +106,7 @@ where
range: node.range,
message,
binding: None,
expected_order: None,
})
}
@@ -119,5 +122,6 @@ where
range: token.range,
message,
binding: None,
expected_order: None,
})
}

View File

@@ -5,7 +5,7 @@ use crate::{
pub struct Finding {
pub range: ast::SourceRange,
pub binding: String,
pub expected_order: Vec<String>,
}
pub fn check(graph: &ScopeGraph) -> Vec<Finding> {
@@ -14,26 +14,26 @@ pub fn check(graph: &ScopeGraph) -> Vec<Finding> {
.iter()
.flat_map(|let_scope| {
let expected = expected_order(graph, let_scope);
let positions = expected
.iter()
.enumerate()
.map(|(position, declaration)| (*declaration, position))
.collect::<Vec<_>>();
let is_ordered = expected.iter().enumerate().all(|(position, declaration)| {
let_scope
.bindings
.get(position)
.is_some_and(|binding| binding.declaration == *declaration)
});
let_scope
.bindings
.iter()
.enumerate()
.filter_map(move |(position, binding)| {
positions
.iter()
.find(|(declaration, _)| *declaration == binding.declaration)
.filter(|(_, expected_position)| position != *expected_position)
.map(|_| Finding {
range: binding.range,
binding: binding.name.clone(),
})
})
(!is_ordered).then(|| Finding {
range: let_scope.bindings[0].range,
expected_order: expected
.iter()
.filter_map(|declaration| {
let_scope
.bindings
.iter()
.find(|binding| binding.declaration == *declaration)
.map(|binding| binding.name.clone())
})
.collect(),
})
})
.collect()
}