From 14c9a418b622b74b8c731cd0bec27428298d680b Mon Sep 17 00:00:00 2001 From: alexion Date: Wed, 30 Sep 2026 11:03:32 -0400 Subject: [PATCH] feat(natu): support preceding suppressions --- src/dotfiles/default.nix | 2 ++ src/natu/natu_test.rs | 21 +++++++++++++++++++++ src/natu/rules.rs | 12 ++++++++++++ 3 files changed, 35 insertions(+) diff --git a/src/dotfiles/default.nix b/src/dotfiles/default.nix index 4032d6d..a38a11b 100644 --- a/src/dotfiles/default.nix +++ b/src/dotfiles/default.nix @@ -22,6 +22,8 @@ let root = root + "/registry"; }; + # TODO: Refactor guest-module composition. + # NOLINT(single_use_binding) guestModules = import ./guests.nix { inherit inputs diff --git a/src/natu/natu_test.rs b/src/natu/natu_test.rs index c746a75..ccd02fa 100644 --- a/src/natu/natu_test.rs +++ b/src/natu/natu_test.rs @@ -77,6 +77,27 @@ fn natu_preserves_suppressed_bindings() -> googletest::Result<()> { Ok(()) } +#[gtest] +fn natu_preserves_preceding_suppressed_binding() -> googletest::Result<()> { + let report = natu::future_sight(vec![SourceFile { + path: PathBuf::from("testdata/preceding_nolint.nix"), + source: r#" +let + # NOLINT(single_use_binding) + value = { + nested = true; + }; +in +value +"# + .into(), + }]) + .or_fail()?; + + expect_that!(report.is_clean(), is_true()); + Ok(()) +} + #[gtest] fn natu_reports_select_dependency_order() -> googletest::Result<()> { let report = natu::future_sight(vec![SourceFile { diff --git a/src/natu/rules.rs b/src/natu/rules.rs index a7e5a35..9fda5f7 100644 --- a/src/natu/rules.rs +++ b/src/natu/rules.rs @@ -127,6 +127,18 @@ pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph, source: &str) -> Ve pub fn is_suppressed(source: &str, range: ast::SourceRange, rule_id: &str) -> bool { let line = source[range.start..].lines().next().unwrap_or_default(); + line_suppresses(line, rule_id) || preceding_line_suppresses(source, range, rule_id) +} + +fn preceding_line_suppresses(source: &str, range: ast::SourceRange, rule_id: &str) -> bool { + let Some((preceding_source, _current_line)) = source[..range.start].rsplit_once('\n') else { + return false; + }; + let preceding_line = preceding_source.lines().next_back().unwrap_or_default(); + line_suppresses(preceding_line, rule_id) +} + +fn line_suppresses(line: &str, rule_id: &str) -> bool { let Some(comment) = line.split_once('#').map(|(_, comment)| comment.trim()) else { return false; };