From f9aff6839e9eedf52725b3bdf6432645fba96495 Mon Sep 17 00:00:00 2001 From: alexion Date: Sat, 19 Sep 2026 23:30:56 -0400 Subject: [PATCH] feat(xatu): add manual inherit from lint --- src/xatu/BUILD | 1 + src/xatu/ast.rs | 14 ++++++++- src/xatu/parser.rs | 42 ++++++++++++++++++++++++--- src/xatu/rules.rs | 9 +++++- src/xatu/rules/manual_inherit.rs | 9 +++--- src/xatu/rules/manual_inherit_from.rs | 24 +++++++++++++++ src/xatu/testdata/clean.nix | 3 +- src/xatu/testdata/lint_findings.nix | 1 + src/xatu/xatu_test.rs | 13 +++++++++ 9 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 src/xatu/rules/manual_inherit_from.rs diff --git a/src/xatu/BUILD b/src/xatu/BUILD index ea83932..f60733b 100644 --- a/src/xatu/BUILD +++ b/src/xatu/BUILD @@ -9,6 +9,7 @@ rust_library( "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 4b3c25c..1deb464 100644 --- a/src/xatu/ast.rs +++ b/src/xatu/ast.rs @@ -34,7 +34,7 @@ pub enum NodeData { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Binding { pub key: Ident, - pub value: Ident, + pub value: Expr, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -42,6 +42,18 @@ pub struct Ident { pub name: String, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Expr { + Ident(Ident), + Select(Select), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Select { + pub receiver: String, + pub attr: Ident, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct Token { pub range: SourceRange, diff --git a/src/xatu/parser.rs b/src/xatu/parser.rs index 7bc6948..61545c3 100644 --- a/src/xatu/parser.rs +++ b/src/xatu/parser.rs @@ -67,15 +67,24 @@ impl TryFrom for ast::Binding { return Err(()); } - let value = match binding.value().ok_or(())? { - rnix::ast::Expr::Ident(ident) => ident.into(), - _ => return Err(()), - }; + let value = binding.value().ok_or(())?.try_into()?; Ok(Self { key, value }) } } +impl TryFrom for ast::Expr { + type Error = (); + + fn try_from(expr: rnix::ast::Expr) -> Result { + match expr { + rnix::ast::Expr::Ident(ident) => Ok(Self::Ident(ident.into())), + rnix::ast::Expr::Select(select) => Ok(Self::Select(select.try_into()?)), + _ => Err(()), + } + } +} + impl From for ast::Ident { fn from(ident: rnix::ast::Ident) -> Self { Self { @@ -84,6 +93,31 @@ impl From for ast::Ident { } } +impl TryFrom for ast::Select { + type Error = (); + + fn try_from(select: rnix::ast::Select) -> Result { + if select.or_token().is_some() { + return Err(()); + } + + let mut attrs = select.attrpath().ok_or(())?.attrs(); + let attr = match attrs.next().ok_or(())? { + Attr::Ident(ident) => ident.into(), + _ => return Err(()), + }; + + if attrs.next().is_some() { + return Err(()); + } + + Ok(Self { + receiver: select.expr().ok_or(())?.syntax().to_string(), + attr, + }) + } +} + fn convert_token(token: rnix::SyntaxToken) -> Option { match token.kind() { rnix::SyntaxKind::TOKEN_URI => Some(ast::Token { diff --git a/src/xatu/rules.rs b/src/xatu/rules.rs index 598e8d7..b27570d 100644 --- a/src/xatu/rules.rs +++ b/src/xatu/rules.rs @@ -1,9 +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)] @@ -51,7 +53,12 @@ pub fn run_rules(file: &ast::ParsedFile) -> Vec { let node_diagnostics = file .nodes .iter() - .flat_map(|node| [run_node_rule::(node)]) + .flat_map(|node| { + [ + run_node_rule::(node), + run_node_rule::(node), + ] + }) .flatten(); let token_diagnostics = file .tokens diff --git a/src/xatu/rules/manual_inherit.rs b/src/xatu/rules/manual_inherit.rs index 9a6a7a9..76c71c7 100644 --- a/src/xatu/rules/manual_inherit.rs +++ b/src/xatu/rules/manual_inherit.rs @@ -14,10 +14,11 @@ impl NodeRule for ManualInherit { } fn check(node: &Self::Node) -> Option<&'static str> { - if node.key == node.value { - Some("Use `inherit` for this assignment") - } else { - None + match &node.value { + ast::Expr::Ident(value) if node.key == *value => { + Some("Use `inherit` for this assignment") + } + _ => None, } } } diff --git a/src/xatu/rules/manual_inherit_from.rs b/src/xatu/rules/manual_inherit_from.rs new file mode 100644 index 0000000..5d8717a --- /dev/null +++ b/src/xatu/rules/manual_inherit_from.rs @@ -0,0 +1,24 @@ +use crate::{ast, rules::NodeRule}; + +pub struct ManualInheritFrom; + +impl NodeRule for ManualInheritFrom { + type Node = ast::Binding; + + const ID: &'static str = "manual_inherit_from"; + + fn select(data: &ast::NodeData) -> Option<&Self::Node> { + let ast::NodeData::Binding(binding) = data; + + Some(binding) + } + + fn check(node: &Self::Node) -> Option<&'static str> { + match &node.value { + ast::Expr::Select(select) if node.key == select.attr => { + Some("Use `inherit` for this selected attribute") + } + _ => None, + } + } +} diff --git a/src/xatu/testdata/clean.nix b/src/xatu/testdata/clean.nix index 01c2d4b..fa09933 100644 --- a/src/xatu/testdata/clean.nix +++ b/src/xatu/testdata/clean.nix @@ -8,5 +8,6 @@ nested.package = nested.package; "quoted" = quoted; ${dynamic} = dynamic; - from = pkgs.from; + from = pkgs.from.other; + fallback = pkgs.fallback or null; } diff --git a/src/xatu/testdata/lint_findings.nix b/src/xatu/testdata/lint_findings.nix index e31135d..17b4837 100644 --- a/src/xatu/testdata/lint_findings.nix +++ b/src/xatu/testdata/lint_findings.nix @@ -1,4 +1,5 @@ { package = package; + from = pkgs.from; url = https://example.com; } diff --git a/src/xatu/xatu_test.rs b/src/xatu/xatu_test.rs index 3a4e8b9..2e82db6 100644 --- a/src/xatu/xatu_test.rs +++ b/src/xatu/xatu_test.rs @@ -77,3 +77,16 @@ fn xatu_reports_manual_inherit() -> googletest::Result<()> { expect_that!(manual_inherit_count, eq(1)); 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 manual_inherit_from_count = report + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.finding.rule_id == "manual_inherit_from") + .count(); + expect_that!(manual_inherit_from_count, eq(1)); + Ok(()) +}