From 9b90e9ad3f82cd4ac88568affd50cf206068d1ed Mon Sep 17 00:00:00 2001 From: alexion Date: Sat, 19 Sep 2026 20:30:08 -0400 Subject: [PATCH 1/4] feat(xatu): add manual inherit rules --- src/xatu/BUILD | 2 ++ src/xatu/ast.rs | 7 +++++ src/xatu/parser.rs | 36 +++++++++++++++++++++++++ src/xatu/rules.rs | 17 +++++++++--- src/xatu/rules/manual_inherit.rs | 24 +++++++++++++++++ src/xatu/rules/manual_inherit_from.rs | 30 +++++++++++++++++++++ src/xatu/testdata/lint_findings.nix | 2 ++ src/xatu/xatu_test.rs | 38 +++++++++++++++++++++++++++ 8 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 src/xatu/rules/manual_inherit.rs create mode 100644 src/xatu/rules/manual_inherit_from.rs diff --git a/src/xatu/BUILD b/src/xatu/BUILD index 4a72c08..f60733b 100644 --- a/src/xatu/BUILD +++ b/src/xatu/BUILD @@ -8,6 +8,8 @@ rust_library( "file_store.rs", "parser.rs", "rules.rs", + "rules/manual_inherit.rs", + "rules/manual_inherit_from.rs", "rules/unquoted_uri.rs", "xatu.rs", ], diff --git a/src/xatu/ast.rs b/src/xatu/ast.rs index 1addbf4..3b7fa50 100644 --- a/src/xatu/ast.rs +++ b/src/xatu/ast.rs @@ -29,6 +29,7 @@ impl std::fmt::Display for SourceRange { #[derive(Debug, Clone, PartialEq, Eq)] pub enum NodeData { Apply(Apply), + Assignment(Assignment), LetIn(LetIn), Inherit(Inherit), } @@ -38,6 +39,12 @@ pub struct Apply { pub function: Expr, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Assignment { + pub name: Ident, + pub value: Expr, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum Expr { Ident(Ident), diff --git a/src/xatu/parser.rs b/src/xatu/parser.rs index e92f605..245605b 100644 --- a/src/xatu/parser.rs +++ b/src/xatu/parser.rs @@ -38,6 +38,7 @@ fn convert_syntax(syntax: rnix::SyntaxNode) -> (Vec, Vec) fn convert_node(node: rnix::SyntaxNode) -> Option { match node.kind() { rnix::SyntaxKind::NODE_APPLY => convert_apply(node), + rnix::SyntaxKind::NODE_ATTRPATH_VALUE => convert_assignment(node), rnix::SyntaxKind::NODE_LET_IN => convert_let_in(node), rnix::SyntaxKind::NODE_INHERIT => convert_inherit(node), _ => None, @@ -65,6 +66,41 @@ impl TryFrom for ast::Apply { } } +fn convert_assignment(node: rnix::SyntaxNode) -> Option { + let assignment = rnix::ast::AttrpathValue::cast(node)?; + let range = assignment.syntax().text_range().into(); + let data = assignment.try_into().ok()?; + + Some(ast::Node { + range, + data: ast::NodeData::Assignment(data), + }) +} + +impl TryFrom for ast::Assignment { + type Error = (); + + fn try_from(assignment: rnix::ast::AttrpathValue) -> Result { + let mut attrs = assignment.attrpath().ok_or(())?.attrs(); + let rnix::ast::Attr::Ident(name) = attrs.next().ok_or(())? else { + return Err(()); + }; + if attrs.next().is_some() { + return Err(()); + } + + let value = assignment.value().ok_or(())?; + if matches!(&value, rnix::ast::Expr::Select(select) if select.or_token().is_some()) { + return Err(()); + } + + Ok(Self { + name: name.into(), + value: value.try_into()?, + }) + } +} + impl TryFrom for ast::Expr { type Error = (); diff --git a/src/xatu/rules.rs b/src/xatu/rules.rs index 45af16e..b27570d 100644 --- a/src/xatu/rules.rs +++ b/src/xatu/rules.rs @@ -1,7 +1,11 @@ use crate::FileId; use crate::ast; +use crate::rules::manual_inherit::ManualInherit; +use crate::rules::manual_inherit_from::ManualInheritFrom; use crate::rules::unquoted_uri::UnquotedUri; +pub mod manual_inherit; +pub mod manual_inherit_from; pub mod unquoted_uri; #[derive(Debug)] @@ -27,7 +31,6 @@ impl std::fmt::Display for Diagnostic { } } -#[expect(dead_code, reason = "retained for planned node rules")] pub trait NodeRule { type Node; @@ -47,7 +50,16 @@ pub trait TokenRule { } pub fn run_rules(file: &ast::ParsedFile) -> Vec { - let node_diagnostics = file.nodes.iter().flat_map(|_| std::iter::empty()); + let node_diagnostics = file + .nodes + .iter() + .flat_map(|node| { + [ + run_node_rule::(node), + run_node_rule::(node), + ] + }) + .flatten(); let token_diagnostics = file .tokens .iter() @@ -63,7 +75,6 @@ pub fn run_rules(file: &ast::ParsedFile) -> Vec { .collect() } -#[expect(dead_code, reason = "retained for planned node rules")] fn run_node_rule(node: &ast::Node) -> Option where R: NodeRule, diff --git a/src/xatu/rules/manual_inherit.rs b/src/xatu/rules/manual_inherit.rs new file mode 100644 index 0000000..3c6f1e7 --- /dev/null +++ b/src/xatu/rules/manual_inherit.rs @@ -0,0 +1,24 @@ +use crate::{ast, rules::NodeRule}; + +pub struct ManualInherit; + +impl NodeRule for ManualInherit { + type Node = ast::Assignment; + + const ID: &'static str = "manual_inherit"; + + fn select(data: &ast::NodeData) -> Option<&Self::Node> { + match data { + ast::NodeData::Assignment(assignment) => Some(assignment), + _ => None, + } + } + + fn check(assignment: &Self::Node) -> Option<&'static str> { + let ast::Expr::Ident(value) = &assignment.value else { + return None; + }; + + (assignment.name == *value).then_some("Use `inherit` for this assignment") + } +} diff --git a/src/xatu/rules/manual_inherit_from.rs b/src/xatu/rules/manual_inherit_from.rs new file mode 100644 index 0000000..4023a39 --- /dev/null +++ b/src/xatu/rules/manual_inherit_from.rs @@ -0,0 +1,30 @@ +use crate::{ast, rules::NodeRule}; + +pub struct ManualInheritFrom; + +impl NodeRule for ManualInheritFrom { + type Node = ast::Assignment; + + const ID: &'static str = "manual_inherit_from"; + + fn select(data: &ast::NodeData) -> Option<&Self::Node> { + match data { + ast::NodeData::Assignment(assignment) => Some(assignment), + _ => None, + } + } + + fn check(assignment: &Self::Node) -> Option<&'static str> { + let ast::Expr::Select(select) = &assignment.value else { + return None; + }; + let ast::Expr::Ident(_) = &select.receiver else { + return None; + }; + let [selected] = select.attrs.as_slice() else { + return None; + }; + + (assignment.name == *selected).then_some("Use `inherit (source)` for this assignment") + } +} diff --git a/src/xatu/testdata/lint_findings.nix b/src/xatu/testdata/lint_findings.nix index 1b922e8..e777e7e 100644 --- a/src/xatu/testdata/lint_findings.nix +++ b/src/xatu/testdata/lint_findings.nix @@ -1,3 +1,5 @@ { + package = package; + sourcePackage = source.sourcePackage; url = https://example.com; } diff --git a/src/xatu/xatu_test.rs b/src/xatu/xatu_test.rs index 57ce7f4..e6feb6e 100644 --- a/src/xatu/xatu_test.rs +++ b/src/xatu/xatu_test.rs @@ -41,6 +41,44 @@ fn xatu_rejects_invalid_source() -> googletest::Result<()> { Ok(()) } +#[gtest] +fn xatu_reports_manual_inherit() -> googletest::Result<()> { + let report = xatu::future_sight(vec![fixture_source("testdata/lint_findings.nix")?]).or_fail()?; + + let findings = report + .diagnostics + .iter() + .map(|diagnostic| diagnostic.finding.rule_id) + .collect::>(); + expect_that!(findings, contains(eq(&"manual_inherit"))); + Ok(()) +} + +#[gtest] +fn xatu_reports_manual_inherit_from() -> googletest::Result<()> { + let report = xatu::future_sight(vec![fixture_source("testdata/lint_findings.nix")?]).or_fail()?; + + let findings = report + .diagnostics + .iter() + .map(|diagnostic| diagnostic.finding.rule_id) + .collect::>(); + expect_that!(findings, contains(eq(&"manual_inherit_from"))); + Ok(()) +} + +#[gtest] +fn xatu_ignores_other_assignment_forms() -> googletest::Result<()> { + let report = xatu::future_sight(vec![SourceFile { + path: PathBuf::from("other_assignment_forms.nix"), + source: "{ nested = source.group.nested; target.path = path; defaulted = source.defaulted or null; renamed = source.original; }".into(), + }]) + .or_fail()?; + + expect_that!(report.is_clean(), is_true()); + Ok(()) +} + #[gtest] fn xatu_reports_unquoted_uri() -> googletest::Result<()> { let report = xatu::future_sight(vec![fixture_source("testdata/lint_findings.nix")?]).or_fail()?; -- 2.47.3 From 3b6184b122933b4d279b6e7a1848ce3f729a1596 Mon Sep 17 00:00:00 2001 From: alexion Date: Sat, 19 Sep 2026 20:32:55 -0400 Subject: [PATCH 2/4] refactor(xatu): remove unused AST nodes --- src/xatu/ast.rs | 18 -------- src/xatu/parser.rs | 63 --------------------------- src/xatu/rules/manual_inherit.rs | 6 +-- src/xatu/rules/manual_inherit_from.rs | 6 +-- 4 files changed, 4 insertions(+), 89 deletions(-) diff --git a/src/xatu/ast.rs b/src/xatu/ast.rs index 3b7fa50..bf962f9 100644 --- a/src/xatu/ast.rs +++ b/src/xatu/ast.rs @@ -28,15 +28,7 @@ impl std::fmt::Display for SourceRange { #[derive(Debug, Clone, PartialEq, Eq)] pub enum NodeData { - Apply(Apply), Assignment(Assignment), - LetIn(LetIn), - Inherit(Inherit), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Apply { - pub function: Expr, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -62,16 +54,6 @@ pub struct Select { pub attrs: Vec, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct LetIn { - pub binding_count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Inherit { - pub attr_count: usize, -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct Token { pub range: SourceRange, diff --git a/src/xatu/parser.rs b/src/xatu/parser.rs index 245605b..8d41928 100644 --- a/src/xatu/parser.rs +++ b/src/xatu/parser.rs @@ -1,4 +1,3 @@ -use rnix::ast::HasEntry; use rowan::ast::AstNode; use crate::{FileId, ast}; @@ -37,35 +36,11 @@ fn convert_syntax(syntax: rnix::SyntaxNode) -> (Vec, Vec) fn convert_node(node: rnix::SyntaxNode) -> Option { match node.kind() { - rnix::SyntaxKind::NODE_APPLY => convert_apply(node), rnix::SyntaxKind::NODE_ATTRPATH_VALUE => convert_assignment(node), - rnix::SyntaxKind::NODE_LET_IN => convert_let_in(node), - rnix::SyntaxKind::NODE_INHERIT => convert_inherit(node), _ => None, } } -fn convert_apply(node: rnix::SyntaxNode) -> Option { - let apply = rnix::ast::Apply::cast(node)?; - let range = apply.syntax().text_range().into(); - let data = apply.try_into().ok()?; - - Some(ast::Node { - range, - data: ast::NodeData::Apply(data), - }) -} - -impl TryFrom for ast::Apply { - type Error = (); - - fn try_from(apply: rnix::ast::Apply) -> Result { - Ok(Self { - function: apply.lambda().ok_or(())?.try_into()?, - }) - } -} - fn convert_assignment(node: rnix::SyntaxNode) -> Option { let assignment = rnix::ast::AttrpathValue::cast(node)?; let range = assignment.syntax().text_range().into(); @@ -140,44 +115,6 @@ impl TryFrom for ast::Select { } } -fn convert_let_in(node: rnix::SyntaxNode) -> Option { - let let_in = rnix::ast::LetIn::cast(node)?; - - Some(ast::Node { - range: let_in.syntax().text_range().into(), - data: ast::NodeData::LetIn(let_in.into()), - }) -} - -impl From for ast::LetIn { - fn from(let_in: rnix::ast::LetIn) -> Self { - Self { - binding_count: let_in.attrpath_values().count() - + let_in - .inherits() - .map(|inherit| inherit.attrs().count()) - .sum::(), - } - } -} - -fn convert_inherit(node: rnix::SyntaxNode) -> Option { - let inherit = rnix::ast::Inherit::cast(node)?; - - Some(ast::Node { - range: inherit.syntax().text_range().into(), - data: ast::NodeData::Inherit(inherit.into()), - }) -} - -impl From for ast::Inherit { - fn from(inherit: rnix::ast::Inherit) -> Self { - Self { - attr_count: inherit.attrs().count(), - } - } -} - fn convert_token(token: rnix::SyntaxToken) -> Option { match token.kind() { rnix::SyntaxKind::TOKEN_URI => Some(ast::Token { diff --git a/src/xatu/rules/manual_inherit.rs b/src/xatu/rules/manual_inherit.rs index 3c6f1e7..bd154bf 100644 --- a/src/xatu/rules/manual_inherit.rs +++ b/src/xatu/rules/manual_inherit.rs @@ -8,10 +8,8 @@ impl NodeRule for ManualInherit { const ID: &'static str = "manual_inherit"; fn select(data: &ast::NodeData) -> Option<&Self::Node> { - match data { - ast::NodeData::Assignment(assignment) => Some(assignment), - _ => None, - } + let ast::NodeData::Assignment(assignment) = data; + Some(assignment) } fn check(assignment: &Self::Node) -> Option<&'static str> { diff --git a/src/xatu/rules/manual_inherit_from.rs b/src/xatu/rules/manual_inherit_from.rs index 4023a39..2fb6c1d 100644 --- a/src/xatu/rules/manual_inherit_from.rs +++ b/src/xatu/rules/manual_inherit_from.rs @@ -8,10 +8,8 @@ impl NodeRule for ManualInheritFrom { const ID: &'static str = "manual_inherit_from"; fn select(data: &ast::NodeData) -> Option<&Self::Node> { - match data { - ast::NodeData::Assignment(assignment) => Some(assignment), - _ => None, - } + let ast::NodeData::Assignment(assignment) = data; + Some(assignment) } fn check(assignment: &Self::Node) -> Option<&'static str> { -- 2.47.3 From e949e526c74195f9f4a657a4b62a042f4d2cec1c Mon Sep 17 00:00:00 2001 From: alexion Date: Sat, 19 Sep 2026 20:36:37 -0400 Subject: [PATCH 3/4] refactor(xatu): rename assignments to bindings --- src/xatu/ast.rs | 4 ++-- src/xatu/parser.rs | 20 ++++++++++---------- src/xatu/rules/manual_inherit.rs | 12 ++++++------ src/xatu/rules/manual_inherit_from.rs | 12 ++++++------ src/xatu/xatu_test.rs | 4 ++-- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/xatu/ast.rs b/src/xatu/ast.rs index bf962f9..64de2c6 100644 --- a/src/xatu/ast.rs +++ b/src/xatu/ast.rs @@ -28,11 +28,11 @@ impl std::fmt::Display for SourceRange { #[derive(Debug, Clone, PartialEq, Eq)] pub enum NodeData { - Assignment(Assignment), + Binding(Binding), } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Assignment { +pub struct Binding { pub name: Ident, pub value: Expr, } diff --git a/src/xatu/parser.rs b/src/xatu/parser.rs index 8d41928..8159954 100644 --- a/src/xatu/parser.rs +++ b/src/xatu/parser.rs @@ -36,27 +36,27 @@ fn convert_syntax(syntax: rnix::SyntaxNode) -> (Vec, Vec) fn convert_node(node: rnix::SyntaxNode) -> Option { match node.kind() { - rnix::SyntaxKind::NODE_ATTRPATH_VALUE => convert_assignment(node), + rnix::SyntaxKind::NODE_ATTRPATH_VALUE => convert_binding(node), _ => None, } } -fn convert_assignment(node: rnix::SyntaxNode) -> Option { - let assignment = rnix::ast::AttrpathValue::cast(node)?; - let range = assignment.syntax().text_range().into(); - let data = assignment.try_into().ok()?; +fn convert_binding(node: rnix::SyntaxNode) -> Option { + let binding = rnix::ast::AttrpathValue::cast(node)?; + let range = binding.syntax().text_range().into(); + let data = binding.try_into().ok()?; Some(ast::Node { range, - data: ast::NodeData::Assignment(data), + data: ast::NodeData::Binding(data), }) } -impl TryFrom for ast::Assignment { +impl TryFrom for ast::Binding { type Error = (); - fn try_from(assignment: rnix::ast::AttrpathValue) -> Result { - let mut attrs = assignment.attrpath().ok_or(())?.attrs(); + fn try_from(binding: rnix::ast::AttrpathValue) -> Result { + let mut attrs = binding.attrpath().ok_or(())?.attrs(); let rnix::ast::Attr::Ident(name) = attrs.next().ok_or(())? else { return Err(()); }; @@ -64,7 +64,7 @@ impl TryFrom for ast::Assignment { return Err(()); } - let value = assignment.value().ok_or(())?; + let value = binding.value().ok_or(())?; if matches!(&value, rnix::ast::Expr::Select(select) if select.or_token().is_some()) { return Err(()); } diff --git a/src/xatu/rules/manual_inherit.rs b/src/xatu/rules/manual_inherit.rs index bd154bf..58cc6c0 100644 --- a/src/xatu/rules/manual_inherit.rs +++ b/src/xatu/rules/manual_inherit.rs @@ -3,20 +3,20 @@ use crate::{ast, rules::NodeRule}; pub struct ManualInherit; impl NodeRule for ManualInherit { - type Node = ast::Assignment; + type Node = ast::Binding; const ID: &'static str = "manual_inherit"; fn select(data: &ast::NodeData) -> Option<&Self::Node> { - let ast::NodeData::Assignment(assignment) = data; - Some(assignment) + let ast::NodeData::Binding(binding) = data; + Some(binding) } - fn check(assignment: &Self::Node) -> Option<&'static str> { - let ast::Expr::Ident(value) = &assignment.value else { + fn check(binding: &Self::Node) -> Option<&'static str> { + let ast::Expr::Ident(value) = &binding.value else { return None; }; - (assignment.name == *value).then_some("Use `inherit` for this assignment") + (binding.name == *value).then_some("Use `inherit` for this binding") } } diff --git a/src/xatu/rules/manual_inherit_from.rs b/src/xatu/rules/manual_inherit_from.rs index 2fb6c1d..5cc4b80 100644 --- a/src/xatu/rules/manual_inherit_from.rs +++ b/src/xatu/rules/manual_inherit_from.rs @@ -3,17 +3,17 @@ use crate::{ast, rules::NodeRule}; pub struct ManualInheritFrom; impl NodeRule for ManualInheritFrom { - type Node = ast::Assignment; + type Node = ast::Binding; const ID: &'static str = "manual_inherit_from"; fn select(data: &ast::NodeData) -> Option<&Self::Node> { - let ast::NodeData::Assignment(assignment) = data; - Some(assignment) + let ast::NodeData::Binding(binding) = data; + Some(binding) } - fn check(assignment: &Self::Node) -> Option<&'static str> { - let ast::Expr::Select(select) = &assignment.value else { + fn check(binding: &Self::Node) -> Option<&'static str> { + let ast::Expr::Select(select) = &binding.value else { return None; }; let ast::Expr::Ident(_) = &select.receiver else { @@ -23,6 +23,6 @@ impl NodeRule for ManualInheritFrom { return None; }; - (assignment.name == *selected).then_some("Use `inherit (source)` for this assignment") + (binding.name == *selected).then_some("Use `inherit (source)` for this binding") } } diff --git a/src/xatu/xatu_test.rs b/src/xatu/xatu_test.rs index e6feb6e..e9eacfa 100644 --- a/src/xatu/xatu_test.rs +++ b/src/xatu/xatu_test.rs @@ -68,9 +68,9 @@ fn xatu_reports_manual_inherit_from() -> googletest::Result<()> { } #[gtest] -fn xatu_ignores_other_assignment_forms() -> googletest::Result<()> { +fn xatu_ignores_other_binding_forms() -> googletest::Result<()> { let report = xatu::future_sight(vec![SourceFile { - path: PathBuf::from("other_assignment_forms.nix"), + path: PathBuf::from("other_binding_forms.nix"), source: "{ nested = source.group.nested; target.path = path; defaulted = source.defaulted or null; renamed = source.original; }".into(), }]) .or_fail()?; -- 2.47.3 From ab68fa3b05df46bffe21b41ddb67878bc01f26dd Mon Sep 17 00:00:00 2001 From: alexion Date: Sat, 19 Sep 2026 20:41:14 -0400 Subject: [PATCH 4/4] refactor(xatu): move binding filters into rules --- src/xatu/ast.rs | 12 ++++- src/xatu/parser.rs | 77 ++++++++++++--------------- src/xatu/rules/manual_inherit.rs | 5 +- src/xatu/rules/manual_inherit_from.rs | 10 +++- 4 files changed, 57 insertions(+), 47 deletions(-) diff --git a/src/xatu/ast.rs b/src/xatu/ast.rs index 64de2c6..a12e30e 100644 --- a/src/xatu/ast.rs +++ b/src/xatu/ast.rs @@ -33,14 +33,21 @@ pub enum NodeData { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Binding { - pub name: Ident, + pub path: Vec, pub value: Expr, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Attr { + Ident(Ident), + Other, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum Expr { Ident(Ident), Select(Box