Skip to content

Commit 6b52e89

Browse files
committed
feat(splinter): add global ignore configuration
Add top-level `ignore` field to splinter configuration that applies to all rules, in addition to existing per-rule ignore. Example configuration: ```json { "splinter": { "ignore": ["audit.*", "temp_*"], "rules": { ... } } } ```
1 parent d1c10da commit 6b52e89

File tree

13 files changed

+365
-71
lines changed

13 files changed

+365
-71
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pgls_configuration/src/splinter/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
mod options;
55
pub use options::SplinterRuleOptions;
66
mod rules;
7+
use biome_deserialize::StringSet;
78
use biome_deserialize_macros::{Merge, Partial};
89
use bpaf::Bpaf;
910
pub use rules::*;
@@ -16,6 +17,11 @@ pub struct SplinterConfiguration {
1617
#[doc = r" if `false`, it disables the feature and the linter won't be executed. `true` by default"]
1718
#[partial(bpaf(hide))]
1819
pub enabled: bool,
20+
#[doc = r" A list of glob patterns for database objects to ignore across all rules."]
21+
#[doc = r" Patterns use Unix-style globs where `*` matches any sequence of characters."]
22+
#[doc = r#" Format: `schema.object_name`, e.g., "public.my_table", "audit.*""#]
23+
#[partial(bpaf(hide))]
24+
pub ignore: StringSet,
1925
#[doc = r" List of rules"]
2026
#[partial(bpaf(pure(Default::default()), optional, hide))]
2127
pub rules: Rules,
@@ -24,11 +30,24 @@ impl SplinterConfiguration {
2430
pub const fn is_disabled(&self) -> bool {
2531
!self.enabled
2632
}
33+
#[doc = r" Build a matcher from the global ignore patterns."]
34+
#[doc = r" Returns None if no patterns are configured."]
35+
pub fn get_global_ignore_matcher(&self) -> Option<pgls_matcher::Matcher> {
36+
if self.ignore.is_empty() {
37+
return None;
38+
}
39+
let mut m = pgls_matcher::Matcher::new(pgls_matcher::MatchOptions::default());
40+
for p in self.ignore.iter() {
41+
let _ = m.add_pattern(p);
42+
}
43+
Some(m)
44+
}
2745
}
2846
impl Default for SplinterConfiguration {
2947
fn default() -> Self {
3048
Self {
3149
enabled: true,
50+
ignore: Default::default(),
3251
rules: Default::default(),
3352
}
3453
}

crates/pgls_splinter/Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ serde_json.workspace = true
2121
sqlx.workspace = true
2222

2323
[dev-dependencies]
24-
insta.workspace = true
25-
pgls_console.workspace = true
26-
pgls_test_utils.workspace = true
24+
biome_deserialize.workspace = true
25+
insta.workspace = true
26+
pgls_console.workspace = true
27+
pgls_test_utils.workspace = true
2728

2829
[lib]
2930
doctest = false

crates/pgls_splinter/src/lib.rs

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pub mod rule;
66
pub mod rules;
77

88
use pgls_analyse::{AnalysisFilter, RegistryVisitor, RuleMeta};
9-
use pgls_configuration::splinter::Rules;
9+
use pgls_configuration::splinter::SplinterConfiguration;
1010
use pgls_schema_cache::SchemaCache;
1111
use sqlx::PgPool;
1212

@@ -18,8 +18,8 @@ pub use rule::SplinterRule;
1818
pub struct SplinterParams<'a> {
1919
pub conn: &'a PgPool,
2020
pub schema_cache: Option<&'a SchemaCache>,
21-
/// Optional rules configuration for per-rule database object filtering
22-
pub rules_config: Option<&'a Rules>,
21+
/// Optional splinter configuration for global and per-rule database object filtering
22+
pub config: Option<&'a SplinterConfiguration>,
2323
}
2424

2525
/// Visitor that collects enabled splinter rules based on filter
@@ -138,24 +138,36 @@ pub async fn run_splinter(
138138

139139
let mut diagnostics: Vec<SplinterDiagnostic> = results.into_iter().map(Into::into).collect();
140140

141-
if let Some(rules_config) = params.rules_config {
142-
let rule_matchers = rules_config.get_ignore_matchers();
141+
if let Some(config) = params.config {
142+
// Build global ignore matcher if patterns exist
143+
let global_ignore_matcher = config.get_global_ignore_matcher();
143144

144-
if !rule_matchers.is_empty() {
145+
// Get per-rule ignore matchers
146+
let rule_matchers = config.rules.get_ignore_matchers();
147+
148+
// Filter diagnostics based on global and per-rule ignore patterns
149+
if global_ignore_matcher.is_some() || !rule_matchers.is_empty() {
145150
diagnostics.retain(|diag| {
146-
let rule_name = diag.category.name().split('/').next_back().unwrap_or("");
151+
let object_identifier = match (&diag.advices.schema, &diag.advices.object_name) {
152+
(Some(schema), Some(name)) => format!("{schema}.{name}"),
153+
_ => return true, // Keep diagnostics without schema.name
154+
};
155+
156+
// Check global ignore first
157+
if let Some(ref matcher) = global_ignore_matcher {
158+
if matcher.matches(&object_identifier) {
159+
return false;
160+
}
161+
}
147162

163+
// Then check per-rule ignore
164+
let rule_name = diag.category.name().split('/').next_back().unwrap_or("");
148165
if let Some(matcher) = rule_matchers.get(rule_name) {
149-
if let (Some(schema), Some(name)) =
150-
(&diag.advices.schema, &diag.advices.object_name)
151-
{
152-
let object_identifier = format!("{schema}.{name}");
153-
154-
if matcher.matches(&object_identifier) {
155-
return false;
156-
}
166+
if matcher.matches(&object_identifier) {
167+
return false;
157168
}
158169
}
170+
159171
true
160172
});
161173
}

0 commit comments

Comments
 (0)