Skip to content

Commit 92005d8

Browse files
committed
refactor: use categories as group
1 parent bc721f7 commit 92005d8

File tree

7 files changed

+167
-65
lines changed

7 files changed

+167
-65
lines changed

crates/pgls_diagnostics_categories/src/categories.rs

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,28 +48,28 @@ define_categories! {
4848
"lint/safety/transactionNesting": "https://pg-language-server.com/latest/rules/transaction-nesting",
4949
// end lint rules
5050
// splinter rules start
51-
"dblint/splinter/authRlsInitplan": "https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan",
52-
"dblint/splinter/authUsersExposed": "https://supabase.com/docs/guides/database/database-linter?lint=0002_auth_users_exposed",
53-
"dblint/splinter/duplicateIndex": "https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index",
54-
"dblint/splinter/extensionInPublic": "https://supabase.com/docs/guides/database/database-linter?lint=0014_extension_in_public",
55-
"dblint/splinter/extensionVersionsOutdated": "https://supabase.com/docs/guides/database/database-linter?lint=0022_extension_versions_outdated",
56-
"dblint/splinter/fkeyToAuthUnique": "https://supabase.com/docs/guides/database/database-linter?lint=0021_fkey_to_auth_unique",
57-
"dblint/splinter/foreignTableInApi": "https://supabase.com/docs/guides/database/database-linter?lint=0017_foreign_table_in_api",
58-
"dblint/splinter/functionSearchPathMutable": "https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable",
59-
"dblint/splinter/insecureQueueExposedInApi": "https://supabase.com/docs/guides/database/database-linter?lint=0019_insecure_queue_exposed_in_api",
60-
"dblint/splinter/materializedViewInApi": "https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api",
61-
"dblint/splinter/multiplePermissivePolicies": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies",
62-
"dblint/splinter/noPrimaryKey": "https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key",
63-
"dblint/splinter/policyExistsRlsDisabled": "https://supabase.com/docs/guides/database/database-linter?lint=0007_policy_exists_rls_disabled",
64-
"dblint/splinter/rlsDisabledInPublic": "https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public",
65-
"dblint/splinter/rlsEnabledNoPolicy": "https://supabase.com/docs/guides/database/database-linter?lint=0008_rls_enabled_no_policy",
66-
"dblint/splinter/rlsReferencesUserMetadata": "https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata",
67-
"dblint/splinter/securityDefinerView": "https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view",
68-
"dblint/splinter/tableBloat": "https://supabase.com/docs/guides/database/database-linter?lint=0020_table_bloat",
69-
"dblint/splinter/unindexedForeignKeys": "https://supabase.com/docs/guides/database/database-linter?lint=0001_unindexed_foreign_keys",
70-
"dblint/splinter/unknown": "https://pg-language-server.com/latest",
71-
"dblint/splinter/unsupportedRegTypes": "https://supabase.com/docs/guides/database/database-linter?lint=unsupported_reg_types",
72-
"dblint/splinter/unusedIndex": "https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index",
51+
"splinter/performance/authRlsInitplan": "https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan",
52+
"splinter/performance/duplicateIndex": "https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index",
53+
"splinter/performance/multiplePermissivePolicies": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies",
54+
"splinter/performance/noPrimaryKey": "https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key",
55+
"splinter/performance/tableBloat": "https://supabase.com/docs/guides/database/database-linter?lint=0020_table_bloat",
56+
"splinter/performance/unindexedForeignKeys": "https://supabase.com/docs/guides/database/database-linter?lint=0001_unindexed_foreign_keys",
57+
"splinter/performance/unusedIndex": "https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index",
58+
"splinter/security/authUsersExposed": "https://supabase.com/docs/guides/database/database-linter?lint=0002_auth_users_exposed",
59+
"splinter/security/extensionInPublic": "https://supabase.com/docs/guides/database/database-linter?lint=0014_extension_in_public",
60+
"splinter/security/extensionVersionsOutdated": "https://supabase.com/docs/guides/database/database-linter?lint=0022_extension_versions_outdated",
61+
"splinter/security/fkeyToAuthUnique": "https://supabase.com/docs/guides/database/database-linter?lint=0021_fkey_to_auth_unique",
62+
"splinter/security/foreignTableInApi": "https://supabase.com/docs/guides/database/database-linter?lint=0017_foreign_table_in_api",
63+
"splinter/security/functionSearchPathMutable": "https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable",
64+
"splinter/security/insecureQueueExposedInApi": "https://supabase.com/docs/guides/database/database-linter?lint=0019_insecure_queue_exposed_in_api",
65+
"splinter/security/materializedViewInApi": "https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api",
66+
"splinter/security/policyExistsRlsDisabled": "https://supabase.com/docs/guides/database/database-linter?lint=0007_policy_exists_rls_disabled",
67+
"splinter/security/rlsDisabledInPublic": "https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public",
68+
"splinter/security/rlsEnabledNoPolicy": "https://supabase.com/docs/guides/database/database-linter?lint=0008_rls_enabled_no_policy",
69+
"splinter/security/rlsReferencesUserMetadata": "https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata",
70+
"splinter/security/securityDefinerView": "https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view",
71+
"splinter/security/unsupportedRegTypes": "https://supabase.com/docs/guides/database/database-linter?lint=unsupported_reg_types",
72+
"splinter/unknown/unknown": "https://pg-language-server.com/latest",
7373
// splinter rules end
7474
;
7575
// General categories

crates/pgls_splinter/src/convert.rs

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use pgls_diagnostics::{Category, Severity, category};
1+
use pgls_diagnostics::{category, Category, Severity};
22
use serde_json::Value;
33

44
use crate::{SplinterAdvices, SplinterDiagnostic, SplinterQueryResult};
@@ -11,8 +11,15 @@ impl From<SplinterQueryResult> for SplinterDiagnostic {
1111
let (schema, object_name, object_type, additional_metadata) =
1212
extract_metadata_fields(&result.metadata);
1313

14+
// for now, we just take the first category as the group
15+
let group = result
16+
.categories
17+
.first()
18+
.map(|s| s.to_lowercase())
19+
.unwrap_or_else(|| "unknown".to_string());
20+
1421
SplinterDiagnostic {
15-
category: rule_name_to_category(&result.name),
22+
category: rule_name_to_category(&result.name, &group),
1623
message: result.detail.into(),
1724
severity,
1825
advices: SplinterAdvices {
@@ -37,32 +44,60 @@ fn parse_severity(level: &str) -> Severity {
3744
}
3845
}
3946

40-
/// Convert rule name to a Category
47+
/// Convert rule name and group to a Category
4148
/// Note: Rule names use snake_case, but categories use camelCase
42-
fn rule_name_to_category(name: &str) -> &'static Category {
43-
match name {
44-
"unindexed_foreign_keys" => category!("dblint/splinter/unindexedForeignKeys"),
45-
"auth_users_exposed" => category!("dblint/splinter/authUsersExposed"),
46-
"auth_rls_initplan" => category!("dblint/splinter/authRlsInitplan"),
47-
"no_primary_key" => category!("dblint/splinter/noPrimaryKey"),
48-
"unused_index" => category!("dblint/splinter/unusedIndex"),
49-
"multiple_permissive_policies" => category!("dblint/splinter/multiplePermissivePolicies"),
50-
"policy_exists_rls_disabled" => category!("dblint/splinter/policyExistsRlsDisabled"),
51-
"rls_enabled_no_policy" => category!("dblint/splinter/rlsEnabledNoPolicy"),
52-
"duplicate_index" => category!("dblint/splinter/duplicateIndex"),
53-
"security_definer_view" => category!("dblint/splinter/securityDefinerView"),
54-
"function_search_path_mutable" => category!("dblint/splinter/functionSearchPathMutable"),
55-
"rls_disabled_in_public" => category!("dblint/splinter/rlsDisabledInPublic"),
56-
"extension_in_public" => category!("dblint/splinter/extensionInPublic"),
57-
"rls_references_user_metadata" => category!("dblint/splinter/rlsReferencesUserMetadata"),
58-
"materialized_view_in_api" => category!("dblint/splinter/materializedViewInApi"),
59-
"foreign_table_in_api" => category!("dblint/splinter/foreignTableInApi"),
60-
"unsupported_reg_types" => category!("dblint/splinter/unsupportedRegTypes"),
61-
"insecure_queue_exposed_in_api" => category!("dblint/splinter/insecureQueueExposedInApi"),
62-
"table_bloat" => category!("dblint/splinter/tableBloat"),
63-
"fkey_to_auth_unique" => category!("dblint/splinter/fkeyToAuthUnique"),
64-
"extension_versions_outdated" => category!("dblint/splinter/extensionVersionsOutdated"),
65-
_ => category!("dblint/splinter/unknown"),
49+
fn rule_name_to_category(name: &str, group: &str) -> &'static Category {
50+
match (group, name) {
51+
("performance", "unindexed_foreign_keys") => {
52+
category!("splinter/performance/unindexedForeignKeys")
53+
}
54+
("performance", "auth_rls_initplan") => {
55+
category!("splinter/performance/authRlsInitplan")
56+
}
57+
("performance", "no_primary_key") => category!("splinter/performance/noPrimaryKey"),
58+
("performance", "unused_index") => category!("splinter/performance/unusedIndex"),
59+
("performance", "duplicate_index") => category!("splinter/performance/duplicateIndex"),
60+
("performance", "table_bloat") => category!("splinter/performance/tableBloat"),
61+
("performance", "multiple_permissive_policies") => {
62+
category!("splinter/performance/multiplePermissivePolicies")
63+
}
64+
("security", "auth_users_exposed") => category!("splinter/security/authUsersExposed"),
65+
("security", "extension_versions_outdated") => {
66+
category!("splinter/security/extensionVersionsOutdated")
67+
}
68+
("security", "policy_exists_rls_disabled") => {
69+
category!("splinter/security/policyExistsRlsDisabled")
70+
}
71+
("security", "rls_enabled_no_policy") => {
72+
category!("splinter/security/rlsEnabledNoPolicy")
73+
}
74+
("security", "security_definer_view") => {
75+
category!("splinter/security/securityDefinerView")
76+
}
77+
("security", "function_search_path_mutable") => {
78+
category!("splinter/security/functionSearchPathMutable")
79+
}
80+
("security", "rls_disabled_in_public") => {
81+
category!("splinter/security/rlsDisabledInPublic")
82+
}
83+
("security", "extension_in_public") => category!("splinter/security/extensionInPublic"),
84+
("security", "rls_references_user_metadata") => {
85+
category!("splinter/security/rlsReferencesUserMetadata")
86+
}
87+
("security", "materialized_view_in_api") => {
88+
category!("splinter/security/materializedViewInApi")
89+
}
90+
("security", "foreign_table_in_api") => {
91+
category!("splinter/security/foreignTableInApi")
92+
}
93+
("security", "unsupported_reg_types") => {
94+
category!("splinter/security/unsupportedRegTypes")
95+
}
96+
("security", "insecure_queue_exposed_in_api") => {
97+
category!("splinter/security/insecureQueueExposedInApi")
98+
}
99+
("security", "fkey_to_auth_unique") => category!("splinter/security/fkeyToAuthUnique"),
100+
_ => category!("splinter/unknown/unknown"),
66101
}
67102
}
68103

crates/pgls_splinter/tests/snapshots/multiple_issues.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ source: crates/pgls_splinter/tests/diagnostics.rs
33
expression: content
44
snapshot_kind: text
55
---
6-
Category: dblint/splinter/unindexedForeignKeys
6+
Category: splinter/performance/unindexedForeignKeys
77
Severity: Information
88
Message: Table \`public.child_table\` has a foreign key \`child_table_parent_id_fkey\` without a covering index. This can lead to suboptimal query performance.
99
Advices:
@@ -14,7 +14,7 @@ Identifies foreign key constraints without a covering index, which can impact da
1414
1515
---
1616
17-
Category: dblint/splinter/noPrimaryKey
17+
Category: splinter/performance/noPrimaryKey
1818
Severity: Information
1919
Message: Table \`public.no_pk_table\` does not have a primary key
2020
Advices:

crates/pgls_splinter/tests/snapshots/no_primary_key.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ source: crates/pgls_splinter/tests/diagnostics.rs
33
expression: content
44
snapshot_kind: text
55
---
6-
Category: dblint/splinter/noPrimaryKey
6+
Category: splinter/performance/noPrimaryKey
77
Severity: Information
88
Message: Table \`public.articles\` does not have a primary key
99
Advices:

crates/pgls_splinter/tests/snapshots/policy_exists_rls_disabled.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ source: crates/pgls_splinter/tests/diagnostics.rs
33
expression: content
44
snapshot_kind: text
55
---
6-
Category: dblint/splinter/policyExistsRlsDisabled
6+
Category: splinter/security/policyExistsRlsDisabled
77
Severity: Error
88
Message: Table \`public.documents\` has RLS policies but RLS is not enabled on the table. Policies include {documents_policy}.
99
Advices:

crates/pgls_splinter/tests/snapshots/unindexed_foreign_key.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ source: crates/pgls_splinter/tests/diagnostics.rs
33
expression: content
44
snapshot_kind: text
55
---
6-
Category: dblint/splinter/unindexedForeignKeys
6+
Category: splinter/performance/unindexedForeignKeys
77
Severity: Information
88
Message: Table \`public.posts\` has a foreign key \`posts_user_id_fkey\` without a covering index. This can lead to suboptimal query performance.
99
Advices:

xtask/codegen/src/generate_splinter.rs

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,40 @@ fn extract_rules_from_sql(content: &str) -> Result<BTreeMap<String, RuleInfo>> {
3030
// Look for pattern: 'rule_name' as "name!",
3131
if line.contains(" as \"name!\"") {
3232
if let Some(name) = extract_string_literal(line) {
33-
// Look ahead for remediation URL
33+
// Look ahead for categories and remediation URL
34+
let mut categories = None;
3435
let mut remediation_url = None;
36+
3537
for j in i..std::cmp::min(i + 30, lines.len()) {
3638
let next_line = lines[j].trim();
39+
40+
// Extract categories from pattern: array['CATEGORY'] as "categories!",
41+
if next_line.contains(" as \"categories!\"") {
42+
categories = extract_categories(next_line);
43+
}
44+
3745
if next_line.contains(" as \"remediation!\"") {
3846
remediation_url = extract_string_literal(next_line);
47+
}
48+
49+
// Stop once we have both
50+
if categories.is_some() && remediation_url.is_some() {
3951
break;
4052
}
4153
}
4254

4355
let url = remediation_url
4456
.with_context(|| format!("Failed to find remediation URL for rule '{name}'"))?;
4557

58+
let cats = categories
59+
.with_context(|| format!("Failed to find categories for rule '{name}'"))?;
60+
4661
rules.insert(
4762
name.clone(),
4863
RuleInfo {
4964
snake_case: name.clone(),
5065
camel_case: snake_to_camel_case(&name),
66+
categories: cats,
5167
url,
5268
},
5369
);
@@ -63,6 +79,7 @@ fn extract_rules_from_sql(content: &str) -> Result<BTreeMap<String, RuleInfo>> {
6379
RuleInfo {
6480
snake_case: "unknown".to_string(),
6581
camel_case: "unknown".to_string(),
82+
categories: vec!["UNKNOWN".to_string()],
6683
url: "https://pg-language-server.com/latest".to_string(),
6784
},
6885
);
@@ -83,6 +100,40 @@ fn extract_string_literal(line: &str) -> Option<String> {
83100
None
84101
}
85102

103+
/// Extract categories from a line like "array['CATEGORY'] as "categories!","
104+
fn extract_categories(line: &str) -> Option<Vec<String>> {
105+
let trimmed = line.trim();
106+
107+
// Look for array['...']
108+
if let Some(start) = trimmed.find("array[") {
109+
if let Some(end) = trimmed[start..].find(']') {
110+
let array_content = &trimmed[start + 6..start + end];
111+
112+
// Extract all string literals within the array
113+
let categories: Vec<String> = array_content
114+
.split(',')
115+
.filter_map(|s| {
116+
let s = s.trim();
117+
if let Some(start_quote) = s.find('\'') {
118+
if let Some(end_quote) = s[start_quote + 1..].find('\'') {
119+
return Some(
120+
s[start_quote + 1..start_quote + 1 + end_quote].to_string(),
121+
);
122+
}
123+
}
124+
None
125+
})
126+
.collect();
127+
128+
if !categories.is_empty() {
129+
return Some(categories);
130+
}
131+
}
132+
}
133+
134+
None
135+
}
136+
86137
/// Convert snake_case to camelCase
87138
fn snake_to_camel_case(s: &str) -> String {
88139
Case::Camel.convert(s)
@@ -92,6 +143,7 @@ struct RuleInfo {
92143
#[allow(dead_code)]
93144
snake_case: String,
94145
camel_case: String,
146+
categories: Vec<String>,
95147
url: String,
96148
}
97149

@@ -102,19 +154,34 @@ fn update_categories_file(rules: BTreeMap<String, RuleInfo>) -> Result<()> {
102154

103155
let content = fs2::read_to_string(&categories_path)?;
104156

105-
// Generate splinter rule entries
106-
let mut splinter_rules: Vec<String> = rules
157+
// Generate splinter rule entries grouped by category
158+
let mut splinter_rules: Vec<(String, String)> = rules
107159
.values()
108-
.map(|rule| {
109-
format!(
110-
" \"dblint/splinter/{}\": \"{}\",",
111-
rule.camel_case, rule.url
112-
)
160+
.flat_map(|rule| {
161+
// For each rule, create entries for all its categories
162+
// In practice, splinter rules have only one category
163+
rule.categories.iter().map(|category| {
164+
let group = category.to_lowercase();
165+
(
166+
group.clone(),
167+
format!(
168+
" \"splinter/{}/{}\": \"{}\",",
169+
group, rule.camel_case, rule.url
170+
),
171+
)
172+
})
113173
})
114-
.collect();
174+
.collect::<Vec<_>>();
175+
176+
// Sort by group, then by entry
177+
splinter_rules.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
115178

116-
splinter_rules.sort();
117-
let splinter_entries = splinter_rules.join("\n");
179+
// Extract just the formatted strings
180+
let splinter_entries: String = splinter_rules
181+
.iter()
182+
.map(|(_, entry)| entry.as_str())
183+
.collect::<Vec<_>>()
184+
.join("\n");
118185

119186
// Replace content between splinter rules markers
120187
let rules_start = "// splinter rules start";

0 commit comments

Comments
 (0)