From dd82367dcbf1f8df447085f5d1c8db24c8600fda Mon Sep 17 00:00:00 2001 From: alexion Date: Sat, 26 Sep 2026 23:30:01 -0400 Subject: [PATCH] feat(natu): support inline suppressions --- src/natu/natu.rs | 2 +- src/natu/natu_test.rs | 6 ++--- src/natu/rules.rs | 44 +++++++++++++++++++++++++------------ src/natu/rules/let_order.rs | 16 +++++++++++--- src/natu/scope_graph.rs | 2 ++ 5 files changed, 49 insertions(+), 21 deletions(-) diff --git a/src/natu/natu.rs b/src/natu/natu.rs index a4df7ba..6197a54 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, &file.path)); + diagnostics.extend(rules::run_rules(&parsed, &graph, &file.source)); } Ok(Report { diff --git a/src/natu/natu_test.rs b/src/natu/natu_test.rs index 3f2f47f..7584791 100644 --- a/src/natu/natu_test.rs +++ b/src/natu/natu_test.rs @@ -66,10 +66,10 @@ fn natu_reports_out_of_order_let_bindings() -> googletest::Result<()> { } #[gtest] -fn natu_preserves_nixos_cfg_bindings() -> googletest::Result<()> { +fn natu_preserves_suppressed_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(), + path: PathBuf::from("testdata/nolint.nix"), + source: "{ config, ... }: let\n cfg = config.example; # NOLINT(let_order)\n value = cfg;\nin value\n".into(), }]) .or_fail()?; diff --git a/src/natu/rules.rs b/src/natu/rules.rs index cf8183d..b295ecd 100644 --- a/src/natu/rules.rs +++ b/src/natu/rules.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use crate::FileId; use crate::ast; use crate::rules::manual_inherit::ManualInherit; @@ -61,7 +59,7 @@ pub trait TokenRule { fn check(token: &Self::Token) -> Option<&'static str>; } -pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph, path: &Path) -> Vec { +pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph, source: &str) -> Vec { let node_diagnostics = file .nodes .iter() @@ -78,21 +76,21 @@ pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph, path: &Path) -> Vec .flat_map(|token| [run_token_rule::(token)]) .flatten(); - 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), - }); + let let_order_diagnostics = + let_order::check(graph, source) + .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) .chain(let_order_diagnostics) + .filter(|finding| !is_suppressed(source, finding.range, finding.rule_id)) .map(|finding| Diagnostic { file_id: file.file_id, finding, @@ -100,6 +98,24 @@ pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph, path: &Path) -> Vec .collect() } +pub fn is_suppressed(source: &str, range: ast::SourceRange, rule_id: &str) -> bool { + let line = source[range.start..].lines().next().unwrap_or_default(); + let Some(comment) = line.split_once('#').map(|(_, comment)| comment.trim()) else { + return false; + }; + if comment == "NOLINT" { + return true; + } + comment + .strip_prefix("NOLINT(") + .and_then(|rules| rules.strip_suffix(')')) + .is_some_and(|rules| { + rules + .split(',') + .any(|candidate| candidate.trim() == rule_id) + }) +} + fn run_node_rule(node: &ast::Node) -> Option where R: NodeRule, diff --git a/src/natu/rules/let_order.rs b/src/natu/rules/let_order.rs index ae8e77d..ca7daf0 100644 --- a/src/natu/rules/let_order.rs +++ b/src/natu/rules/let_order.rs @@ -1,5 +1,6 @@ use crate::{ ast, + rules::is_suppressed, scope_graph::{LetScope, NodeId, ScopeGraph}, }; @@ -8,19 +9,19 @@ pub struct Finding { pub expected_order: Vec, } -pub fn check(graph: &ScopeGraph, ignore_cfg: bool) -> Vec { +pub fn check(graph: &ScopeGraph, source: &str) -> Vec { graph .let_scopes() .iter() .flat_map(|let_scope| { let expected = expected_order(graph, let_scope) .into_iter() - .filter(|declaration| !ignore_cfg || binding_name(let_scope, *declaration) != "cfg") + .filter(|declaration| !binding_suppressed(let_scope, *declaration, source)) .collect::>(); let actual = let_scope .bindings .iter() - .filter(|binding| !ignore_cfg || binding.name != "cfg") + .filter(|binding| !is_suppressed(source, binding.range, "let_order")) .map(|binding| binding.declaration) .collect::>(); @@ -35,6 +36,15 @@ pub fn check(graph: &ScopeGraph, ignore_cfg: bool) -> Vec { .collect() } +fn binding_suppressed(let_scope: &LetScope, declaration: NodeId, source: &str) -> bool { + let binding = let_scope + .bindings + .iter() + .find(|binding| binding.declaration == declaration) + .expect("let declaration"); + is_suppressed(source, binding.range, "let_order") +} + fn binding_name(let_scope: &LetScope, declaration: NodeId) -> &str { &let_scope .bindings diff --git a/src/natu/scope_graph.rs b/src/natu/scope_graph.rs index 46dff26..e509f6d 100644 --- a/src/natu/scope_graph.rs +++ b/src/natu/scope_graph.rs @@ -64,6 +64,7 @@ impl ScopeGraph { declaration: self.add_declaration(&binding.key.name, scope), name: binding.key.name.clone(), references: Vec::new(), + range: binding.range, }) .collect::>(); @@ -196,4 +197,5 @@ pub(crate) struct Binding { pub(crate) declaration: NodeId, pub(crate) name: String, pub(crate) references: Vec, + pub(crate) range: ast::SourceRange, } -- 2.47.3