From b12cfc0a0f7743d59f67b747ce2da456b4ad23df Mon Sep 17 00:00:00 2001 From: psteinroe Date: Sun, 14 Dec 2025 18:30:18 +0100 Subject: [PATCH 01/14] refactor: analyse --- Cargo.lock | 1 + PLAN.md | 194 ++++++++++++++++++ crates/pgls_analyse/src/context.rs | 96 --------- crates/pgls_analyse/src/filter.rs | 8 +- crates/pgls_analyse/src/lib.rs | 15 +- crates/pgls_analyse/src/metadata.rs | 157 ++++++++++++++ crates/pgls_analyse/src/registry.rs | 118 +---------- crates/pgls_analyser/Cargo.toml | 1 + crates/pgls_analyser/src/lib.rs | 44 ++-- .../src/lint/safety/add_serial_column.rs | 3 +- .../lint/safety/adding_field_with_default.rs | 3 +- .../safety/adding_foreign_key_constraint.rs | 3 +- .../src/lint/safety/adding_not_null_field.rs | 3 +- .../safety/adding_primary_key_constraint.rs | 3 +- .../src/lint/safety/adding_required_field.rs | 3 +- .../src/lint/safety/ban_char_field.rs | 3 +- ...oncurrent_index_creation_in_transaction.rs | 3 +- .../src/lint/safety/ban_drop_column.rs | 3 +- .../src/lint/safety/ban_drop_database.rs | 3 +- .../src/lint/safety/ban_drop_not_null.rs | 3 +- .../src/lint/safety/ban_drop_table.rs | 3 +- .../src/lint/safety/ban_truncate_cascade.rs | 3 +- .../src/lint/safety/changing_column_type.rs | 3 +- .../safety/constraint_missing_not_valid.rs | 3 +- .../src/lint/safety/creating_enum.rs | 3 +- .../lint/safety/disallow_unique_constraint.rs | 3 +- .../src/lint/safety/lock_timeout_warning.rs | 3 +- .../src/lint/safety/multiple_alter_table.rs | 3 +- .../src/lint/safety/prefer_big_int.rs | 3 +- .../src/lint/safety/prefer_bigint_over_int.rs | 3 +- .../safety/prefer_bigint_over_smallint.rs | 3 +- .../src/lint/safety/prefer_identity.rs | 3 +- .../src/lint/safety/prefer_jsonb.rs | 3 +- .../src/lint/safety/prefer_robust_stmts.rs | 3 +- .../src/lint/safety/prefer_text_field.rs | 3 +- .../src/lint/safety/prefer_timestamptz.rs | 3 +- .../src/lint/safety/renaming_column.rs | 3 +- .../src/lint/safety/renaming_table.rs | 3 +- .../require_concurrent_index_creation.rs | 8 +- .../require_concurrent_index_deletion.rs | 3 +- ...tatement_while_holding_access_exclusive.rs | 3 +- .../src/lint/safety/transaction_nesting.rs | 5 +- .../src/linter_context.rs} | 94 +++++++++ .../src/linter_options.rs} | 19 +- crates/pgls_analyser/src/linter_registry.rs | 118 +++++++++++ .../src/linter_rule.rs} | 188 ++--------------- crates/pgls_analyser/src/options.rs | 67 +++--- crates/pgls_analyser/src/registry.rs | 167 ++++++++++++++- crates/pgls_configuration/src/linter/rules.rs | 3 +- .../src/rules/configuration.rs | 3 +- crates/pgls_splinter/src/convert.rs | 7 +- crates/pgls_splinter/src/diagnostics.rs | 10 +- crates/pgls_workspace/src/configuration.rs | 6 +- crates/pgls_workspace/src/workspace/server.rs | 7 +- .../src/workspace/server/analyser.rs | 6 +- docs/codegen/src/rules_docs.rs | 6 +- docs/codegen/src/utils.rs | 8 +- xtask/codegen/src/generate_analyser.rs | 61 +++++- xtask/codegen/src/generate_configuration.rs | 6 +- xtask/rules_check/src/lib.rs | 12 +- 60 files changed, 997 insertions(+), 531 deletions(-) create mode 100644 PLAN.md delete mode 100644 crates/pgls_analyse/src/context.rs create mode 100644 crates/pgls_analyse/src/metadata.rs rename crates/{pgls_analyse/src/analysed_file_context.rs => pgls_analyser/src/linter_context.rs} (66%) rename crates/{pgls_analyse/src/options.rs => pgls_analyser/src/linter_options.rs} (83%) create mode 100644 crates/pgls_analyser/src/linter_registry.rs rename crates/{pgls_analyse/src/rule.rs => pgls_analyser/src/linter_rule.rs} (50%) diff --git a/Cargo.lock b/Cargo.lock index b5f2f6f57..3425f1a83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2667,6 +2667,7 @@ dependencies = [ "pgls_statement_splitter", "pgls_test_macros", "pgls_text_size", + "rustc-hash 2.1.0", "serde", "termcolor", ] diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 000000000..86e0536db --- /dev/null +++ b/PLAN.md @@ -0,0 +1,194 @@ +# Splinter Integration Plan + +## Goal +Integrate splinter into the codegen/rule setup used for the analyser, providing a consistent API (both internally and user-facing) for all types of analysers/linters. + +## Architecture Vision + +### Crate Responsibilities + +**`pgls_analyse`** - Generic framework for all analyzer types +- Generic traits: `RuleMeta`, `RuleGroup`, `GroupCategory`, `RegistryVisitor` +- Shared types: `RuleMetadata`, `RuleCategory` +- Configuration traits (execution-agnostic) +- Macros: `declare_rule!`, `declare_group!`, `declare_category!` + +**`pgls_linter`** (renamed from `pgls_analyser`) - AST-based source code linting +- `LinterRule` trait (extends `RuleMeta`) +- `LinterRuleContext` (wraps AST nodes) +- `LinterDiagnostic` (span-based diagnostics) +- `LinterRuleRegistry` (type-erased executors) + +**`pgls_splinter`** - Database-level linting +- `SplinterRule` trait (extends `RuleMeta`) +- `SplinterRuleRegistry` (metadata-based) +- `SplinterDiagnostic` (db-object-based diagnostics) +- Generated rule types from SQL files + +**`pgls_configuration`** +- `analyser/linter/` - Generated from `pgls_linter` +- `analyser/splinter/` - Generated from `pgls_splinter` +- Per-rule configuration for both + +## Implementation Phases + +### Phase 1: Refactor pgls_analyse ⏳ IN PROGRESS +Extract AST-specific code into pgls_linter, keep only generic framework in pgls_analyse. + +**Tasks:** +- [x] Analyze current `pgls_analyse` exports +- [x] Identify AST-specific vs generic code +- [x] Create new modules in `pgls_analyser`: + - [x] `linter_rule.rs` - LinterRule trait, LinterDiagnostic + - [x] `linter_context.rs` - LinterRuleContext, AnalysedFileContext + - [x] `linter_options.rs` - LinterOptions, LinterRules + - [x] `linter_registry.rs` - LinterRuleRegistry, LinterRegistryVisitor +- [x] Create `pgls_analyse/src/metadata.rs` - Generic traits only +- [x] Update `pgls_analyse/src/registry.rs` - Keep MetadataRegistry only +- [x] Update `pgls_analyse/src/lib.rs` - Export generic framework +- [x] Update `pgls_analyser/src/lib.rs` - Use new modules +- [x] Fix imports in filter.rs (RuleMeta instead of Rule) +- [x] Update generated files (options.rs, registry.rs) +- [x] Fix imports in all rule files +- [x] Add rustc-hash dependency +- [x] Verify compilation completes - **RESOLVED** +- [x] Separate visitor concerns from executor creation +- [x] Update codegen to generate factory function +- [x] Fix all import paths across workspace +- [x] Verify full workspace compiles +- [ ] Run tests + +**Resolution:** +Separated two concerns: +1. **Visitor pattern** (generic): Collects rule keys that match the filter + - Implementation in `LinterRuleRegistryBuilder::record_rule` + - Only requires `R: RuleMeta` (satisfies trait) +2. **Factory mapping** (AST-specific): Maps rule keys to executor factories + - Function `get_linter_rule_factory` in `registry.rs` + - Will be generated by codegen with full type information + - Each factory can require `R: LinterRule` + +**Changes Made:** +- `LinterRuleRegistryBuilder` stores `Vec` instead of factories +- `record_rule` just collects keys (no LinterRule bounds needed) +- `build()` calls `get_linter_rule_factory` to create executors +- Added stub `get_linter_rule_factory` in `registry.rs` (will be generated) + +**Next Steps:** +- Update codegen to generate `get_linter_rule_factory` with match on all rules + +**Design Decisions:** +- ✅ Keep `RuleDiagnostic` generic or make it linter-specific? → **Move to pgls_linter as LinterDiagnostic** (Option A) + - Rationale: Fundamentally different location models (spans vs db objects) + - LinterDiagnostic: span-based + - SplinterDiagnostic: db-object-based + +**Code Classification:** + +AST-specific (move to pgls_analyser): +- `Rule` trait +- `RuleContext` +- `RuleDiagnostic` → `LinterDiagnostic` +- `AnalysedFileContext` +- `RegistryRuleParams` +- `RuleRegistry`, `RuleRegistryBuilder` (AST execution) +- `AnalyserOptions`, `AnalyserRules` (rule options storage) + +Generic (keep in pgls_analyse): +- `RuleMeta` trait +- `RuleMetadata` struct +- `RuleGroup` trait +- `GroupCategory` trait +- `RegistryVisitor` trait +- `RuleCategory` enum +- `RuleSource` enum +- `RuleFilter`, `AnalysisFilter`, `RuleKey`, `GroupKey` +- `MetadataRegistry` +- Macros: `declare_rule!`, `declare_lint_rule!`, `declare_lint_group!`, `declare_category!` + +--- + +### Phase 2: Enhance pgls_splinter 📋 PLANNED +Add rule type generation and registry similar to linter. + +**Tasks:** +- [ ] Create `pgls_splinter/src/rule.rs` with `SplinterRule` trait +- [ ] Create `pgls_splinter/src/rules/` directory structure +- [ ] Generate rule types from SQL files +- [ ] Generate registry with `visit_registry()` function +- [ ] Update diagnostics to use generated categories + +**Structure:** +``` +pgls_splinter/src/ + rules/ + performance/ + unindexed_foreign_keys.rs + auth_rls_initplan.rs + security/ + auth_users_exposed.rs + rule.rs # SplinterRule trait + registry.rs # Generated visit_registry() +``` + +--- + +### Phase 3: Update codegen for both linters 📋 PLANNED +Generalize codegen to handle both linter types. + +**Tasks:** +- [ ] Rename `generate_analyser.rs` → `generate_linter.rs` +- [ ] Enhance `generate_splinter.rs` to generate rules + registry +- [ ] Update `generate_configuration.rs` for both linters +- [ ] Update justfile commands +- [ ] Test full generation cycle + +**Codegen outputs:** +- Linter: registry.rs, options.rs, configuration +- Splinter: rules/, registry.rs, configuration + +--- + +### Phase 4: Rename pgls_analyser → pgls_linter 📋 PLANNED +Final rename to clarify purpose. + +**Tasks:** +- [ ] Rename crate in Cargo.toml +- [ ] Update all imports +- [ ] Update documentation +- [ ] Update CLAUDE.md / AGENTS.md +- [ ] Verify tests pass + +--- + +## Progress Tracking + +### Current Status +- [x] Requirements gathering & design +- [x] Architecture proposal (Option 1 - Dual-Track) +- [ ] Phase 1: Refactor pgls_analyse - **IN PROGRESS** +- [ ] Phase 2: Enhance pgls_splinter +- [ ] Phase 3: Update codegen +- [ ] Phase 4: Rename to pgls_linter + +### Open Questions +None currently + +### Decisions Made +1. Use `LinterRule` (not `ASTRule` or `SourceCodeRule`) for clarity +2. Use `SplinterRule` for database-level rules +3. Keep codegen in xtask (not build.rs) for consistency +4. Mirror file structure between linter and splinter + +--- + +## Testing Strategy +- [ ] Existing linter tests continue to pass +- [ ] Splinter rules generate correctly from SQL +- [ ] Configuration schema validates +- [ ] Integration test: enable/disable rules via config +- [ ] Integration test: severity overrides work + +--- + +Last updated: 2025-12-14 diff --git a/crates/pgls_analyse/src/context.rs b/crates/pgls_analyse/src/context.rs deleted file mode 100644 index bf3f48762..000000000 --- a/crates/pgls_analyse/src/context.rs +++ /dev/null @@ -1,96 +0,0 @@ -use pgls_schema_cache::SchemaCache; - -use crate::{ - AnalysedFileContext, - categories::RuleCategory, - rule::{GroupCategory, Rule, RuleGroup, RuleMetadata}, -}; - -pub struct RuleContext<'a, R: Rule> { - stmt: &'a pgls_query::NodeEnum, - options: &'a R::Options, - schema_cache: Option<&'a SchemaCache>, - file_context: &'a AnalysedFileContext<'a>, -} - -impl<'a, R> RuleContext<'a, R> -where - R: Rule + Sized + 'static, -{ - #[allow(clippy::too_many_arguments)] - pub fn new( - stmt: &'a pgls_query::NodeEnum, - options: &'a R::Options, - schema_cache: Option<&'a SchemaCache>, - file_context: &'a AnalysedFileContext, - ) -> Self { - Self { - stmt, - options, - schema_cache, - file_context, - } - } - - /// Returns the group that belongs to the current rule - pub fn group(&self) -> &'static str { - ::NAME - } - - /// Returns the category that belongs to the current rule - pub fn category(&self) -> RuleCategory { - <::Category as GroupCategory>::CATEGORY - } - - /// Returns the AST root - pub fn stmt(&self) -> &pgls_query::NodeEnum { - self.stmt - } - - pub fn file_context(&self) -> &AnalysedFileContext { - self.file_context - } - - pub fn schema_cache(&self) -> Option<&SchemaCache> { - self.schema_cache - } - - /// Returns the metadata of the rule - /// - /// The metadata contains information about the rule, such as the name, version, language, and whether it is recommended. - /// - /// ## Examples - /// ```rust,ignore - /// declare_lint_rule! { - /// /// Some doc - /// pub(crate) Foo { - /// version: "0.0.0", - /// name: "foo", - /// recommended: true, - /// } - /// } - /// - /// impl Rule for Foo { - /// const CATEGORY: RuleCategory = RuleCategory::Lint; - /// type State = (); - /// type Signals = (); - /// type Options = (); - /// - /// fn run(ctx: &RuleContext) -> Self::Signals { - /// assert_eq!(ctx.metadata().name, "foo"); - /// } - /// } - /// ``` - pub fn metadata(&self) -> &RuleMetadata { - &R::METADATA - } - - /// It retrieves the options that belong to a rule, if they exist. - /// - /// In order to retrieve a typed data structure, you have to create a deserializable - /// data structure and define it inside the generic type `type Options` of the [Rule] - /// - pub fn options(&self) -> &R::Options { - self.options - } -} diff --git a/crates/pgls_analyse/src/filter.rs b/crates/pgls_analyse/src/filter.rs index bb9cdc27c..72e33cf59 100644 --- a/crates/pgls_analyse/src/filter.rs +++ b/crates/pgls_analyse/src/filter.rs @@ -2,7 +2,7 @@ use std::fmt::{Debug, Display, Formatter}; use crate::{ categories::RuleCategories, - rule::{GroupCategory, Rule, RuleGroup}, + metadata::{GroupCategory, RuleGroup, RuleMeta}, }; /// Allow filtering a single rule or group of rules by their names @@ -52,7 +52,7 @@ impl<'analysis> AnalysisFilter<'analysis> { } /// Return `true` if the rule `R` matches this filter - pub fn match_rule(&self) -> bool { + pub fn match_rule(&self) -> bool { self.match_category::<::Category>() && self.enabled_rules.is_none_or(|enabled_rules| { enabled_rules.iter().any(|filter| filter.match_rule::()) @@ -83,7 +83,7 @@ impl<'a> RuleFilter<'a> { /// Return `true` if the rule `R` matches this filter pub fn match_rule(self) -> bool where - R: Rule, + R: RuleMeta, { match self { RuleFilter::Group(group) => group == ::NAME, @@ -160,7 +160,7 @@ impl RuleKey { Self { group, rule } } - pub fn rule() -> Self { + pub fn rule() -> Self { Self::new(::NAME, R::METADATA.name) } diff --git a/crates/pgls_analyse/src/lib.rs b/crates/pgls_analyse/src/lib.rs index 29b18f362..b327b843a 100644 --- a/crates/pgls_analyse/src/lib.rs +++ b/crates/pgls_analyse/src/lib.rs @@ -1,25 +1,16 @@ -mod analysed_file_context; mod categories; -pub mod context; mod filter; pub mod macros; -pub mod options; +mod metadata; mod registry; -mod rule; // Re-exported for use in the `declare_group` macro pub use pgls_diagnostics::category_concat; -pub use crate::analysed_file_context::AnalysedFileContext; pub use crate::categories::{ ActionCategory, RefactorKind, RuleCategories, RuleCategoriesBuilder, RuleCategory, SUPPRESSION_ACTION_CATEGORY, SourceActionKind, }; pub use crate::filter::{AnalysisFilter, GroupKey, RuleFilter, RuleKey}; -pub use crate::options::{AnalyserOptions, AnalyserRules}; -pub use crate::registry::{ - MetadataRegistry, RegistryRuleParams, RegistryVisitor, RuleRegistry, RuleRegistryBuilder, -}; -pub use crate::rule::{ - GroupCategory, Rule, RuleDiagnostic, RuleGroup, RuleMeta, RuleMetadata, RuleSource, -}; +pub use crate::metadata::{GroupCategory, RuleGroup, RuleMeta, RuleMetadata, RuleSource}; +pub use crate::registry::{MetadataRegistry, RegistryVisitor}; diff --git a/crates/pgls_analyse/src/metadata.rs b/crates/pgls_analyse/src/metadata.rs new file mode 100644 index 000000000..a614e400e --- /dev/null +++ b/crates/pgls_analyse/src/metadata.rs @@ -0,0 +1,157 @@ +use pgls_diagnostics::Severity; +use std::cmp::Ordering; + +use crate::{categories::RuleCategory, registry::RegistryVisitor}; + +#[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize), + serde(rename_all = "camelCase") +)] +/// Static metadata containing information about a rule +pub struct RuleMetadata { + /// It marks if a rule is deprecated, and if so a reason has to be provided. + pub deprecated: Option<&'static str>, + /// The version when the rule was implemented + pub version: &'static str, + /// The name of this rule, displayed in the diagnostics it emits + pub name: &'static str, + /// The content of the documentation comments for this rule + pub docs: &'static str, + /// Whether a rule is recommended or not + pub recommended: bool, + /// The source URL of the rule + pub sources: &'static [RuleSource], + /// The default severity of the rule + pub severity: Severity, +} + +impl RuleMetadata { + pub const fn new( + version: &'static str, + name: &'static str, + docs: &'static str, + severity: Severity, + ) -> Self { + Self { + deprecated: None, + version, + name, + docs, + sources: &[], + recommended: false, + severity, + } + } + + pub const fn recommended(mut self, recommended: bool) -> Self { + self.recommended = recommended; + self + } + + pub const fn deprecated(mut self, deprecated: &'static str) -> Self { + self.deprecated = Some(deprecated); + self + } + + pub const fn sources(mut self, sources: &'static [RuleSource]) -> Self { + self.sources = sources; + self + } +} + +pub trait RuleMeta { + type Group: RuleGroup; + const METADATA: RuleMetadata; +} + +/// A rule group is a collection of rules under a given name, serving as a +/// "namespace" for lint rules and allowing the entire set of rules to be +/// disabled at once +pub trait RuleGroup { + type Category: GroupCategory; + /// The name of this group, displayed in the diagnostics emitted by its rules + const NAME: &'static str; + /// Register all the rules belonging to this group into `registry` + fn record_rules(registry: &mut V); +} + +/// A group category is a collection of rule groups under a given category ID, +/// serving as a broad classification on the kind of diagnostic or code action +/// these rule emit, and allowing whole categories of rules to be disabled at +/// once depending on the kind of analysis being performed +pub trait GroupCategory { + /// The category ID used for all groups and rule belonging to this category + const CATEGORY: RuleCategory; + /// Register all the groups belonging to this category into `registry` + fn record_groups(registry: &mut V); +} + +#[derive(Debug, Clone, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +pub enum RuleSource { + /// Rules from [Squawk](https://squawkhq.com) + Squawk(&'static str), + /// Rules from [Eugene](https://github.com/kaaveland/eugene) + Eugene(&'static str), +} + +impl PartialEq for RuleSource { + fn eq(&self, other: &Self) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) + } +} + +impl std::fmt::Display for RuleSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Squawk(_) => write!(f, "Squawk"), + Self::Eugene(_) => write!(f, "Eugene"), + } + } +} + +impl PartialOrd for RuleSource { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for RuleSource { + fn cmp(&self, other: &Self) -> Ordering { + let self_rule = self.as_rule_name(); + let other_rule = other.as_rule_name(); + self_rule.cmp(other_rule) + } +} + +impl RuleSource { + pub fn as_rule_name(&self) -> &'static str { + match self { + Self::Squawk(rule_name) => rule_name, + Self::Eugene(rule_name) => rule_name, + } + } + + pub fn to_namespaced_rule_name(&self) -> String { + match self { + Self::Squawk(rule_name) => format!("squawk/{rule_name}"), + Self::Eugene(rule_name) => format!("eugene/{rule_name}"), + } + } + + pub fn to_rule_url(&self) -> String { + match self { + Self::Squawk(rule_name) => format!("https://squawkhq.com/docs/{rule_name}"), + Self::Eugene(rule_name) => { + format!("https://kaveland.no/eugene/hints/{rule_name}/index.html") + } + } + } + + pub fn as_url_and_rule_name(&self) -> (String, &'static str) { + (self.to_rule_url(), self.as_rule_name()) + } +} diff --git a/crates/pgls_analyse/src/registry.rs b/crates/pgls_analyse/src/registry.rs index 3278c926d..413fa2e6c 100644 --- a/crates/pgls_analyse/src/registry.rs +++ b/crates/pgls_analyse/src/registry.rs @@ -1,11 +1,8 @@ use std::{borrow, collections::BTreeSet}; use crate::{ - AnalyserOptions, - analysed_file_context::AnalysedFileContext, - context::RuleContext, - filter::{AnalysisFilter, GroupKey, RuleKey}, - rule::{GroupCategory, Rule, RuleDiagnostic, RuleGroup}, + filter::{GroupKey, RuleKey}, + metadata::{GroupCategory, RuleGroup, RuleMeta}, }; pub trait RegistryVisitor { @@ -22,7 +19,7 @@ pub trait RegistryVisitor { /// Record the rule `R` to this visitor fn record_rule(&mut self) where - R: Rule + 'static; + R: RuleMeta + 'static; } /// Key struct for a rule in the metadata map, sorted alphabetically @@ -85,115 +82,8 @@ impl MetadataRegistry { impl RegistryVisitor for MetadataRegistry { fn record_rule(&mut self) where - R: Rule + 'static, + R: RuleMeta + 'static, { self.insert_rule(::NAME, R::METADATA.name); } } - -pub struct RuleRegistryBuilder<'a> { - filter: &'a AnalysisFilter<'a>, - // Rule Registry - registry: RuleRegistry, -} - -impl RegistryVisitor for RuleRegistryBuilder<'_> { - fn record_category(&mut self) { - if self.filter.match_category::() { - C::record_groups(self); - } - } - - fn record_group(&mut self) { - if self.filter.match_group::() { - G::record_rules(self); - } - } - - /// Add the rule `R` to the list of rules stored in this registry instance - fn record_rule(&mut self) - where - R: Rule + 'static, - { - if !self.filter.match_rule::() { - return; - } - - let rule = RegistryRule::new::(); - - self.registry.rules.push(rule); - } -} - -/// The rule registry holds type-erased instances of all active analysis rules -pub struct RuleRegistry { - pub rules: Vec, -} - -impl IntoIterator for RuleRegistry { - type Item = RegistryRule; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.rules.into_iter() - } -} - -/// Internal representation of a single rule in the registry -#[derive(Copy, Clone)] -pub struct RegistryRule { - pub run: RuleExecutor, -} - -impl RuleRegistry { - pub fn builder<'a>(filter: &'a AnalysisFilter<'a>) -> RuleRegistryBuilder<'a> { - RuleRegistryBuilder { - filter, - registry: RuleRegistry { - rules: Default::default(), - }, - } - } -} - -pub struct RegistryRuleParams<'a> { - pub root: &'a pgls_query::NodeEnum, - pub options: &'a AnalyserOptions, - pub analysed_file_context: &'a AnalysedFileContext<'a>, - pub schema_cache: Option<&'a pgls_schema_cache::SchemaCache>, -} - -/// Executor for rule as a generic function pointer -type RuleExecutor = fn(&RegistryRuleParams) -> Vec; - -impl RegistryRule { - fn new() -> Self - where - R: Rule + 'static, - { - /// Generic implementation of RuleExecutor for any rule type R - fn run(params: &RegistryRuleParams) -> Vec - where - R: Rule + 'static, - { - let options = params.options.rule_options::().unwrap_or_default(); - - let ctx = RuleContext::new( - params.root, - &options, - params.schema_cache, - params.analysed_file_context, - ); - - R::run(&ctx) - } - - Self { run: run:: } - } -} - -impl RuleRegistryBuilder<'_> { - pub fn build(self) -> RuleRegistry { - self.registry - } -} diff --git a/crates/pgls_analyser/Cargo.toml b/crates/pgls_analyser/Cargo.toml index ce9f0bf22..26c8ba3eb 100644 --- a/crates/pgls_analyser/Cargo.toml +++ b/crates/pgls_analyser/Cargo.toml @@ -19,6 +19,7 @@ pgls_query = { workspace = true } pgls_query_ext = { workspace = true } pgls_schema_cache = { workspace = true } pgls_text_size = { workspace = true } +rustc-hash = { workspace = true } serde = { workspace = true } [dev-dependencies] diff --git a/crates/pgls_analyser/src/lib.rs b/crates/pgls_analyser/src/lib.rs index fa1d407ba..038a53239 100644 --- a/crates/pgls_analyser/src/lib.rs +++ b/crates/pgls_analyser/src/lib.rs @@ -1,21 +1,41 @@ use std::{ops::Deref, sync::LazyLock}; -use pgls_analyse::{ - AnalysedFileContext, AnalyserOptions, AnalysisFilter, MetadataRegistry, RegistryRuleParams, - RuleDiagnostic, RuleRegistry, -}; +use pgls_analyse::{AnalysisFilter, MetadataRegistry}; pub use registry::visit_registry; mod lint; +mod linter_context; +mod linter_options; +mod linter_registry; +mod linter_rule; pub mod options; mod registry; +// Re-export linter-specific types +pub use linter_context::{AnalysedFileContext, LinterRuleContext}; +pub use linter_options::{LinterOptions, LinterRules, RuleOptions}; +pub use linter_registry::{ + LinterRegistryRuleParams, LinterRuleRegistry, LinterRuleRegistryBuilder, +}; +pub use linter_rule::{LinterDiagnostic, LinterRule}; + +// For convenience in macros and rule files - keep these shorter names +pub use LinterDiagnostic as RuleDiagnostic; +pub use LinterRule as Rule; +pub use LinterRuleContext as RuleContext; + pub static METADATA: LazyLock = LazyLock::new(|| { let mut metadata = MetadataRegistry::default(); - visit_registry(&mut metadata); + // Use a separate visitor for metadata that implements pgls_analyse::RegistryVisitor + visit_metadata_registry(&mut metadata); metadata }); +// Separate function for visiting metadata registry (uses pgls_analyse::RegistryVisitor) +fn visit_metadata_registry(registry: &mut V) { + registry.record_category::(); +} + /// Main entry point to the analyser. pub struct Analyser<'a> { /// Holds the metadata for all the rules statically known to the analyser @@ -24,10 +44,10 @@ pub struct Analyser<'a> { metadata: &'a MetadataRegistry, /// Holds all rule options - options: &'a AnalyserOptions, + options: &'a LinterOptions, /// Holds all rules - registry: RuleRegistry, + registry: LinterRuleRegistry, } #[derive(Debug)] @@ -42,13 +62,13 @@ pub struct AnalyserParams<'a> { } pub struct AnalyserConfig<'a> { - pub options: &'a AnalyserOptions, + pub options: &'a LinterOptions, pub filter: AnalysisFilter<'a>, } impl<'a> Analyser<'a> { pub fn new(conf: AnalyserConfig<'a>) -> Self { - let mut builder = RuleRegistry::builder(&conf.filter); + let mut builder = LinterRuleRegistry::builder(&conf.filter); visit_registry(&mut builder); let registry = builder.build(); @@ -59,7 +79,7 @@ impl<'a> Analyser<'a> { } } - pub fn run(&self, params: AnalyserParams) -> Vec { + pub fn run(&self, params: AnalyserParams) -> Vec { let mut diagnostics = vec![]; let roots: Vec = @@ -68,7 +88,7 @@ impl<'a> Analyser<'a> { for (i, stmt) in params.stmts.into_iter().enumerate() { let stmt_diagnostics: Vec<_> = { - let rule_params = RegistryRuleParams { + let rule_params = LinterRegistryRuleParams { root: &roots[i], options: self.options, analysed_file_context: &file_context, @@ -131,7 +151,7 @@ mod tests { let ast = pgls_query::parse(SQL).expect("failed to parse SQL"); let range = TextRange::new(0.into(), u32::try_from(SQL.len()).unwrap().into()); - let options = AnalyserOptions::default(); + let options = LinterOptions::default(); let analyser = Analyser::new(crate::AnalyserConfig { options: &options, diff --git a/crates/pgls_analyser/src/lint/safety/add_serial_column.rs b/crates/pgls_analyser/src/lint/safety/add_serial_column.rs index 0c53c2eb0..c249235fa 100644 --- a/crates/pgls_analyser/src/lint/safety/add_serial_column.rs +++ b/crates/pgls_analyser/src/lint/safety/add_serial_column.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/adding_field_with_default.rs b/crates/pgls_analyser/src/lint/safety/adding_field_with_default.rs index 604f60dab..e9b55cc8b 100644 --- a/crates/pgls_analyser/src/lint/safety/adding_field_with_default.rs +++ b/crates/pgls_analyser/src/lint/safety/adding_field_with_default.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/adding_foreign_key_constraint.rs b/crates/pgls_analyser/src/lint/safety/adding_foreign_key_constraint.rs index 58059b910..5c707339c 100644 --- a/crates/pgls_analyser/src/lint/safety/adding_foreign_key_constraint.rs +++ b/crates/pgls_analyser/src/lint/safety/adding_foreign_key_constraint.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/adding_not_null_field.rs b/crates/pgls_analyser/src/lint/safety/adding_not_null_field.rs index 8b14d5227..bdbbc43b0 100644 --- a/crates/pgls_analyser/src/lint/safety/adding_not_null_field.rs +++ b/crates/pgls_analyser/src/lint/safety/adding_not_null_field.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/adding_primary_key_constraint.rs b/crates/pgls_analyser/src/lint/safety/adding_primary_key_constraint.rs index 129812035..c38225271 100644 --- a/crates/pgls_analyser/src/lint/safety/adding_primary_key_constraint.rs +++ b/crates/pgls_analyser/src/lint/safety/adding_primary_key_constraint.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/adding_required_field.rs b/crates/pgls_analyser/src/lint/safety/adding_required_field.rs index c39597d53..22a978d4d 100644 --- a/crates/pgls_analyser/src/lint/safety/adding_required_field.rs +++ b/crates/pgls_analyser/src/lint/safety/adding_required_field.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/ban_char_field.rs b/crates/pgls_analyser/src/lint/safety/ban_char_field.rs index 39b1cdbe6..f9c948129 100644 --- a/crates/pgls_analyser/src/lint/safety/ban_char_field.rs +++ b/crates/pgls_analyser/src/lint/safety/ban_char_field.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/ban_concurrent_index_creation_in_transaction.rs b/crates/pgls_analyser/src/lint/safety/ban_concurrent_index_creation_in_transaction.rs index 2de0563d1..dcc4af04e 100644 --- a/crates/pgls_analyser/src/lint/safety/ban_concurrent_index_creation_in_transaction.rs +++ b/crates/pgls_analyser/src/lint/safety/ban_concurrent_index_creation_in_transaction.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/ban_drop_column.rs b/crates/pgls_analyser/src/lint/safety/ban_drop_column.rs index 868782a98..f499d7afb 100644 --- a/crates/pgls_analyser/src/lint/safety/ban_drop_column.rs +++ b/crates/pgls_analyser/src/lint/safety/ban_drop_column.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/ban_drop_database.rs b/crates/pgls_analyser/src/lint/safety/ban_drop_database.rs index 837cb8e77..8448eb26a 100644 --- a/crates/pgls_analyser/src/lint/safety/ban_drop_database.rs +++ b/crates/pgls_analyser/src/lint/safety/ban_drop_database.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/ban_drop_not_null.rs b/crates/pgls_analyser/src/lint/safety/ban_drop_not_null.rs index ba4eb09c2..a9ed90ea8 100644 --- a/crates/pgls_analyser/src/lint/safety/ban_drop_not_null.rs +++ b/crates/pgls_analyser/src/lint/safety/ban_drop_not_null.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/ban_drop_table.rs b/crates/pgls_analyser/src/lint/safety/ban_drop_table.rs index f98bc6d26..77265d24e 100644 --- a/crates/pgls_analyser/src/lint/safety/ban_drop_table.rs +++ b/crates/pgls_analyser/src/lint/safety/ban_drop_table.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/ban_truncate_cascade.rs b/crates/pgls_analyser/src/lint/safety/ban_truncate_cascade.rs index 892086a5f..e2bd9009b 100644 --- a/crates/pgls_analyser/src/lint/safety/ban_truncate_cascade.rs +++ b/crates/pgls_analyser/src/lint/safety/ban_truncate_cascade.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; use pgls_query::protobuf::DropBehavior; diff --git a/crates/pgls_analyser/src/lint/safety/changing_column_type.rs b/crates/pgls_analyser/src/lint/safety/changing_column_type.rs index 9e7cc9e75..f093b4242 100644 --- a/crates/pgls_analyser/src/lint/safety/changing_column_type.rs +++ b/crates/pgls_analyser/src/lint/safety/changing_column_type.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/constraint_missing_not_valid.rs b/crates/pgls_analyser/src/lint/safety/constraint_missing_not_valid.rs index 5599317ec..1e27fc06e 100644 --- a/crates/pgls_analyser/src/lint/safety/constraint_missing_not_valid.rs +++ b/crates/pgls_analyser/src/lint/safety/constraint_missing_not_valid.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/creating_enum.rs b/crates/pgls_analyser/src/lint/safety/creating_enum.rs index fa7eb61d7..e6b6ccf8a 100644 --- a/crates/pgls_analyser/src/lint/safety/creating_enum.rs +++ b/crates/pgls_analyser/src/lint/safety/creating_enum.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/disallow_unique_constraint.rs b/crates/pgls_analyser/src/lint/safety/disallow_unique_constraint.rs index 5fdf710c8..c7a081133 100644 --- a/crates/pgls_analyser/src/lint/safety/disallow_unique_constraint.rs +++ b/crates/pgls_analyser/src/lint/safety/disallow_unique_constraint.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/lock_timeout_warning.rs b/crates/pgls_analyser/src/lint/safety/lock_timeout_warning.rs index f3c5af62e..7660b9db4 100644 --- a/crates/pgls_analyser/src/lint/safety/lock_timeout_warning.rs +++ b/crates/pgls_analyser/src/lint/safety/lock_timeout_warning.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/multiple_alter_table.rs b/crates/pgls_analyser/src/lint/safety/multiple_alter_table.rs index 907a29b00..b7b7178b1 100644 --- a/crates/pgls_analyser/src/lint/safety/multiple_alter_table.rs +++ b/crates/pgls_analyser/src/lint/safety/multiple_alter_table.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/prefer_big_int.rs b/crates/pgls_analyser/src/lint/safety/prefer_big_int.rs index c89a08b5a..5aae8df47 100644 --- a/crates/pgls_analyser/src/lint/safety/prefer_big_int.rs +++ b/crates/pgls_analyser/src/lint/safety/prefer_big_int.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/prefer_bigint_over_int.rs b/crates/pgls_analyser/src/lint/safety/prefer_bigint_over_int.rs index 71db41684..58d0deff3 100644 --- a/crates/pgls_analyser/src/lint/safety/prefer_bigint_over_int.rs +++ b/crates/pgls_analyser/src/lint/safety/prefer_bigint_over_int.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/prefer_bigint_over_smallint.rs b/crates/pgls_analyser/src/lint/safety/prefer_bigint_over_smallint.rs index 06e92bb11..066923b02 100644 --- a/crates/pgls_analyser/src/lint/safety/prefer_bigint_over_smallint.rs +++ b/crates/pgls_analyser/src/lint/safety/prefer_bigint_over_smallint.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/prefer_identity.rs b/crates/pgls_analyser/src/lint/safety/prefer_identity.rs index ddc378b5c..bc82b1967 100644 --- a/crates/pgls_analyser/src/lint/safety/prefer_identity.rs +++ b/crates/pgls_analyser/src/lint/safety/prefer_identity.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/prefer_jsonb.rs b/crates/pgls_analyser/src/lint/safety/prefer_jsonb.rs index 637d32f01..16f51f9ff 100644 --- a/crates/pgls_analyser/src/lint/safety/prefer_jsonb.rs +++ b/crates/pgls_analyser/src/lint/safety/prefer_jsonb.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/prefer_robust_stmts.rs b/crates/pgls_analyser/src/lint/safety/prefer_robust_stmts.rs index 021c08473..0451db031 100644 --- a/crates/pgls_analyser/src/lint/safety/prefer_robust_stmts.rs +++ b/crates/pgls_analyser/src/lint/safety/prefer_robust_stmts.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/prefer_text_field.rs b/crates/pgls_analyser/src/lint/safety/prefer_text_field.rs index ce03c9958..847ad43d5 100644 --- a/crates/pgls_analyser/src/lint/safety/prefer_text_field.rs +++ b/crates/pgls_analyser/src/lint/safety/prefer_text_field.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/prefer_timestamptz.rs b/crates/pgls_analyser/src/lint/safety/prefer_timestamptz.rs index 7dc876783..8fd048e01 100644 --- a/crates/pgls_analyser/src/lint/safety/prefer_timestamptz.rs +++ b/crates/pgls_analyser/src/lint/safety/prefer_timestamptz.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/renaming_column.rs b/crates/pgls_analyser/src/lint/safety/renaming_column.rs index a7d151480..a54fbb51a 100644 --- a/crates/pgls_analyser/src/lint/safety/renaming_column.rs +++ b/crates/pgls_analyser/src/lint/safety/renaming_column.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/renaming_table.rs b/crates/pgls_analyser/src/lint/safety/renaming_table.rs index 6c3465da8..2cc1046fc 100644 --- a/crates/pgls_analyser/src/lint/safety/renaming_table.rs +++ b/crates/pgls_analyser/src/lint/safety/renaming_table.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/require_concurrent_index_creation.rs b/crates/pgls_analyser/src/lint/safety/require_concurrent_index_creation.rs index e2289ea8a..257536d3a 100644 --- a/crates/pgls_analyser/src/lint/safety/require_concurrent_index_creation.rs +++ b/crates/pgls_analyser/src/lint/safety/require_concurrent_index_creation.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; @@ -74,10 +75,7 @@ impl Rule for RequireConcurrentIndexCreation { } } -fn is_table_created_in_file( - file_context: &pgls_analyse::AnalysedFileContext, - table_name: &str, -) -> bool { +fn is_table_created_in_file(file_context: &crate::AnalysedFileContext, table_name: &str) -> bool { // Check all statements in the file to see if this table was created for stmt in file_context.stmts { if let pgls_query::NodeEnum::CreateStmt(create_stmt) = stmt { diff --git a/crates/pgls_analyser/src/lint/safety/require_concurrent_index_deletion.rs b/crates/pgls_analyser/src/lint/safety/require_concurrent_index_deletion.rs index cac317170..7117908c4 100644 --- a/crates/pgls_analyser/src/lint/safety/require_concurrent_index_deletion.rs +++ b/crates/pgls_analyser/src/lint/safety/require_concurrent_index_deletion.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/running_statement_while_holding_access_exclusive.rs b/crates/pgls_analyser/src/lint/safety/running_statement_while_holding_access_exclusive.rs index 7d18e7b16..f481f76a9 100644 --- a/crates/pgls_analyser/src/lint/safety/running_statement_while_holding_access_exclusive.rs +++ b/crates/pgls_analyser/src/lint/safety/running_statement_while_holding_access_exclusive.rs @@ -1,4 +1,5 @@ -use pgls_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule}; +use crate::{Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyser/src/lint/safety/transaction_nesting.rs b/crates/pgls_analyser/src/lint/safety/transaction_nesting.rs index 4f76c99db..d7205c945 100644 --- a/crates/pgls_analyser/src/lint/safety/transaction_nesting.rs +++ b/crates/pgls_analyser/src/lint/safety/transaction_nesting.rs @@ -1,6 +1,5 @@ -use pgls_analyse::{ - AnalysedFileContext, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, -}; +use crate::{AnalysedFileContext, Rule, RuleContext, RuleDiagnostic}; +use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; diff --git a/crates/pgls_analyse/src/analysed_file_context.rs b/crates/pgls_analyser/src/linter_context.rs similarity index 66% rename from crates/pgls_analyse/src/analysed_file_context.rs rename to crates/pgls_analyser/src/linter_context.rs index 5907d70e6..d445efb41 100644 --- a/crates/pgls_analyse/src/analysed_file_context.rs +++ b/crates/pgls_analyser/src/linter_context.rs @@ -1,3 +1,97 @@ +use pgls_analyse::{GroupCategory, RuleCategory, RuleGroup, RuleMetadata}; +use pgls_schema_cache::SchemaCache; + +use crate::linter_rule::LinterRule; + +pub struct LinterRuleContext<'a, R: LinterRule> { + stmt: &'a pgls_query::NodeEnum, + options: &'a R::Options, + schema_cache: Option<&'a SchemaCache>, + file_context: &'a AnalysedFileContext<'a>, +} + +impl<'a, R> LinterRuleContext<'a, R> +where + R: LinterRule + Sized + 'static, +{ + #[allow(clippy::too_many_arguments)] + pub fn new( + stmt: &'a pgls_query::NodeEnum, + options: &'a R::Options, + schema_cache: Option<&'a SchemaCache>, + file_context: &'a AnalysedFileContext, + ) -> Self { + Self { + stmt, + options, + schema_cache, + file_context, + } + } + + /// Returns the group that belongs to the current rule + pub fn group(&self) -> &'static str { + ::NAME + } + + /// Returns the category that belongs to the current rule + pub fn category(&self) -> RuleCategory { + <::Category as GroupCategory>::CATEGORY + } + + /// Returns the AST root + pub fn stmt(&self) -> &pgls_query::NodeEnum { + self.stmt + } + + pub fn file_context(&self) -> &AnalysedFileContext { + self.file_context + } + + pub fn schema_cache(&self) -> Option<&SchemaCache> { + self.schema_cache + } + + /// Returns the metadata of the rule + /// + /// The metadata contains information about the rule, such as the name, version, language, and whether it is recommended. + /// + /// ## Examples + /// ```rust,ignore + /// declare_lint_rule! { + /// /// Some doc + /// pub(crate) Foo { + /// version: "0.0.0", + /// name: "foo", + /// recommended: true, + /// } + /// } + /// + /// impl LinterRule for Foo { + /// const CATEGORY: RuleCategory = RuleCategory::Lint; + /// type State = (); + /// type Signals = (); + /// type Options = (); + /// + /// fn run(ctx: &LinterRuleContext) -> Self::Signals { + /// assert_eq!(ctx.metadata().name, "foo"); + /// } + /// } + /// ``` + pub fn metadata(&self) -> &RuleMetadata { + &R::METADATA + } + + /// It retrieves the options that belong to a rule, if they exist. + /// + /// In order to retrieve a typed data structure, you have to create a deserializable + /// data structure and define it inside the generic type `type Options` of the [LinterRule] + /// + pub fn options(&self) -> &R::Options { + self.options + } +} + pub struct AnalysedFileContext<'a> { pub stmts: &'a Vec, pos: usize, diff --git a/crates/pgls_analyse/src/options.rs b/crates/pgls_analyser/src/linter_options.rs similarity index 83% rename from crates/pgls_analyse/src/options.rs rename to crates/pgls_analyser/src/linter_options.rs index e46b7104f..65460236c 100644 --- a/crates/pgls_analyse/src/options.rs +++ b/crates/pgls_analyser/src/linter_options.rs @@ -1,9 +1,10 @@ +use pgls_analyse::RuleKey; use rustc_hash::FxHashMap; - -use crate::{Rule, RuleKey}; use std::any::{Any, TypeId}; use std::fmt::Debug; +use crate::linter_rule::LinterRule; + /// A convenient new type data structure to store the options that belong to a rule #[derive(Debug)] pub struct RuleOptions(TypeId, Box); @@ -28,9 +29,9 @@ impl RuleOptions { /// A convenient new type data structure to insert and get rules #[derive(Debug, Default)] -pub struct AnalyserRules(FxHashMap); +pub struct LinterRules(FxHashMap); -impl AnalyserRules { +impl LinterRules { /// It tracks the options of a specific rule pub fn push_rule(&mut self, rule_key: RuleKey, options: RuleOptions) { self.0.insert(rule_key, options); @@ -42,17 +43,17 @@ impl AnalyserRules { } } -/// A set of information useful to the analyser infrastructure +/// A set of information useful to the linter infrastructure #[derive(Debug, Default)] -pub struct AnalyserOptions { +pub struct LinterOptions { /// A data structured derived from the [`postgres-language-server.jsonc`] file - pub rules: AnalyserRules, + pub rules: LinterRules, } -impl AnalyserOptions { +impl LinterOptions { pub fn rule_options(&self) -> Option where - R: Rule + 'static, + R: LinterRule + 'static, { self.rules .get_rule_options::(&RuleKey::rule::()) diff --git a/crates/pgls_analyser/src/linter_registry.rs b/crates/pgls_analyser/src/linter_registry.rs new file mode 100644 index 000000000..5840d069f --- /dev/null +++ b/crates/pgls_analyser/src/linter_registry.rs @@ -0,0 +1,118 @@ +use pgls_analyse::{AnalysisFilter, GroupCategory, RuleGroup, RuleKey}; + +use crate::linter_context::{AnalysedFileContext, LinterRuleContext}; +use crate::linter_options::LinterOptions; +use crate::linter_rule::{LinterDiagnostic, LinterRule}; + +pub struct LinterRuleRegistryBuilder<'a> { + filter: &'a AnalysisFilter<'a>, + // Store rule keys discovered during traversal + rule_keys: Vec, +} + +// Implement RegistryVisitor - only needs RuleMeta! +impl pgls_analyse::RegistryVisitor for LinterRuleRegistryBuilder<'_> { + fn record_category(&mut self) { + if self.filter.match_category::() { + C::record_groups(self); + } + } + + fn record_group(&mut self) { + if self.filter.match_group::() { + G::record_rules(self); + } + } + + fn record_rule(&mut self) + where + R: pgls_analyse::RuleMeta + 'static, + { + // Visitor just collects which rules match the filter + // No executor creation happens here - that's AST-specific + if self.filter.match_rule::() { + self.rule_keys.push(RuleKey::rule::()); + } + } +} + +/// The rule registry holds type-erased instances of all active analysis rules +pub struct LinterRuleRegistry { + pub rules: Vec, +} + +impl IntoIterator for LinterRuleRegistry { + type Item = RegistryLinterRule; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.rules.into_iter() + } +} + +/// Internal representation of a single rule in the registry +#[derive(Copy, Clone)] +pub struct RegistryLinterRule { + pub run: LinterRuleExecutor, +} + +impl LinterRuleRegistry { + pub fn builder<'a>(filter: &'a AnalysisFilter<'a>) -> LinterRuleRegistryBuilder<'a> { + LinterRuleRegistryBuilder { + filter, + rule_keys: Vec::new(), + } + } +} + +pub struct LinterRegistryRuleParams<'a> { + pub root: &'a pgls_query::NodeEnum, + pub options: &'a LinterOptions, + pub analysed_file_context: &'a AnalysedFileContext<'a>, + pub schema_cache: Option<&'a pgls_schema_cache::SchemaCache>, +} + +/// Executor for rule as a generic function pointer +type LinterRuleExecutor = fn(&LinterRegistryRuleParams) -> Vec; + +impl RegistryLinterRule { + pub fn new() -> Self + where + R: LinterRule + 'static, + { + /// Generic implementation of LinterRuleExecutor for any rule type R + fn run(params: &LinterRegistryRuleParams) -> Vec + where + R: LinterRule + 'static, + { + let options = params.options.rule_options::().unwrap_or_default(); + + let ctx = LinterRuleContext::new( + params.root, + &options, + params.schema_cache, + params.analysed_file_context, + ); + + R::run(&ctx) + } + + Self { run: run:: } + } +} + +impl LinterRuleRegistryBuilder<'_> { + pub fn build(self) -> LinterRuleRegistry { + // Look up factory for each collected rule key and create executors + let rules = self + .rule_keys + .into_iter() + .filter_map(|key| { + // This function will be generated by codegen + crate::registry::get_linter_rule_factory(&key).map(|factory| factory()) + }) + .collect(); + + LinterRuleRegistry { rules } + } +} diff --git a/crates/pgls_analyse/src/rule.rs b/crates/pgls_analyser/src/linter_rule.rs similarity index 50% rename from crates/pgls_analyse/src/rule.rs rename to crates/pgls_analyser/src/linter_rule.rs index 42a08cda3..d9fc8d006 100644 --- a/crates/pgls_analyse/src/rule.rs +++ b/crates/pgls_analyser/src/linter_rule.rs @@ -1,114 +1,28 @@ +use pgls_analyse::RuleMeta; use pgls_console::fmt::Display; use pgls_console::{MarkupBuf, markup}; use pgls_diagnostics::advice::CodeSuggestionAdvice; use pgls_diagnostics::{ Advices, Category, Diagnostic, DiagnosticTags, Location, LogCategory, MessageAndDescription, - Severity, Visit, + Visit, }; use pgls_text_size::TextRange; -use std::cmp::Ordering; use std::fmt::Debug; -use crate::{categories::RuleCategory, context::RuleContext, registry::RegistryVisitor}; +use crate::linter_context::LinterRuleContext; -#[derive(Clone, Debug)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize), - serde(rename_all = "camelCase") -)] -/// Static metadata containing information about a rule -pub struct RuleMetadata { - /// It marks if a rule is deprecated, and if so a reason has to be provided. - pub deprecated: Option<&'static str>, - /// The version when the rule was implemented - pub version: &'static str, - /// The name of this rule, displayed in the diagnostics it emits - pub name: &'static str, - /// The content of the documentation comments for this rule - pub docs: &'static str, - /// Whether a rule is recommended or not - pub recommended: bool, - /// The source URL of the rule - pub sources: &'static [RuleSource], - /// The default severity of the rule - pub severity: Severity, -} - -impl RuleMetadata { - pub const fn new( - version: &'static str, - name: &'static str, - docs: &'static str, - severity: Severity, - ) -> Self { - Self { - deprecated: None, - version, - name, - docs, - sources: &[], - recommended: false, - severity, - } - } - - pub const fn recommended(mut self, recommended: bool) -> Self { - self.recommended = recommended; - self - } - - pub const fn deprecated(mut self, deprecated: &'static str) -> Self { - self.deprecated = Some(deprecated); - self - } - - pub const fn sources(mut self, sources: &'static [RuleSource]) -> Self { - self.sources = sources; - self - } -} - -pub trait RuleMeta { - type Group: RuleGroup; - const METADATA: RuleMetadata; -} - -/// A rule group is a collection of rules under a given name, serving as a -/// "namespace" for lint rules and allowing the entire set of rules to be -/// disabled at once -pub trait RuleGroup { - type Category: GroupCategory; - /// The name of this group, displayed in the diagnostics emitted by its rules - const NAME: &'static str; - /// Register all the rules belonging to this group into `registry` - fn record_rules(registry: &mut V); -} - -/// A group category is a collection of rule groups under a given category ID, -/// serving as a broad classification on the kind of diagnostic or code action -/// these rule emit, and allowing whole categories of rules to be disabled at -/// once depending on the kind of analysis being performed -pub trait GroupCategory { - /// The category ID used for all groups and rule belonging to this category - const CATEGORY: RuleCategory; - /// Register all the groups belonging to this category into `registry` - fn record_groups(registry: &mut V); -} - -/// Trait implemented by all analysis rules: declares interest to a certain AstNode type, -/// and a callback function to be executed on all nodes matching the query to possibly -/// raise an analysis event -pub trait Rule: RuleMeta + Sized { +/// Trait implemented by all AST-based linter rules +pub trait LinterRule: RuleMeta + Sized { type Options: Default + Clone + Debug; + /// Execute the rule on the given AST context /// `schema_cache` will only be available if the user has a working database connection. - fn run(rule_context: &RuleContext) -> Vec; + fn run(rule_context: &LinterRuleContext) -> Vec; } -/// Diagnostic object returned by a single analysis rule +/// Diagnostic object returned by a single linter rule #[derive(Debug, Diagnostic, PartialEq)] -pub struct RuleDiagnostic { +pub struct LinterDiagnostic { #[category] pub(crate) category: &'static Category, #[location(span)] @@ -180,8 +94,8 @@ pub struct Detail { pub range: Option, } -impl RuleDiagnostic { - /// Creates a new [`RuleDiagnostic`] with a severity and title that will be +impl LinterDiagnostic { + /// Creates a new [`LinterDiagnostic`] with a severity and title that will be /// used in a builder-like way to modify labels. pub fn new(category: &'static Category, span: Option, title: impl Display) -> Self { let message = markup!({ title }).to_owned(); @@ -224,9 +138,9 @@ impl RuleDiagnostic { self } - /// Attaches a label to this [`RuleDiagnostic`]. + /// Attaches a label to this [`LinterDiagnostic`]. /// - /// The given span has to be in the file that was provided while creating this [`RuleDiagnostic`]. + /// The given span has to be in the file that was provided while creating this [`LinterDiagnostic`]. pub fn label(mut self, span: Option, msg: impl Display) -> Self { self.rule_advice.details.push(Detail { log_category: LogCategory::Info, @@ -236,12 +150,12 @@ impl RuleDiagnostic { self } - /// Attaches a detailed message to this [`RuleDiagnostic`]. + /// Attaches a detailed message to this [`LinterDiagnostic`]. pub fn detail(self, span: Option, msg: impl Display) -> Self { self.label(span, msg) } - /// Adds a footer to this [`RuleDiagnostic`], which will be displayed under the actual error. + /// Adds a footer to this [`LinterDiagnostic`], which will be displayed under the actual error. fn footer(mut self, log_category: LogCategory, msg: impl Display) -> Self { self.rule_advice .notes @@ -249,7 +163,7 @@ impl RuleDiagnostic { self } - /// Adds a footer to this [`RuleDiagnostic`], with the `Info` log category. + /// Adds a footer to this [`LinterDiagnostic`], with the `Info` log category. pub fn note(self, msg: impl Display) -> Self { self.footer(LogCategory::Info, msg) } @@ -270,7 +184,7 @@ impl RuleDiagnostic { self } - /// Adds a footer to this [`RuleDiagnostic`], with the `Warn` severity. + /// Adds a footer to this [`LinterDiagnostic`], with the `Warn` severity. pub fn warning(self, msg: impl Display) -> Self { self.footer(LogCategory::Warn, msg) } @@ -284,71 +198,3 @@ impl RuleDiagnostic { self.category.name() } } - -#[derive(Debug, Clone, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -pub enum RuleSource { - /// Rules from [Squawk](https://squawkhq.com) - Squawk(&'static str), - /// Rules from [Eugene](https://github.com/kaaveland/eugene) - Eugene(&'static str), -} - -impl PartialEq for RuleSource { - fn eq(&self, other: &Self) -> bool { - std::mem::discriminant(self) == std::mem::discriminant(other) - } -} - -impl std::fmt::Display for RuleSource { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Squawk(_) => write!(f, "Squawk"), - Self::Eugene(_) => write!(f, "Eugene"), - } - } -} - -impl PartialOrd for RuleSource { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for RuleSource { - fn cmp(&self, other: &Self) -> Ordering { - let self_rule = self.as_rule_name(); - let other_rule = other.as_rule_name(); - self_rule.cmp(other_rule) - } -} - -impl RuleSource { - pub fn as_rule_name(&self) -> &'static str { - match self { - Self::Squawk(rule_name) => rule_name, - Self::Eugene(rule_name) => rule_name, - } - } - - pub fn to_namespaced_rule_name(&self) -> String { - match self { - Self::Squawk(rule_name) => format!("squawk/{rule_name}"), - Self::Eugene(rule_name) => format!("eugene/{rule_name}"), - } - } - - pub fn to_rule_url(&self) -> String { - match self { - Self::Squawk(rule_name) => format!("https://squawkhq.com/docs/{rule_name}"), - Self::Eugene(rule_name) => { - format!("https://kaveland.no/eugene/hints/{rule_name}/index.html") - } - } - } - - pub fn as_url_and_rule_name(&self) -> (String, &'static str) { - (self.to_rule_url(), self.as_rule_name()) - } -} diff --git a/crates/pgls_analyser/src/options.rs b/crates/pgls_analyser/src/options.rs index 0cd484bf2..b04539c45 100644 --- a/crates/pgls_analyser/src/options.rs +++ b/crates/pgls_analyser/src/options.rs @@ -2,53 +2,54 @@ use crate::lint; pub type AddSerialColumn = - ::Options; -pub type AddingFieldWithDefault = < lint :: safety :: adding_field_with_default :: AddingFieldWithDefault as pgls_analyse :: Rule > :: Options ; -pub type AddingForeignKeyConstraint = < lint :: safety :: adding_foreign_key_constraint :: AddingForeignKeyConstraint as pgls_analyse :: Rule > :: Options ; + ::Options; +pub type AddingFieldWithDefault = + ::Options; +pub type AddingForeignKeyConstraint = < lint :: safety :: adding_foreign_key_constraint :: AddingForeignKeyConstraint as crate::LinterRule > :: Options ; pub type AddingNotNullField = - ::Options; -pub type AddingPrimaryKeyConstraint = < lint :: safety :: adding_primary_key_constraint :: AddingPrimaryKeyConstraint as pgls_analyse :: Rule > :: Options ; + ::Options; +pub type AddingPrimaryKeyConstraint = < lint :: safety :: adding_primary_key_constraint :: AddingPrimaryKeyConstraint as crate::LinterRule > :: Options ; pub type AddingRequiredField = - ::Options; -pub type BanCharField = ::Options; -pub type BanConcurrentIndexCreationInTransaction = < lint :: safety :: ban_concurrent_index_creation_in_transaction :: BanConcurrentIndexCreationInTransaction as pgls_analyse :: Rule > :: Options ; + ::Options; +pub type BanCharField = ::Options; +pub type BanConcurrentIndexCreationInTransaction = < lint :: safety :: ban_concurrent_index_creation_in_transaction :: BanConcurrentIndexCreationInTransaction as crate::LinterRule > :: Options ; pub type BanDropColumn = - ::Options; + ::Options; pub type BanDropDatabase = - ::Options; + ::Options; pub type BanDropNotNull = - ::Options; -pub type BanDropTable = ::Options; + ::Options; +pub type BanDropTable = ::Options; pub type BanTruncateCascade = - ::Options; + ::Options; pub type ChangingColumnType = - ::Options; -pub type ConstraintMissingNotValid = < lint :: safety :: constraint_missing_not_valid :: ConstraintMissingNotValid as pgls_analyse :: Rule > :: Options ; -pub type CreatingEnum = ::Options; -pub type DisallowUniqueConstraint = < lint :: safety :: disallow_unique_constraint :: DisallowUniqueConstraint as pgls_analyse :: Rule > :: Options ; + ::Options; +pub type ConstraintMissingNotValid = < lint :: safety :: constraint_missing_not_valid :: ConstraintMissingNotValid as crate::LinterRule > :: Options ; +pub type CreatingEnum = ::Options; +pub type DisallowUniqueConstraint = < lint :: safety :: disallow_unique_constraint :: DisallowUniqueConstraint as crate::LinterRule > :: Options ; pub type LockTimeoutWarning = - ::Options; + ::Options; pub type MultipleAlterTable = - ::Options; -pub type PreferBigInt = ::Options; + ::Options; +pub type PreferBigInt = ::Options; pub type PreferBigintOverInt = - ::Options; -pub type PreferBigintOverSmallint = < lint :: safety :: prefer_bigint_over_smallint :: PreferBigintOverSmallint as pgls_analyse :: Rule > :: Options ; + ::Options; +pub type PreferBigintOverSmallint = < lint :: safety :: prefer_bigint_over_smallint :: PreferBigintOverSmallint as crate::LinterRule > :: Options ; pub type PreferIdentity = - ::Options; -pub type PreferJsonb = ::Options; + ::Options; +pub type PreferJsonb = ::Options; pub type PreferRobustStmts = - ::Options; + ::Options; pub type PreferTextField = - ::Options; + ::Options; pub type PreferTimestamptz = - ::Options; + ::Options; pub type RenamingColumn = - ::Options; + ::Options; pub type RenamingTable = - ::Options; -pub type RequireConcurrentIndexCreation = < lint :: safety :: require_concurrent_index_creation :: RequireConcurrentIndexCreation as pgls_analyse :: Rule > :: Options ; -pub type RequireConcurrentIndexDeletion = < lint :: safety :: require_concurrent_index_deletion :: RequireConcurrentIndexDeletion as pgls_analyse :: Rule > :: Options ; -pub type RunningStatementWhileHoldingAccessExclusive = < lint :: safety :: running_statement_while_holding_access_exclusive :: RunningStatementWhileHoldingAccessExclusive as pgls_analyse :: Rule > :: Options ; + ::Options; +pub type RequireConcurrentIndexCreation = < lint :: safety :: require_concurrent_index_creation :: RequireConcurrentIndexCreation as crate::LinterRule > :: Options ; +pub type RequireConcurrentIndexDeletion = < lint :: safety :: require_concurrent_index_deletion :: RequireConcurrentIndexDeletion as crate::LinterRule > :: Options ; +pub type RunningStatementWhileHoldingAccessExclusive = < lint :: safety :: running_statement_while_holding_access_exclusive :: RunningStatementWhileHoldingAccessExclusive as crate::LinterRule > :: Options ; pub type TransactionNesting = - ::Options; + ::Options; diff --git a/crates/pgls_analyser/src/registry.rs b/crates/pgls_analyser/src/registry.rs index 4978e40fd..29862d096 100644 --- a/crates/pgls_analyser/src/registry.rs +++ b/crates/pgls_analyser/src/registry.rs @@ -1,6 +1,171 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -use pgls_analyse::RegistryVisitor; +use crate::linter_registry::RegistryLinterRule; +use pgls_analyse::{RegistryVisitor, RuleKey}; pub fn visit_registry(registry: &mut V) { registry.record_category::(); } +#[doc = r" Maps rule keys to factory functions that create rule executors"] +#[doc = r" This function is generated by codegen and includes all linter rules"] +pub fn get_linter_rule_factory(key: &RuleKey) -> Option RegistryLinterRule>> { + match key.rule_name() { + "addSerialColumn" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::add_serial_column::AddSerialColumn, + >() + })), + "addingFieldWithDefault" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::adding_field_with_default::AddingFieldWithDefault, + >() + })), + "addingForeignKeyConstraint" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::adding_foreign_key_constraint::AddingForeignKeyConstraint, + >() + })), + "addingNotNullField" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::adding_not_null_field::AddingNotNullField, + >() + })), + "addingPrimaryKeyConstraint" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::adding_primary_key_constraint::AddingPrimaryKeyConstraint, + >() + })), + "addingRequiredField" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::adding_required_field::AddingRequiredField, + >() + })), + "banCharField" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::ban_char_field::BanCharField, + >() + })), + "banConcurrentIndexCreationInTransaction" => Some(Box::new(|| { + crate :: linter_registry :: RegistryLinterRule :: new :: < crate::lint::safety::ban_concurrent_index_creation_in_transaction :: BanConcurrentIndexCreationInTransaction > () + })), + "banDropColumn" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::ban_drop_column::BanDropColumn, + >() + })), + "banDropDatabase" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::ban_drop_database::BanDropDatabase, + >() + })), + "banDropNotNull" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::ban_drop_not_null::BanDropNotNull, + >() + })), + "banDropTable" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::ban_drop_table::BanDropTable, + >() + })), + "banTruncateCascade" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::ban_truncate_cascade::BanTruncateCascade, + >() + })), + "changingColumnType" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::changing_column_type::ChangingColumnType, + >() + })), + "constraintMissingNotValid" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::constraint_missing_not_valid::ConstraintMissingNotValid, + >() + })), + "creatingEnum" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::creating_enum::CreatingEnum, + >() + })), + "disallowUniqueConstraint" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::disallow_unique_constraint::DisallowUniqueConstraint, + >() + })), + "lockTimeoutWarning" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::lock_timeout_warning::LockTimeoutWarning, + >() + })), + "multipleAlterTable" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::multiple_alter_table::MultipleAlterTable, + >() + })), + "preferBigInt" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::prefer_big_int::PreferBigInt, + >() + })), + "preferBigintOverInt" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::prefer_bigint_over_int::PreferBigintOverInt, + >() + })), + "preferBigintOverSmallint" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::prefer_bigint_over_smallint::PreferBigintOverSmallint, + >() + })), + "preferIdentity" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::prefer_identity::PreferIdentity, + >() + })), + "preferJsonb" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::prefer_jsonb::PreferJsonb, + >() + })), + "preferRobustStmts" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::prefer_robust_stmts::PreferRobustStmts, + >() + })), + "preferTextField" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::prefer_text_field::PreferTextField, + >() + })), + "preferTimestamptz" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::prefer_timestamptz::PreferTimestamptz, + >() + })), + "renamingColumn" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::renaming_column::RenamingColumn, + >() + })), + "renamingTable" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::renaming_table::RenamingTable, + >() + })), + "requireConcurrentIndexCreation" => Some(Box::new(|| { + crate :: linter_registry :: RegistryLinterRule :: new :: < crate::lint::safety::require_concurrent_index_creation :: RequireConcurrentIndexCreation > () + })), + "requireConcurrentIndexDeletion" => Some(Box::new(|| { + crate :: linter_registry :: RegistryLinterRule :: new :: < crate::lint::safety::require_concurrent_index_deletion :: RequireConcurrentIndexDeletion > () + })), + "runningStatementWhileHoldingAccessExclusive" => Some(Box::new(|| { + crate :: linter_registry :: RegistryLinterRule :: new :: < crate::lint::safety::running_statement_while_holding_access_exclusive :: RunningStatementWhileHoldingAccessExclusive > () + })), + "transactionNesting" => Some(Box::new(|| { + crate::linter_registry::RegistryLinterRule::new::< + crate::lint::safety::transaction_nesting::TransactionNesting, + >() + })), + _ => None, + } +} diff --git a/crates/pgls_configuration/src/linter/rules.rs b/crates/pgls_configuration/src/linter/rules.rs index 5e27b92d1..32fc1a2fc 100644 --- a/crates/pgls_configuration/src/linter/rules.rs +++ b/crates/pgls_configuration/src/linter/rules.rs @@ -3,7 +3,8 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rules::{RuleConfiguration, RulePlainConfiguration}; use biome_deserialize_macros::Merge; -use pgls_analyse::{RuleFilter, options::RuleOptions}; +use pgls_analyse::RuleFilter; +use pgls_analyser::RuleOptions; use pgls_diagnostics::{Category, Severity}; use rustc_hash::FxHashSet; #[cfg(feature = "schema")] diff --git a/crates/pgls_configuration/src/rules/configuration.rs b/crates/pgls_configuration/src/rules/configuration.rs index a3e43f63c..ad93abeeb 100644 --- a/crates/pgls_configuration/src/rules/configuration.rs +++ b/crates/pgls_configuration/src/rules/configuration.rs @@ -1,6 +1,7 @@ use biome_deserialize::Merge; use biome_deserialize_macros::Deserializable; -use pgls_analyse::options::RuleOptions; +use pgls_analyse::RuleFilter; +use pgls_analyser::RuleOptions; use pgls_diagnostics::Severity; #[cfg(feature = "schema")] use schemars::JsonSchema; diff --git a/crates/pgls_splinter/src/convert.rs b/crates/pgls_splinter/src/convert.rs index 926de1d40..c5d0c7904 100644 --- a/crates/pgls_splinter/src/convert.rs +++ b/crates/pgls_splinter/src/convert.rs @@ -1,4 +1,4 @@ -use pgls_diagnostics::{Category, Severity, category}; +use pgls_diagnostics::{Category, DatabaseObjectOwned, Severity, category}; use serde_json::Value; use crate::{SplinterAdvices, SplinterDiagnostic, SplinterQueryResult}; @@ -22,6 +22,11 @@ impl From for SplinterDiagnostic { category: rule_name_to_category(&result.name, &group), message: result.detail.into(), severity, + db_object: object_name.as_ref().map(|name| DatabaseObjectOwned { + schema: schema.clone(), + name: name.clone(), + object_type: object_type.clone(), + }), advices: SplinterAdvices { description: result.description, schema, diff --git a/crates/pgls_splinter/src/diagnostics.rs b/crates/pgls_splinter/src/diagnostics.rs index 7ff6ca942..ac9e32d77 100644 --- a/crates/pgls_splinter/src/diagnostics.rs +++ b/crates/pgls_splinter/src/diagnostics.rs @@ -1,5 +1,6 @@ use pgls_diagnostics::{ - Advices, Category, Diagnostic, LogCategory, MessageAndDescription, Severity, Visit, + Advices, Category, DatabaseObjectOwned, Diagnostic, LogCategory, MessageAndDescription, + Severity, Visit, }; use serde_json::Value; use std::io; @@ -10,10 +11,9 @@ pub struct SplinterDiagnostic { #[category] pub category: &'static Category, - // TODO: add new location type for database objects - // This will map schema + object_name to source code location - // #[location(span)] - // pub span: Option, + #[location(database_object)] + pub db_object: Option, + #[message] #[description] pub message: MessageAndDescription, diff --git a/crates/pgls_workspace/src/configuration.rs b/crates/pgls_workspace/src/configuration.rs index 0804a1c84..ce5ce6e36 100644 --- a/crates/pgls_workspace/src/configuration.rs +++ b/crates/pgls_workspace/src/configuration.rs @@ -6,7 +6,7 @@ use std::{ }; use biome_deserialize::Merge; -use pgls_analyse::AnalyserRules; +use pgls_analyser::LinterRules; use pgls_configuration::{ ConfigurationDiagnostic, ConfigurationPathHint, ConfigurationPayload, PartialConfiguration, VERSION, diagnostics::CantLoadExtendFile, push_to_analyser_rules, @@ -216,8 +216,8 @@ pub fn create_config( } /// Returns the rules applied to a specific [Path], given the [Settings] -pub fn to_analyser_rules(settings: &Settings) -> AnalyserRules { - let mut analyser_rules = AnalyserRules::default(); +pub fn to_analyser_rules(settings: &Settings) -> LinterRules { + let mut analyser_rules = LinterRules::default(); if let Some(rules) = settings.linter.rules.as_ref() { push_to_analyser_rules(rules, pgls_analyser::METADATA.deref(), &mut analyser_rules); } diff --git a/crates/pgls_workspace/src/workspace/server.rs b/crates/pgls_workspace/src/workspace/server.rs index 2ff11ec5f..e5feed085 100644 --- a/crates/pgls_workspace/src/workspace/server.rs +++ b/crates/pgls_workspace/src/workspace/server.rs @@ -15,8 +15,9 @@ use document::{ }; use futures::{StreamExt, stream}; use pg_query::convert_to_positional_params; -use pgls_analyse::{AnalyserOptions, AnalysisFilter}; -use pgls_analyser::{Analyser, AnalyserConfig, AnalyserParams}; +use pgls_analyse::AnalysisFilter; +use pgls_analyser::{Analyser, AnalyserConfig, AnalyserParams, LinterOptions}; + use pgls_diagnostics::{ Diagnostic, DiagnosticExt, Error, Severity, serde::Diagnostic as SDiagnostic, }; @@ -601,7 +602,7 @@ impl Workspace for WorkspaceServer { .with_linter_rules(¶ms.only, ¶ms.skip) .finish(); - let options = AnalyserOptions { + let options = LinterOptions { rules: to_analyser_rules(settings), }; diff --git a/crates/pgls_workspace/src/workspace/server/analyser.rs b/crates/pgls_workspace/src/workspace/server/analyser.rs index b7382f2bb..d8de3bbb3 100644 --- a/crates/pgls_workspace/src/workspace/server/analyser.rs +++ b/crates/pgls_workspace/src/workspace/server/analyser.rs @@ -1,4 +1,4 @@ -use pgls_analyse::{GroupCategory, RegistryVisitor, Rule, RuleCategory, RuleFilter, RuleGroup}; +use pgls_analyse::{GroupCategory, RegistryVisitor, RuleCategory, RuleFilter, RuleGroup, RuleMeta}; use pgls_configuration::RuleSelector; use rustc_hash::FxHashSet; @@ -91,7 +91,7 @@ impl<'a, 'b> LintVisitor<'a, 'b> { fn push_rule(&mut self) where - R: Rule + 'static, + R: RuleMeta + 'static, { // Do not report unused suppression comment diagnostics if a single rule is run. for selector in self.only { @@ -132,7 +132,7 @@ impl RegistryVisitor for LintVisitor<'_, '_> { fn record_rule(&mut self) where - R: Rule + 'static, + R: RuleMeta + 'static, { self.push_rule::() } diff --git a/docs/codegen/src/rules_docs.rs b/docs/codegen/src/rules_docs.rs index eae0340e5..f68950315 100644 --- a/docs/codegen/src/rules_docs.rs +++ b/docs/codegen/src/rules_docs.rs @@ -1,7 +1,7 @@ use anyhow::{Result, bail}; use biome_string_case::Case; -use pgls_analyse::{AnalyserOptions, AnalysisFilter, RuleFilter, RuleMetadata}; -use pgls_analyser::{AnalysableStatement, Analyser, AnalyserConfig}; +use pgls_analyse::{AnalysisFilter, RuleFilter, RuleMetadata}; +use pgls_analyser::{AnalysableStatement, Analyser, AnalyserConfig, LinterOptions}; use pgls_console::StdDisplay; use pgls_diagnostics::{Diagnostic, DiagnosticExt, PrintDiagnostic}; use pgls_query_ext::diagnostics::SyntaxDiagnostic; @@ -431,7 +431,7 @@ fn print_diagnostics( ..AnalysisFilter::default() }; let settings = Settings::default(); - let options = AnalyserOptions::default(); + let options = LinterOptions::default(); let analyser = Analyser::new(AnalyserConfig { options: &options, filter, diff --git a/docs/codegen/src/utils.rs b/docs/codegen/src/utils.rs index 692ef9308..8d53cf909 100644 --- a/docs/codegen/src/utils.rs +++ b/docs/codegen/src/utils.rs @@ -1,4 +1,6 @@ -use pgls_analyse::{GroupCategory, RegistryVisitor, Rule, RuleCategory, RuleGroup, RuleMetadata}; +use pgls_analyse::{ + GroupCategory, RegistryVisitor, RuleCategory, RuleGroup, RuleMeta, RuleMetadata, +}; use regex::Regex; use std::collections::BTreeMap; @@ -32,7 +34,7 @@ pub(crate) struct LintRulesVisitor { impl LintRulesVisitor { fn push_rule(&mut self) where - R: Rule + 'static, + R: RuleMeta + 'static, { let group = self .groups @@ -52,7 +54,7 @@ impl RegistryVisitor for LintRulesVisitor { fn record_rule(&mut self) where - R: Rule + 'static, + R: RuleMeta + 'static, { self.push_rule::() } diff --git a/xtask/codegen/src/generate_analyser.rs b/xtask/codegen/src/generate_analyser.rs index 105ca7a82..fc1257d5c 100644 --- a/xtask/codegen/src/generate_analyser.rs +++ b/xtask/codegen/src/generate_analyser.rs @@ -15,11 +15,12 @@ pub fn generate_analyser() -> Result<()> { fn generate_linter() -> Result<()> { let base_path = project_root().join("crates/pgls_analyser/src"); let mut analysers = BTreeMap::new(); - generate_category("lint", &mut analysers, &base_path)?; + let mut all_rules = BTreeMap::new(); + generate_category("lint", &mut analysers, &mut all_rules, &base_path)?; generate_options(&base_path)?; - update_linter_registry_builder(analysers) + update_linter_registry_builder(analysers, all_rules) } fn generate_options(base_path: &Path) -> Result<()> { @@ -39,7 +40,7 @@ fn generate_options(base_path: &Path) -> Result<()> { let rule_module_name = format_ident!("{}", rule_filename); let rule_name = format_ident!("{}", rule_name); rules_options.insert(rule_filename.to_string(), quote! { - pub type #rule_name = <#category_name::#group_name::#rule_module_name::#rule_name as pgls_analyse::Rule>::Options; + pub type #rule_name = <#category_name::#group_name::#rule_module_name::#rule_name as crate::LinterRule>::Options; }); } } @@ -63,6 +64,7 @@ fn generate_options(base_path: &Path) -> Result<()> { fn generate_category( name: &'static str, entries: &mut BTreeMap<&'static str, TokenStream>, + all_rules: &mut BTreeMap, base_path: &Path, ) -> Result<()> { let path = base_path.join(name); @@ -81,7 +83,7 @@ fn generate_category( .to_str() .context("could not convert file name to string")?; - generate_group(name, file_name, base_path)?; + generate_group(name, file_name, all_rules, base_path)?; let module_name = format_ident!("{}", file_name); let group_name = format_ident!("{}", Case::Pascal.convert(file_name)); @@ -135,7 +137,12 @@ fn generate_category( Ok(()) } -fn generate_group(category: &'static str, group: &str, base_path: &Path) -> Result<()> { +fn generate_group( + category: &'static str, + group: &str, + all_rules: &mut BTreeMap, + base_path: &Path, +) -> Result<()> { let path = base_path.join(category).join(group); let mut rules = BTreeMap::new(); @@ -151,7 +158,21 @@ fn generate_group(category: &'static str, group: &str, base_path: &Path) -> Resu let key = rule_type.clone(); let module_name = format_ident!("{}", file_name); - let rule_type = format_ident!("{}", rule_type); + let rule_type_ident = format_ident!("{}", rule_type); + + // Collect for factory generation + // Key is the rule name (camelCase for config), value is (full_path_tokens, metadata_name) + let category_ident = format_ident!("{}", category); + let group_module_ident = format_ident!("{}", group); // Module name (lowercase) + all_rules.insert( + Case::Camel.convert(&key), + ( + quote! { + crate::#category_ident::#group_module_ident::#module_name::#rule_type_ident + }, + file_name.to_string(), + ), + ); rules.insert( key, @@ -160,7 +181,7 @@ fn generate_group(category: &'static str, group: &str, base_path: &Path) -> Resu pub mod #module_name; }, quote! { - self::#module_name::#rule_type + self::#module_name::#rule_type_ident }, ), ); @@ -199,17 +220,39 @@ fn generate_group(category: &'static str, group: &str, base_path: &Path) -> Resu Ok(()) } -fn update_linter_registry_builder(rules: BTreeMap<&'static str, TokenStream>) -> Result<()> { +fn update_linter_registry_builder( + rules: BTreeMap<&'static str, TokenStream>, + all_rules: BTreeMap, +) -> Result<()> { let path = project_root().join("crates/pgls_analyser/src/registry.rs"); let categories = rules.into_values(); + // Generate factory match arms for all rules + let factory_arms = all_rules.iter().map(|(rule_name, (rule_path, _))| { + quote! { + #rule_name => Some(Box::new(|| crate::linter_registry::RegistryLinterRule::new::<#rule_path>())) + } + }); + let tokens = xtask::reformat(quote! { - use pgls_analyse::RegistryVisitor; + use pgls_analyse::{RegistryVisitor, RuleKey}; + use crate::linter_registry::RegistryLinterRule; pub fn visit_registry(registry: &mut V) { #( #categories )* } + + /// Maps rule keys to factory functions that create rule executors + /// This function is generated by codegen and includes all linter rules + pub fn get_linter_rule_factory( + key: &RuleKey, + ) -> Option RegistryLinterRule>> { + match key.rule_name() { + #( #factory_arms, )* + _ => None, + } + } })?; fs2::write(path, tokens)?; diff --git a/xtask/codegen/src/generate_configuration.rs b/xtask/codegen/src/generate_configuration.rs index f4a922129..2a9913bf1 100644 --- a/xtask/codegen/src/generate_configuration.rs +++ b/xtask/codegen/src/generate_configuration.rs @@ -1,6 +1,8 @@ use crate::{to_capitalized, update}; use biome_string_case::Case; -use pgls_analyse::{GroupCategory, RegistryVisitor, Rule, RuleCategory, RuleGroup, RuleMetadata}; +use pgls_analyse::{ + GroupCategory, RegistryVisitor, RuleCategory, RuleGroup, RuleMeta, RuleMetadata, +}; use pgls_diagnostics::Severity; use proc_macro2::{Ident, Literal, Span, TokenStream}; use pulldown_cmark::{Event, Parser, Tag, TagEnd}; @@ -92,7 +94,7 @@ impl RegistryVisitor for CategoryRulesVisitor { fn record_rule(&mut self) where - R: Rule + 'static, + R: RuleMeta + 'static, { self.groups .entry(::NAME) diff --git a/xtask/rules_check/src/lib.rs b/xtask/rules_check/src/lib.rs index 3276e1a00..508d8c831 100644 --- a/xtask/rules_check/src/lib.rs +++ b/xtask/rules_check/src/lib.rs @@ -4,10 +4,10 @@ use std::{fmt::Write, slice}; use anyhow::bail; use pgls_analyse::{ - AnalyserOptions, AnalysisFilter, GroupCategory, RegistryVisitor, Rule, RuleCategory, - RuleFilter, RuleGroup, RuleMetadata, + AnalysisFilter, GroupCategory, RegistryVisitor, RuleCategory, RuleFilter, RuleGroup, RuleMeta, + RuleMetadata, }; -use pgls_analyser::{AnalysableStatement, Analyser, AnalyserConfig}; +use pgls_analyser::{AnalysableStatement, Analyser, AnalyserConfig, LinterOptions}; use pgls_console::{markup, Console}; use pgls_diagnostics::{Diagnostic, DiagnosticExt, PrintDiagnostic}; use pgls_query_ext::diagnostics::SyntaxDiagnostic; @@ -23,7 +23,7 @@ pub fn check_rules() -> anyhow::Result<()> { impl LintRulesVisitor { fn push_rule(&mut self) where - R: Rule + 'static, + R: RuleMeta + 'static, { self.groups .entry(::NAME) @@ -41,7 +41,7 @@ pub fn check_rules() -> anyhow::Result<()> { fn record_rule(&mut self) where - R: Rule + 'static, + R: RuleMeta + 'static, { self.push_rule::() } @@ -120,7 +120,7 @@ fn assert_lint( ..AnalysisFilter::default() }; let settings = Settings::default(); - let options = AnalyserOptions::default(); + let options = LinterOptions::default(); let analyser = Analyser::new(AnalyserConfig { options: &options, filter, From dc098c0a2c2307190b351567c0688541dfac14a4 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Sun, 14 Dec 2025 18:40:52 +0100 Subject: [PATCH 02/14] fix: dont use dyn --- crates/pgls_analyser/src/linter_registry.rs | 4 +- crates/pgls_analyser/src/options.rs | 18 +-- crates/pgls_analyser/src/registry.rs | 165 +------------------- xtask/codegen/src/generate_analyser.rs | 14 +- 4 files changed, 21 insertions(+), 180 deletions(-) diff --git a/crates/pgls_analyser/src/linter_registry.rs b/crates/pgls_analyser/src/linter_registry.rs index 5840d069f..3d64db4b8 100644 --- a/crates/pgls_analyser/src/linter_registry.rs +++ b/crates/pgls_analyser/src/linter_registry.rs @@ -103,13 +103,13 @@ impl RegistryLinterRule { impl LinterRuleRegistryBuilder<'_> { pub fn build(self) -> LinterRuleRegistry { - // Look up factory for each collected rule key and create executors + // Look up executor for each collected rule key let rules = self .rule_keys .into_iter() .filter_map(|key| { // This function will be generated by codegen - crate::registry::get_linter_rule_factory(&key).map(|factory| factory()) + crate::registry::get_linter_rule_executor(&key) }) .collect(); diff --git a/crates/pgls_analyser/src/options.rs b/crates/pgls_analyser/src/options.rs index b04539c45..c7625a5a3 100644 --- a/crates/pgls_analyser/src/options.rs +++ b/crates/pgls_analyser/src/options.rs @@ -5,14 +5,14 @@ pub type AddSerialColumn = ::Options; pub type AddingFieldWithDefault = ::Options; -pub type AddingForeignKeyConstraint = < lint :: safety :: adding_foreign_key_constraint :: AddingForeignKeyConstraint as crate::LinterRule > :: Options ; +pub type AddingForeignKeyConstraint = < lint :: safety :: adding_foreign_key_constraint :: AddingForeignKeyConstraint as crate :: LinterRule > :: Options ; pub type AddingNotNullField = ::Options; -pub type AddingPrimaryKeyConstraint = < lint :: safety :: adding_primary_key_constraint :: AddingPrimaryKeyConstraint as crate::LinterRule > :: Options ; +pub type AddingPrimaryKeyConstraint = < lint :: safety :: adding_primary_key_constraint :: AddingPrimaryKeyConstraint as crate :: LinterRule > :: Options ; pub type AddingRequiredField = ::Options; pub type BanCharField = ::Options; -pub type BanConcurrentIndexCreationInTransaction = < lint :: safety :: ban_concurrent_index_creation_in_transaction :: BanConcurrentIndexCreationInTransaction as crate::LinterRule > :: Options ; +pub type BanConcurrentIndexCreationInTransaction = < lint :: safety :: ban_concurrent_index_creation_in_transaction :: BanConcurrentIndexCreationInTransaction as crate :: LinterRule > :: Options ; pub type BanDropColumn = ::Options; pub type BanDropDatabase = @@ -24,9 +24,9 @@ pub type BanTruncateCascade = ::Options; pub type ChangingColumnType = ::Options; -pub type ConstraintMissingNotValid = < lint :: safety :: constraint_missing_not_valid :: ConstraintMissingNotValid as crate::LinterRule > :: Options ; +pub type ConstraintMissingNotValid = < lint :: safety :: constraint_missing_not_valid :: ConstraintMissingNotValid as crate :: LinterRule > :: Options ; pub type CreatingEnum = ::Options; -pub type DisallowUniqueConstraint = < lint :: safety :: disallow_unique_constraint :: DisallowUniqueConstraint as crate::LinterRule > :: Options ; +pub type DisallowUniqueConstraint = < lint :: safety :: disallow_unique_constraint :: DisallowUniqueConstraint as crate :: LinterRule > :: Options ; pub type LockTimeoutWarning = ::Options; pub type MultipleAlterTable = @@ -34,7 +34,7 @@ pub type MultipleAlterTable = pub type PreferBigInt = ::Options; pub type PreferBigintOverInt = ::Options; -pub type PreferBigintOverSmallint = < lint :: safety :: prefer_bigint_over_smallint :: PreferBigintOverSmallint as crate::LinterRule > :: Options ; +pub type PreferBigintOverSmallint = < lint :: safety :: prefer_bigint_over_smallint :: PreferBigintOverSmallint as crate :: LinterRule > :: Options ; pub type PreferIdentity = ::Options; pub type PreferJsonb = ::Options; @@ -48,8 +48,8 @@ pub type RenamingColumn = ::Options; pub type RenamingTable = ::Options; -pub type RequireConcurrentIndexCreation = < lint :: safety :: require_concurrent_index_creation :: RequireConcurrentIndexCreation as crate::LinterRule > :: Options ; -pub type RequireConcurrentIndexDeletion = < lint :: safety :: require_concurrent_index_deletion :: RequireConcurrentIndexDeletion as crate::LinterRule > :: Options ; -pub type RunningStatementWhileHoldingAccessExclusive = < lint :: safety :: running_statement_while_holding_access_exclusive :: RunningStatementWhileHoldingAccessExclusive as crate::LinterRule > :: Options ; +pub type RequireConcurrentIndexCreation = < lint :: safety :: require_concurrent_index_creation :: RequireConcurrentIndexCreation as crate :: LinterRule > :: Options ; +pub type RequireConcurrentIndexDeletion = < lint :: safety :: require_concurrent_index_deletion :: RequireConcurrentIndexDeletion as crate :: LinterRule > :: Options ; +pub type RunningStatementWhileHoldingAccessExclusive = < lint :: safety :: running_statement_while_holding_access_exclusive :: RunningStatementWhileHoldingAccessExclusive as crate :: LinterRule > :: Options ; pub type TransactionNesting = ::Options; diff --git a/crates/pgls_analyser/src/registry.rs b/crates/pgls_analyser/src/registry.rs index 29862d096..35dc5e914 100644 --- a/crates/pgls_analyser/src/registry.rs +++ b/crates/pgls_analyser/src/registry.rs @@ -5,167 +5,8 @@ use pgls_analyse::{RegistryVisitor, RuleKey}; pub fn visit_registry(registry: &mut V) { registry.record_category::(); } -#[doc = r" Maps rule keys to factory functions that create rule executors"] +#[doc = r" Maps rule keys to rule executors"] #[doc = r" This function is generated by codegen and includes all linter rules"] -pub fn get_linter_rule_factory(key: &RuleKey) -> Option RegistryLinterRule>> { - match key.rule_name() { - "addSerialColumn" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::add_serial_column::AddSerialColumn, - >() - })), - "addingFieldWithDefault" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::adding_field_with_default::AddingFieldWithDefault, - >() - })), - "addingForeignKeyConstraint" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::adding_foreign_key_constraint::AddingForeignKeyConstraint, - >() - })), - "addingNotNullField" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::adding_not_null_field::AddingNotNullField, - >() - })), - "addingPrimaryKeyConstraint" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::adding_primary_key_constraint::AddingPrimaryKeyConstraint, - >() - })), - "addingRequiredField" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::adding_required_field::AddingRequiredField, - >() - })), - "banCharField" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::ban_char_field::BanCharField, - >() - })), - "banConcurrentIndexCreationInTransaction" => Some(Box::new(|| { - crate :: linter_registry :: RegistryLinterRule :: new :: < crate::lint::safety::ban_concurrent_index_creation_in_transaction :: BanConcurrentIndexCreationInTransaction > () - })), - "banDropColumn" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::ban_drop_column::BanDropColumn, - >() - })), - "banDropDatabase" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::ban_drop_database::BanDropDatabase, - >() - })), - "banDropNotNull" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::ban_drop_not_null::BanDropNotNull, - >() - })), - "banDropTable" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::ban_drop_table::BanDropTable, - >() - })), - "banTruncateCascade" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::ban_truncate_cascade::BanTruncateCascade, - >() - })), - "changingColumnType" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::changing_column_type::ChangingColumnType, - >() - })), - "constraintMissingNotValid" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::constraint_missing_not_valid::ConstraintMissingNotValid, - >() - })), - "creatingEnum" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::creating_enum::CreatingEnum, - >() - })), - "disallowUniqueConstraint" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::disallow_unique_constraint::DisallowUniqueConstraint, - >() - })), - "lockTimeoutWarning" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::lock_timeout_warning::LockTimeoutWarning, - >() - })), - "multipleAlterTable" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::multiple_alter_table::MultipleAlterTable, - >() - })), - "preferBigInt" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::prefer_big_int::PreferBigInt, - >() - })), - "preferBigintOverInt" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::prefer_bigint_over_int::PreferBigintOverInt, - >() - })), - "preferBigintOverSmallint" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::prefer_bigint_over_smallint::PreferBigintOverSmallint, - >() - })), - "preferIdentity" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::prefer_identity::PreferIdentity, - >() - })), - "preferJsonb" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::prefer_jsonb::PreferJsonb, - >() - })), - "preferRobustStmts" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::prefer_robust_stmts::PreferRobustStmts, - >() - })), - "preferTextField" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::prefer_text_field::PreferTextField, - >() - })), - "preferTimestamptz" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::prefer_timestamptz::PreferTimestamptz, - >() - })), - "renamingColumn" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::renaming_column::RenamingColumn, - >() - })), - "renamingTable" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::renaming_table::RenamingTable, - >() - })), - "requireConcurrentIndexCreation" => Some(Box::new(|| { - crate :: linter_registry :: RegistryLinterRule :: new :: < crate::lint::safety::require_concurrent_index_creation :: RequireConcurrentIndexCreation > () - })), - "requireConcurrentIndexDeletion" => Some(Box::new(|| { - crate :: linter_registry :: RegistryLinterRule :: new :: < crate::lint::safety::require_concurrent_index_deletion :: RequireConcurrentIndexDeletion > () - })), - "runningStatementWhileHoldingAccessExclusive" => Some(Box::new(|| { - crate :: linter_registry :: RegistryLinterRule :: new :: < crate::lint::safety::running_statement_while_holding_access_exclusive :: RunningStatementWhileHoldingAccessExclusive > () - })), - "transactionNesting" => Some(Box::new(|| { - crate::linter_registry::RegistryLinterRule::new::< - crate::lint::safety::transaction_nesting::TransactionNesting, - >() - })), - _ => None, - } +pub fn get_linter_rule_executor(key: &RuleKey) -> Option { + match key . rule_name () { "addSerialColumn" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: add_serial_column :: AddSerialColumn > ()) , "addingFieldWithDefault" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: adding_field_with_default :: AddingFieldWithDefault > ()) , "addingForeignKeyConstraint" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: adding_foreign_key_constraint :: AddingForeignKeyConstraint > ()) , "addingNotNullField" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: adding_not_null_field :: AddingNotNullField > ()) , "addingPrimaryKeyConstraint" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: adding_primary_key_constraint :: AddingPrimaryKeyConstraint > ()) , "addingRequiredField" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: adding_required_field :: AddingRequiredField > ()) , "banCharField" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: ban_char_field :: BanCharField > ()) , "banConcurrentIndexCreationInTransaction" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: ban_concurrent_index_creation_in_transaction :: BanConcurrentIndexCreationInTransaction > ()) , "banDropColumn" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: ban_drop_column :: BanDropColumn > ()) , "banDropDatabase" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: ban_drop_database :: BanDropDatabase > ()) , "banDropNotNull" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: ban_drop_not_null :: BanDropNotNull > ()) , "banDropTable" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: ban_drop_table :: BanDropTable > ()) , "banTruncateCascade" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: ban_truncate_cascade :: BanTruncateCascade > ()) , "changingColumnType" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: changing_column_type :: ChangingColumnType > ()) , "constraintMissingNotValid" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: constraint_missing_not_valid :: ConstraintMissingNotValid > ()) , "creatingEnum" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: creating_enum :: CreatingEnum > ()) , "disallowUniqueConstraint" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: disallow_unique_constraint :: DisallowUniqueConstraint > ()) , "lockTimeoutWarning" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: lock_timeout_warning :: LockTimeoutWarning > ()) , "multipleAlterTable" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: multiple_alter_table :: MultipleAlterTable > ()) , "preferBigInt" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_big_int :: PreferBigInt > ()) , "preferBigintOverInt" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_bigint_over_int :: PreferBigintOverInt > ()) , "preferBigintOverSmallint" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_bigint_over_smallint :: PreferBigintOverSmallint > ()) , "preferIdentity" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_identity :: PreferIdentity > ()) , "preferJsonb" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_jsonb :: PreferJsonb > ()) , "preferRobustStmts" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_robust_stmts :: PreferRobustStmts > ()) , "preferTextField" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_text_field :: PreferTextField > ()) , "preferTimestamptz" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_timestamptz :: PreferTimestamptz > ()) , "renamingColumn" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: renaming_column :: RenamingColumn > ()) , "renamingTable" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: renaming_table :: RenamingTable > ()) , "requireConcurrentIndexCreation" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: require_concurrent_index_creation :: RequireConcurrentIndexCreation > ()) , "requireConcurrentIndexDeletion" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: require_concurrent_index_deletion :: RequireConcurrentIndexDeletion > ()) , "runningStatementWhileHoldingAccessExclusive" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: running_statement_while_holding_access_exclusive :: RunningStatementWhileHoldingAccessExclusive > ()) , "transactionNesting" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: transaction_nesting :: TransactionNesting > ()) , _ => None , } } diff --git a/xtask/codegen/src/generate_analyser.rs b/xtask/codegen/src/generate_analyser.rs index fc1257d5c..8698795fb 100644 --- a/xtask/codegen/src/generate_analyser.rs +++ b/xtask/codegen/src/generate_analyser.rs @@ -228,10 +228,10 @@ fn update_linter_registry_builder( let categories = rules.into_values(); - // Generate factory match arms for all rules - let factory_arms = all_rules.iter().map(|(rule_name, (rule_path, _))| { + // Generate match arms that directly create executors (no closure/Box overhead) + let executor_arms = all_rules.iter().map(|(rule_name, (rule_path, _))| { quote! { - #rule_name => Some(Box::new(|| crate::linter_registry::RegistryLinterRule::new::<#rule_path>())) + #rule_name => Some(crate::linter_registry::RegistryLinterRule::new::<#rule_path>()) } }); @@ -243,13 +243,13 @@ fn update_linter_registry_builder( #( #categories )* } - /// Maps rule keys to factory functions that create rule executors + /// Maps rule keys to rule executors (zero-cost abstraction) /// This function is generated by codegen and includes all linter rules - pub fn get_linter_rule_factory( + pub fn get_linter_rule_executor( key: &RuleKey, - ) -> Option RegistryLinterRule>> { + ) -> Option { match key.rule_name() { - #( #factory_arms, )* + #( #executor_arms, )* _ => None, } } From 6fae04119cd8d705cc4e945ef3cc1fb8d0e4323a Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 15 Dec 2025 12:06:49 +0100 Subject: [PATCH 03/14] fix: correct AnalyserRules reference in codegen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed pgls_analyse::AnalyserRules to pgls_analyser::LinterRules and fixed import statements to use the correct crate for RuleOptions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- crates/pgls_analyser/src/registry.rs | 2 +- crates/pgls_configuration/src/linter/rules.rs | 2 +- crates/pgls_configuration/src/rules/configuration.rs | 1 - xtask/codegen/src/generate_configuration.rs | 8 +++++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/pgls_analyser/src/registry.rs b/crates/pgls_analyser/src/registry.rs index 35dc5e914..7e3872905 100644 --- a/crates/pgls_analyser/src/registry.rs +++ b/crates/pgls_analyser/src/registry.rs @@ -5,7 +5,7 @@ use pgls_analyse::{RegistryVisitor, RuleKey}; pub fn visit_registry(registry: &mut V) { registry.record_category::(); } -#[doc = r" Maps rule keys to rule executors"] +#[doc = r" Maps rule keys to rule executors (zero-cost abstraction)"] #[doc = r" This function is generated by codegen and includes all linter rules"] pub fn get_linter_rule_executor(key: &RuleKey) -> Option { match key . rule_name () { "addSerialColumn" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: add_serial_column :: AddSerialColumn > ()) , "addingFieldWithDefault" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: adding_field_with_default :: AddingFieldWithDefault > ()) , "addingForeignKeyConstraint" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: adding_foreign_key_constraint :: AddingForeignKeyConstraint > ()) , "addingNotNullField" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: adding_not_null_field :: AddingNotNullField > ()) , "addingPrimaryKeyConstraint" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: adding_primary_key_constraint :: AddingPrimaryKeyConstraint > ()) , "addingRequiredField" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: adding_required_field :: AddingRequiredField > ()) , "banCharField" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: ban_char_field :: BanCharField > ()) , "banConcurrentIndexCreationInTransaction" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: ban_concurrent_index_creation_in_transaction :: BanConcurrentIndexCreationInTransaction > ()) , "banDropColumn" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: ban_drop_column :: BanDropColumn > ()) , "banDropDatabase" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: ban_drop_database :: BanDropDatabase > ()) , "banDropNotNull" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: ban_drop_not_null :: BanDropNotNull > ()) , "banDropTable" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: ban_drop_table :: BanDropTable > ()) , "banTruncateCascade" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: ban_truncate_cascade :: BanTruncateCascade > ()) , "changingColumnType" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: changing_column_type :: ChangingColumnType > ()) , "constraintMissingNotValid" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: constraint_missing_not_valid :: ConstraintMissingNotValid > ()) , "creatingEnum" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: creating_enum :: CreatingEnum > ()) , "disallowUniqueConstraint" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: disallow_unique_constraint :: DisallowUniqueConstraint > ()) , "lockTimeoutWarning" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: lock_timeout_warning :: LockTimeoutWarning > ()) , "multipleAlterTable" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: multiple_alter_table :: MultipleAlterTable > ()) , "preferBigInt" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_big_int :: PreferBigInt > ()) , "preferBigintOverInt" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_bigint_over_int :: PreferBigintOverInt > ()) , "preferBigintOverSmallint" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_bigint_over_smallint :: PreferBigintOverSmallint > ()) , "preferIdentity" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_identity :: PreferIdentity > ()) , "preferJsonb" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_jsonb :: PreferJsonb > ()) , "preferRobustStmts" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_robust_stmts :: PreferRobustStmts > ()) , "preferTextField" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_text_field :: PreferTextField > ()) , "preferTimestamptz" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: prefer_timestamptz :: PreferTimestamptz > ()) , "renamingColumn" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: renaming_column :: RenamingColumn > ()) , "renamingTable" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: renaming_table :: RenamingTable > ()) , "requireConcurrentIndexCreation" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: require_concurrent_index_creation :: RequireConcurrentIndexCreation > ()) , "requireConcurrentIndexDeletion" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: require_concurrent_index_deletion :: RequireConcurrentIndexDeletion > ()) , "runningStatementWhileHoldingAccessExclusive" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: running_statement_while_holding_access_exclusive :: RunningStatementWhileHoldingAccessExclusive > ()) , "transactionNesting" => Some (crate :: linter_registry :: RegistryLinterRule :: new :: < crate :: lint :: safety :: transaction_nesting :: TransactionNesting > ()) , _ => None , } diff --git a/crates/pgls_configuration/src/linter/rules.rs b/crates/pgls_configuration/src/linter/rules.rs index 32fc1a2fc..4bc3d4c98 100644 --- a/crates/pgls_configuration/src/linter/rules.rs +++ b/crates/pgls_configuration/src/linter/rules.rs @@ -908,7 +908,7 @@ impl Safety { pub fn push_to_analyser_rules( rules: &Rules, metadata: &pgls_analyse::MetadataRegistry, - analyser_rules: &mut pgls_analyse::AnalyserRules, + analyser_rules: &mut pgls_analyser::LinterRules, ) { if let Some(rules) = rules.safety.as_ref() { for rule_name in Safety::GROUP_RULES { diff --git a/crates/pgls_configuration/src/rules/configuration.rs b/crates/pgls_configuration/src/rules/configuration.rs index ad93abeeb..6b86fef69 100644 --- a/crates/pgls_configuration/src/rules/configuration.rs +++ b/crates/pgls_configuration/src/rules/configuration.rs @@ -1,6 +1,5 @@ use biome_deserialize::Merge; use biome_deserialize_macros::Deserializable; -use pgls_analyse::RuleFilter; use pgls_analyser::RuleOptions; use pgls_diagnostics::Severity; #[cfg(feature = "schema")] diff --git a/xtask/codegen/src/generate_configuration.rs b/xtask/codegen/src/generate_configuration.rs index 2a9913bf1..43224d48e 100644 --- a/xtask/codegen/src/generate_configuration.rs +++ b/xtask/codegen/src/generate_configuration.rs @@ -287,7 +287,8 @@ fn generate_lint_rules_file( use crate::rules::{RuleConfiguration, RulePlainConfiguration}; use biome_deserialize_macros::Merge; - use pgls_analyse::{RuleFilter, options::RuleOptions}; + use pgls_analyse::RuleFilter; + use pgls_analyser::RuleOptions; use pgls_diagnostics::{Category, Severity}; use rustc_hash::FxHashSet; #[cfg(feature = "schema")] @@ -426,7 +427,7 @@ fn generate_lint_rules_file( pub fn push_to_analyser_rules( rules: &Rules, metadata: &pgls_analyse::MetadataRegistry, - analyser_rules: &mut pgls_analyse::AnalyserRules, + analyser_rules: &mut pgls_analyser::LinterRules, ) { #( if let Some(rules) = rules.#group_idents.as_ref() { @@ -789,7 +790,8 @@ fn generate_action_actions_file( use crate::rules::{RuleAssistConfiguration, RuleAssistPlainConfiguration}; use biome_deserialize_macros::{Deserializable, Merge}; - use pgls_analyse::{RuleFilter, options::RuleOptions}; + use pgls_analyse::RuleFilter; + use pgls_analyser::RuleOptions; use pgls_diagnostics::{Category, Severity}; use rustc_hash::FxHashSet; #[cfg(feature = "schema")] From 4e9757f31a97d149dd0d3db0597ef69fc26ab417 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 15 Dec 2025 12:32:53 +0100 Subject: [PATCH 04/14] fix: update test imports to use LinterOptions and LinterDiagnostic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated test files to use the new type names after the refactor: - Changed AnalyserOptions to LinterOptions - Changed RuleDiagnostic to LinterDiagnostic - Fixed import paths to import from pgls_analyser instead of pgls_analyse 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- crates/pgls_analyser/src/lib.rs | 3 ++- crates/pgls_analyser/tests/rules_tests.rs | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/pgls_analyser/src/lib.rs b/crates/pgls_analyser/src/lib.rs index 038a53239..577a2a886 100644 --- a/crates/pgls_analyser/src/lib.rs +++ b/crates/pgls_analyser/src/lib.rs @@ -116,7 +116,8 @@ impl<'a> Analyser<'a> { mod tests { use core::slice; - use pgls_analyse::{AnalyserOptions, AnalysisFilter, RuleFilter}; + use pgls_analyse::{AnalysisFilter, RuleFilter}; + use crate::LinterOptions; use pgls_console::{ Markup, fmt::{Formatter, Termcolor}, diff --git a/crates/pgls_analyser/tests/rules_tests.rs b/crates/pgls_analyser/tests/rules_tests.rs index 2b023d472..4749b3401 100644 --- a/crates/pgls_analyser/tests/rules_tests.rs +++ b/crates/pgls_analyser/tests/rules_tests.rs @@ -1,8 +1,8 @@ use core::slice; use std::{collections::HashMap, fmt::Write, fs::read_to_string, path::Path}; -use pgls_analyse::{AnalyserOptions, AnalysisFilter, RuleDiagnostic, RuleFilter}; -use pgls_analyser::{AnalysableStatement, Analyser, AnalyserConfig, AnalyserParams}; +use pgls_analyse::{AnalysisFilter, RuleFilter}; +use pgls_analyser::{AnalysableStatement, Analyser, AnalyserConfig, AnalyserParams, LinterDiagnostic, LinterOptions}; use pgls_console::StdDisplay; use pgls_diagnostics::PrintDiagnostic; @@ -25,7 +25,7 @@ fn rule_test(full_path: &'static str, _: &str, _: &str) { let query = read_to_string(full_path).unwrap_or_else(|_| panic!("Failed to read file: {full_path} ")); - let options = AnalyserOptions::default(); + let options = LinterOptions::default(); let analyser = Analyser::new(AnalyserConfig { options: &options, filter, @@ -79,7 +79,7 @@ fn parse_test_path(path: &Path) -> (String, String, String) { (group.into(), rule.into(), fname.into()) } -fn write_snapshot(snapshot: &mut String, query: &str, diagnostics: &[RuleDiagnostic]) { +fn write_snapshot(snapshot: &mut String, query: &str, diagnostics: &[LinterDiagnostic]) { writeln!(snapshot, "# Input").unwrap(); writeln!(snapshot, "```").unwrap(); writeln!(snapshot, "{query}").unwrap(); @@ -140,7 +140,7 @@ impl Expectation { ); } - fn assert(&self, diagnostics: &[RuleDiagnostic]) { + fn assert(&self, diagnostics: &[LinterDiagnostic]) { match self { Self::NoDiagnostics => { if !diagnostics.is_empty() { From 4f16e91572205ea50f75f14efcae6a9bda203bc1 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 15 Dec 2025 12:50:37 +0100 Subject: [PATCH 05/14] style: apply rustfmt to test files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed import ordering and line length formatting issues. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- crates/pgls_analyser/src/lib.rs | 2 +- crates/pgls_analyser/tests/rules_tests.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/pgls_analyser/src/lib.rs b/crates/pgls_analyser/src/lib.rs index 577a2a886..5cfd01897 100644 --- a/crates/pgls_analyser/src/lib.rs +++ b/crates/pgls_analyser/src/lib.rs @@ -116,8 +116,8 @@ impl<'a> Analyser<'a> { mod tests { use core::slice; - use pgls_analyse::{AnalysisFilter, RuleFilter}; use crate::LinterOptions; + use pgls_analyse::{AnalysisFilter, RuleFilter}; use pgls_console::{ Markup, fmt::{Formatter, Termcolor}, diff --git a/crates/pgls_analyser/tests/rules_tests.rs b/crates/pgls_analyser/tests/rules_tests.rs index 4749b3401..39b74e62c 100644 --- a/crates/pgls_analyser/tests/rules_tests.rs +++ b/crates/pgls_analyser/tests/rules_tests.rs @@ -2,7 +2,9 @@ use core::slice; use std::{collections::HashMap, fmt::Write, fs::read_to_string, path::Path}; use pgls_analyse::{AnalysisFilter, RuleFilter}; -use pgls_analyser::{AnalysableStatement, Analyser, AnalyserConfig, AnalyserParams, LinterDiagnostic, LinterOptions}; +use pgls_analyser::{ + AnalysableStatement, Analyser, AnalyserConfig, AnalyserParams, LinterDiagnostic, LinterOptions, +}; use pgls_console::StdDisplay; use pgls_diagnostics::PrintDiagnostic; From 4798b242fa9aa51261dade16ba2490a8bd56562e Mon Sep 17 00:00:00 2001 From: psteinroe Date: Sun, 14 Dec 2025 19:40:21 +0100 Subject: [PATCH 06/14] chore: integrate splinter into codegen --- Cargo.lock | 2 + PLAN.md | 406 +++++++++- .../src/analyser/splinter/mod.rs | 58 ++ .../src/analyser/splinter/rules.rs | 754 ++++++++++++++++++ .../src/generated/splinter.rs | 29 + crates/pgls_configuration/src/linter/rules.rs | 4 +- .../src/categories.rs | 44 +- crates/pgls_splinter/Cargo.toml | 1 + crates/pgls_splinter/src/convert.rs | 6 +- crates/pgls_splinter/src/lib.rs | 4 + crates/pgls_splinter/src/registry.rs | 79 ++ crates/pgls_splinter/src/rule.rs | 14 + crates/pgls_splinter/src/rules/mod.rs | 6 + .../rules/performance/auth_rls_initplan.rs | 11 + .../src/rules/performance/duplicate_index.rs | 11 + .../src/rules/performance/mod.rs | 11 + .../multiple_permissive_policies.rs | 11 + .../src/rules/performance/no_primary_key.rs | 11 + .../src/rules/performance/table_bloat.rs | 11 + .../performance/unindexed_foreign_keys.rs | 11 + .../src/rules/performance/unused_index.rs | 11 + .../src/rules/security/auth_users_exposed.rs | 11 + .../src/rules/security/extension_in_public.rs | 11 + .../security/extension_versions_outdated.rs | 11 + .../src/rules/security/fkey_to_auth_unique.rs | 11 + .../rules/security/foreign_table_in_api.rs | 11 + .../security/function_search_path_mutable.rs | 11 + .../security/insecure_queue_exposed_in_api.rs | 11 + .../security/materialized_view_in_api.rs | 11 + .../pgls_splinter/src/rules/security/mod.rs | 18 + .../security/policy_exists_rls_disabled.rs | 11 + .../rules/security/rls_disabled_in_public.rs | 11 + .../rules/security/rls_enabled_no_policy.rs | 11 + .../security/rls_references_user_metadata.rs | 11 + .../rules/security/security_definer_view.rs | 11 + .../rules/security/unsupported_reg_types.rs | 11 + .../vendor/performance/auth_rls_initplan.sql | 107 +++ .../vendor/performance/duplicate_index.sql | 61 ++ .../multiple_permissive_policies.sql | 79 ++ .../vendor/performance/no_primary_key.sql | 52 ++ .../vendor/performance/table_bloat.sql | 105 +++ .../performance/unindexed_foreign_keys.sql | 78 ++ .../vendor/performance/unused_index.sql | 49 ++ .../vendor/security/auth_users_exposed.sql | 95 +++ .../vendor/security/extension_in_public.sql | 40 + .../security/extension_versions_outdated.sql | 45 ++ .../vendor/security/fkey_to_auth_unique.sql | 49 ++ .../vendor/security/foreign_table_in_api.sql | 49 ++ .../security/function_search_path_mutable.sql | 50 ++ .../insecure_queue_exposed_in_api.sql | 46 ++ .../security/materialized_view_in_api.sql | 49 ++ .../security/policy_exists_rls_disabled.sql | 52 ++ .../security/rls_disabled_in_public.sql | 47 ++ .../vendor/security/rls_enabled_no_policy.sql | 52 ++ .../security/rls_references_user_metadata.sql | 61 ++ .../vendor/security/security_definer_view.sql | 59 ++ .../vendor/security/unsupported_reg_types.sql | 49 ++ xtask/codegen/Cargo.toml | 1 + xtask/codegen/src/generate_splinter.rs | 542 +++++++++---- 59 files changed, 3267 insertions(+), 217 deletions(-) create mode 100644 crates/pgls_configuration/src/analyser/splinter/mod.rs create mode 100644 crates/pgls_configuration/src/analyser/splinter/rules.rs create mode 100644 crates/pgls_configuration/src/generated/splinter.rs create mode 100644 crates/pgls_splinter/src/registry.rs create mode 100644 crates/pgls_splinter/src/rule.rs create mode 100644 crates/pgls_splinter/src/rules/mod.rs create mode 100644 crates/pgls_splinter/src/rules/performance/auth_rls_initplan.rs create mode 100644 crates/pgls_splinter/src/rules/performance/duplicate_index.rs create mode 100644 crates/pgls_splinter/src/rules/performance/mod.rs create mode 100644 crates/pgls_splinter/src/rules/performance/multiple_permissive_policies.rs create mode 100644 crates/pgls_splinter/src/rules/performance/no_primary_key.rs create mode 100644 crates/pgls_splinter/src/rules/performance/table_bloat.rs create mode 100644 crates/pgls_splinter/src/rules/performance/unindexed_foreign_keys.rs create mode 100644 crates/pgls_splinter/src/rules/performance/unused_index.rs create mode 100644 crates/pgls_splinter/src/rules/security/auth_users_exposed.rs create mode 100644 crates/pgls_splinter/src/rules/security/extension_in_public.rs create mode 100644 crates/pgls_splinter/src/rules/security/extension_versions_outdated.rs create mode 100644 crates/pgls_splinter/src/rules/security/fkey_to_auth_unique.rs create mode 100644 crates/pgls_splinter/src/rules/security/foreign_table_in_api.rs create mode 100644 crates/pgls_splinter/src/rules/security/function_search_path_mutable.rs create mode 100644 crates/pgls_splinter/src/rules/security/insecure_queue_exposed_in_api.rs create mode 100644 crates/pgls_splinter/src/rules/security/materialized_view_in_api.rs create mode 100644 crates/pgls_splinter/src/rules/security/mod.rs create mode 100644 crates/pgls_splinter/src/rules/security/policy_exists_rls_disabled.rs create mode 100644 crates/pgls_splinter/src/rules/security/rls_disabled_in_public.rs create mode 100644 crates/pgls_splinter/src/rules/security/rls_enabled_no_policy.rs create mode 100644 crates/pgls_splinter/src/rules/security/rls_references_user_metadata.rs create mode 100644 crates/pgls_splinter/src/rules/security/security_definer_view.rs create mode 100644 crates/pgls_splinter/src/rules/security/unsupported_reg_types.rs create mode 100644 crates/pgls_splinter/vendor/performance/auth_rls_initplan.sql create mode 100644 crates/pgls_splinter/vendor/performance/duplicate_index.sql create mode 100644 crates/pgls_splinter/vendor/performance/multiple_permissive_policies.sql create mode 100644 crates/pgls_splinter/vendor/performance/no_primary_key.sql create mode 100644 crates/pgls_splinter/vendor/performance/table_bloat.sql create mode 100644 crates/pgls_splinter/vendor/performance/unindexed_foreign_keys.sql create mode 100644 crates/pgls_splinter/vendor/performance/unused_index.sql create mode 100644 crates/pgls_splinter/vendor/security/auth_users_exposed.sql create mode 100644 crates/pgls_splinter/vendor/security/extension_in_public.sql create mode 100644 crates/pgls_splinter/vendor/security/extension_versions_outdated.sql create mode 100644 crates/pgls_splinter/vendor/security/fkey_to_auth_unique.sql create mode 100644 crates/pgls_splinter/vendor/security/foreign_table_in_api.sql create mode 100644 crates/pgls_splinter/vendor/security/function_search_path_mutable.sql create mode 100644 crates/pgls_splinter/vendor/security/insecure_queue_exposed_in_api.sql create mode 100644 crates/pgls_splinter/vendor/security/materialized_view_in_api.sql create mode 100644 crates/pgls_splinter/vendor/security/policy_exists_rls_disabled.sql create mode 100644 crates/pgls_splinter/vendor/security/rls_disabled_in_public.sql create mode 100644 crates/pgls_splinter/vendor/security/rls_enabled_no_policy.sql create mode 100644 crates/pgls_splinter/vendor/security/rls_references_user_metadata.sql create mode 100644 crates/pgls_splinter/vendor/security/security_definer_view.sql create mode 100644 crates/pgls_splinter/vendor/security/unsupported_reg_types.sql diff --git a/Cargo.lock b/Cargo.lock index 3425f1a83..677328fa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2996,6 +2996,7 @@ name = "pgls_splinter" version = "0.0.0" dependencies = [ "insta", + "pgls_analyse", "pgls_console", "pgls_diagnostics", "pgls_test_utils", @@ -5615,6 +5616,7 @@ dependencies = [ "pgls_analyser", "pgls_diagnostics", "pgls_env", + "pgls_splinter", "pgls_workspace", "proc-macro2", "pulldown-cmark", diff --git a/PLAN.md b/PLAN.md index 86e0536db..72c8b279f 100644 --- a/PLAN.md +++ b/PLAN.md @@ -56,6 +56,7 @@ Extract AST-specific code into pgls_linter, keep only generic framework in pgls_ - [x] Update codegen to generate factory function - [x] Fix all import paths across workspace - [x] Verify full workspace compiles +- [x] Optimize executor creation (zero-cost abstraction) - [ ] Run tests **Resolution:** @@ -63,19 +64,22 @@ Separated two concerns: 1. **Visitor pattern** (generic): Collects rule keys that match the filter - Implementation in `LinterRuleRegistryBuilder::record_rule` - Only requires `R: RuleMeta` (satisfies trait) -2. **Factory mapping** (AST-specific): Maps rule keys to executor factories - - Function `get_linter_rule_factory` in `registry.rs` - - Will be generated by codegen with full type information - - Each factory can require `R: LinterRule` - -**Changes Made:** -- `LinterRuleRegistryBuilder` stores `Vec` instead of factories -- `record_rule` just collects keys (no LinterRule bounds needed) -- `build()` calls `get_linter_rule_factory` to create executors -- Added stub `get_linter_rule_factory` in `registry.rs` (will be generated) - -**Next Steps:** -- Update codegen to generate `get_linter_rule_factory` with match on all rules +2. **Executor mapping** (AST-specific): Maps rule keys directly to executors + - Function `get_linter_rule_executor` in `registry.rs` + - Generated by codegen with full type information + - Zero-cost abstraction: no Box, no dyn, no closures + +**Final Implementation:** +- `LinterRuleRegistryBuilder` stores `Vec` from visitor traversal +- `record_rule` only collects keys (generic, no LinterRule bounds) +- `build()` calls `get_linter_rule_executor` for each key +- Generated match statement returns executors directly (no heap allocation) + +**Performance:** +- ✅ No `Box` - returns values directly +- ✅ No closure overhead - simple match statement +- ✅ No dynamic dispatch - static dispatch only +- ✅ Clean codegen - 33 rules map to executors efficiently **Design Decisions:** - ✅ Keep `RuleDiagnostic` generic or make it linter-specific? → **Move to pgls_linter as LinterDiagnostic** (Option A) @@ -108,44 +112,106 @@ Generic (keep in pgls_analyse): --- -### Phase 2: Enhance pgls_splinter 📋 PLANNED +### Phase 2: Enhance pgls_splinter ✅ COMPLETED Add rule type generation and registry similar to linter. **Tasks:** -- [ ] Create `pgls_splinter/src/rule.rs` with `SplinterRule` trait -- [ ] Create `pgls_splinter/src/rules/` directory structure -- [ ] Generate rule types from SQL files -- [ ] Generate registry with `visit_registry()` function -- [ ] Update diagnostics to use generated categories +- [x] Create `pgls_splinter/src/rule.rs` with `SplinterRule` trait +- [x] Create `pgls_splinter/src/rules/` directory structure +- [x] Generate rule types from SQL files +- [x] Generate registry with `visit_registry()` function +- [x] Split monolithic SQL files into individual rule files with metadata +- [x] Create codegen to extract metadata from SQL comments +- [x] Generate get_sql_file_path() function for SQL file mapping +- [ ] Update diagnostics to use generated categories (deferred) +- [ ] Update runtime to build dynamic queries (deferred to Phase 3) **Structure:** ``` pgls_splinter/src/ rules/ performance/ - unindexed_foreign_keys.rs - auth_rls_initplan.rs + unindexed_foreign_keys.rs # Generated + auth_rls_initplan.rs # Generated + ... # 7 total + mod.rs # Generated group security/ - auth_users_exposed.rs - rule.rs # SplinterRule trait - registry.rs # Generated visit_registry() + auth_users_exposed.rs # Generated + ... # 14 total + mod.rs # Generated group + mod.rs # Generated category + rule.rs # Generated SplinterRule trait + registry.rs # Generated visit_registry() + get_sql_file_path() + +pgls_splinter/vendor/ + performance/ + *.sql # 7 individual SQL files with metadata + security/ + *.sql # 14 individual SQL files with metadata ``` +**Implementation Summary:** +- Implemented Option C (Hybrid Approach) from initial analysis +- SQL files remain source of truth with metadata comments +- Codegen extracts metadata and generates Rust structures +- `SplinterRule` trait extends `RuleMeta` with `sql_file_path()` method +- Registry provides centralized rule discovery via visitor pattern +- Category structure: `Splinter` (Lint) → `Performance`/`Security` (groups) → individual rules +- Successfully compiles without errors + --- -### Phase 3: Update codegen for both linters 📋 PLANNED -Generalize codegen to handle both linter types. +### Phase 3: Integrate configuration and documentation 📋 PLANNED +Complete integration of splinter into the configuration and documentation systems. **Tasks:** -- [ ] Rename `generate_analyser.rs` → `generate_linter.rs` -- [ ] Enhance `generate_splinter.rs` to generate rules + registry -- [ ] Update `generate_configuration.rs` for both linters -- [ ] Update justfile commands -- [ ] Test full generation cycle +- [ ] **Configuration Generation**: + - [ ] Create `pgls_configuration/src/analyser/splinter/` directory + - [ ] Generate splinter configuration types (groups, rules) + - [ ] Update `generate_configuration.rs` to visit splinter registry + - [ ] Generate `crates/pgls_configuration/src/generated/splinter.rs` + - [ ] Update `analyser/mod.rs` to export splinter config + - [ ] Add splinter to RuleSelector enum + +- [ ] **Documentation Enhancement** (FUTURE): + - [ ] Add SQL query examples to splinter rule docs (similar to linter) + - [ ] Extract SQL from vendor/*.sql files into doc comments + - [ ] Add usage examples and remediation steps + - [ ] Generate rule documentation via docs_codegen + +- [ ] **Runtime Integration** (DEFERRED): + - [ ] Update `run_splinter()` to use visitor pattern with AnalysisFilter + - [ ] Build dynamic SQL queries from enabled rules only + - [ ] Remove hardcoded SQL query execution + - [ ] Remove hardcoded category mapping in convert.rs + +- [ ] **Testing**: + - [ ] Run `just gen-lint` successfully + - [ ] Verify linter configuration still works + - [ ] Verify splinter configuration generates + - [ ] Test enabling/disabling splinter rules via config + - [ ] Verify full workspace compiles + +**Codegen Outputs After Phase 3:** +``` +Linter: + - crates/pgls_analyser/src/registry.rs (generated) + - crates/pgls_analyser/src/options.rs (generated) + - crates/pgls_configuration/src/analyser/linter/ (generated) + - crates/pgls_configuration/src/generated/linter.rs (generated) + +Splinter: + - crates/pgls_splinter/src/rules/ (generated - ✅ DONE) + - crates/pgls_splinter/src/rule.rs (generated - ✅ DONE) + - crates/pgls_splinter/src/registry.rs (generated - ✅ DONE) + - crates/pgls_configuration/src/analyser/splinter/ (TODO) + - crates/pgls_configuration/src/generated/splinter.rs (TODO) +``` -**Codegen outputs:** -- Linter: registry.rs, options.rs, configuration -- Splinter: rules/, registry.rs, configuration +**Notes:** +- Runtime integration (dynamic SQL query building) is deferred as it requires more complex changes to `run_splinter()` +- Documentation enhancement with SQL examples is marked as FUTURE work +- Focus Phase 3 on configuration integration to enable rule enable/disable via config files --- @@ -161,15 +227,279 @@ Final rename to clarify purpose. --- +### Phase 5: Runtime & Documentation Enhancements 📋 FUTURE +Advanced features for splinter integration (optional future work). + +--- + +#### **Part A: Configuration Structure Design** + +**Goal:** Create a splinter configuration structure that mirrors linter but shares common code. + +**Shared Components** (from `analyser/mod.rs`): +- `RuleConfiguration` - Configuration wrapper with severity levels +- `RulePlainConfiguration` - Severity enum (Warn, Error, Info, Off) +- Merge/Deserialize traits + +**New Splinter-Specific Types** (to generate in Phase 3): + +```rust +// analyser/splinter/mod.rs +pub struct SplinterConfiguration { + /// Enable/disable splinter linting + pub enabled: bool, + + /// Splinter rules configuration + pub rules: Rules, + + /// Ignore/include patterns (shared with linter) + pub ignore: StringSet, + pub include: StringSet, +} + +// analyser/splinter/rules.rs (GENERATED) +pub enum RuleGroup { + Performance, + Security, +} + +pub struct Rules { + /// Enable recommended rules + pub recommended: Option, + + /// Enable all rules + pub all: Option, + + /// Performance group + pub performance: Option, + + /// Security group + pub security: Option, +} + +// Performance group (GENERATED) +pub struct Performance { + pub recommended: Option, + pub all: Option, + + // Individual rules - note: using RuleConfiguration<()> since no options + pub unindexed_foreign_keys: Option>, + pub auth_rls_initplan: Option>, + pub duplicate_index: Option>, + pub multiple_permissive_policies: Option>, + pub no_primary_key: Option>, + pub table_bloat: Option>, + pub unused_index: Option>, +} + +impl Performance { + const GROUP_NAME: &'static str = "performance"; + const GROUP_RULES: &'static [&'static str] = &[ + "unindexedForeignKeys", + "authRlsInitplan", + // ... all 7 rules + ]; + + // Methods mirroring Safety group + pub fn has_rule(rule_name: &str) -> Option<&'static str> { /* ... */ } + pub fn severity(rule_name: &str) -> Severity { /* ... */ } + pub fn all_rules_as_filters() -> impl Iterator> { /* ... */ } + pub fn recommended_rules_as_filters() -> impl Iterator> { /* ... */ } + pub fn collect_preset_rules(&self, ...) { /* ... */ } + pub fn get_enabled_rules(&self) -> FxHashSet> { /* ... */ } + pub fn get_disabled_rules(&self) -> FxHashSet> { /* ... */ } +} + +// Security group (GENERATED) - same structure, 14 rules +pub struct Security { /* ... */ } +``` + +**Config File Example:** +```json +{ + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "safety": { + "addSerialColumn": "error" + } + } + }, + "splinter": { + "enabled": true, + "rules": { + "recommended": true, + "performance": { + "unindexedForeignKeys": "warn", + "noPrimaryKey": "off" + }, + "security": { + "all": true, + "authUsersExposed": "error" + } + } + } +} +``` + +**Code Sharing Strategy:** +- ✅ Reuse `RuleConfiguration` with `T = ()` for splinter (no rule options) +- ✅ Reuse severity conversion logic +- ✅ Same `as_enabled_rules()` / `as_disabled_rules()` pattern +- ✅ Same methods for each group (has_rule, severity, etc.) +- ⚠️ RuleGroup enum is separate (linter has Safety, splinter has Performance/Security) +- ⚠️ RuleSelector needs updating to handle both "lint/" and "splinter/" prefixes + +--- + +#### **Part B: Dynamic SQL Query Building** + +**Tasks:** +- [ ] Modify `run_splinter()` signature: + ```rust + pub async fn run_splinter( + params: SplinterParams<'_>, + filter: &AnalysisFilter, // NEW + ) -> Result, sqlx::Error> + ``` + +- [ ] Use visitor pattern to collect enabled rules: + ```rust + struct SplinterRuleCollector<'a> { + filter: &'a AnalysisFilter<'a>, + enabled_rules: Vec, // Rule names in camelCase + } + + impl RegistryVisitor for SplinterRuleCollector<'_> { + fn record_rule(&mut self) { + if self.filter.match_rule::() { + self.enabled_rules.push(R::METADATA.name.to_string()); + } + } + } + ``` + +- [ ] Build dynamic SQL query: + ```rust + let mut collector = SplinterRuleCollector { filter, enabled_rules: Vec::new() }; + crate::registry::visit_registry(&mut collector); + + // Map rule names to SQL file paths + let mut sql_queries = Vec::new(); + for rule_name in &collector.enabled_rules { + if let Some(sql_path) = crate::registry::get_sql_file_path(rule_name) { + let sql = std::fs::read_to_string(sql_path)?; + sql_queries.push(sql); + } + } + + // Combine with UNION ALL (only if enabled rules exist) + if sql_queries.is_empty() { + return Ok(Vec::new()); + } + + let combined_sql = sql_queries.join("\nUNION ALL\n"); + let results = sqlx::query_as::<_, SplinterQueryResult>(&combined_sql) + .fetch_all(params.conn) + .await?; + ``` + +- [ ] Remove hardcoded functions: + - Delete `load_generic_splinter_results()` + - Delete `load_supabase_splinter_results()` + - Remove `check_supabase_roles()` logic (rules are filtered by config) + +- [ ] Update convert.rs: + - Use generated category from `RuleMeta::METADATA.category` instead of `rule_name_to_category()` + - Or better: get category from diagnostic category system + +--- + +#### **Part C: Enhanced Documentation** + +**Tasks:** +- [ ] Extract SQL queries into rule doc comments: + ```rust + // In generate_splinter.rs codegen + let sql_content = std::fs::read_to_string(&sql_path)?; + let sql_query = extract_sql_query(&sql_content)?; // Remove metadata comments + + let content = quote! { + /// #title + /// + /// #description + /// + /// ## SQL Query + /// + /// ```sql + /// #sql_query + /// ``` + /// + /// ## Remediation + /// + /// #remediation + pub #struct_name { ... } + }; + ``` + +- [ ] Add example SQL snippets showing what triggers the rule +- [ ] Update docs_codegen to process splinter rules +- [ ] Generate markdown documentation for website + +--- + +#### **Benefits Summary:** + +**Performance:** +- 🚀 Only execute SQL for enabled rules (vs. running all 21 rules) +- 🚀 Skip expensive queries when rules are disabled +- 🚀 Example: User disables 18/21 rules → only 3 SQL queries execute + +**Consistency:** +- ✅ Same enable/disable mechanism as linter +- ✅ Same configuration file structure +- ✅ Same visitor pattern for rule discovery + +**Maintainability:** +- ✅ No hardcoded SQL queries in Rust +- ✅ SQL files remain source of truth +- ✅ Adding new rules = add SQL file + run codegen +- ✅ No manual category mapping needed + +**Documentation:** +- ✅ Rule docs show actual SQL query +- ✅ Better understanding of what each rule does +- ✅ Easier to debug and customize + +--- + +#### **Migration Path:** + +**Phase 3** (Configuration Integration): +1. Generate splinter configuration types +2. Wire into config system +3. Users can enable/disable via config (but all enabled rules still run) + +**Phase 5** (This Phase - Runtime Optimization): +1. Update `run_splinter()` to use filter +2. Build dynamic SQL queries +3. Performance benefit: only enabled rules execute + +This allows incremental rollout - config works in Phase 3, optimization comes in Phase 5. + +--- + ## Progress Tracking ### Current Status - [x] Requirements gathering & design -- [x] Architecture proposal (Option 1 - Dual-Track) -- [ ] Phase 1: Refactor pgls_analyse - **IN PROGRESS** -- [ ] Phase 2: Enhance pgls_splinter -- [ ] Phase 3: Update codegen +- [x] Architecture proposal (Option C - Hybrid Approach) +- [x] Phase 1: Refactor pgls_analyse - **COMPLETED** +- [x] Phase 2: Enhance pgls_splinter - **COMPLETED** +- [ ] Phase 3: Integrate configuration - **NEXT** - [ ] Phase 4: Rename to pgls_linter +- [ ] Phase 5: Runtime & Docs (FUTURE) ### Open Questions None currently diff --git a/crates/pgls_configuration/src/analyser/splinter/mod.rs b/crates/pgls_configuration/src/analyser/splinter/mod.rs new file mode 100644 index 000000000..086c6ce0f --- /dev/null +++ b/crates/pgls_configuration/src/analyser/splinter/mod.rs @@ -0,0 +1,58 @@ +mod rules; + +use biome_deserialize::StringSet; +use biome_deserialize_macros::{Merge, Partial}; +use bpaf::Bpaf; +pub use rules::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Eq, Partial, PartialEq, Serialize)] +#[partial(derive(Bpaf, Clone, Eq, Merge, PartialEq))] +#[partial(cfg_attr(feature = "schema", derive(schemars::JsonSchema)))] +#[partial(serde(rename_all = "camelCase", default, deny_unknown_fields))] +pub struct SplinterConfiguration { + /// if `false`, it disables the feature and the splinter won't be executed. `true` by default + #[partial(bpaf(hide))] + pub enabled: bool, + + /// List of rules + #[partial(bpaf(pure(Default::default()), optional, hide))] + pub rules: Rules, + + /// A list of Unix shell style patterns. The splinter will ignore files/folders that will + /// match these patterns. + #[partial(bpaf(hide))] + pub ignore: StringSet, + + /// A list of Unix shell style patterns. The splinter will include files/folders that will + /// match these patterns. + #[partial(bpaf(hide))] + pub include: StringSet, +} + +impl SplinterConfiguration { + pub const fn is_disabled(&self) -> bool { + !self.enabled + } +} + +impl Default for SplinterConfiguration { + fn default() -> Self { + Self { + enabled: true, + rules: Default::default(), + ignore: Default::default(), + include: Default::default(), + } + } +} + +impl PartialSplinterConfiguration { + pub const fn is_disabled(&self) -> bool { + matches!(self.enabled, Some(false)) + } + + pub fn get_rules(&self) -> Rules { + self.rules.clone().unwrap_or_default() + } +} diff --git a/crates/pgls_configuration/src/analyser/splinter/rules.rs b/crates/pgls_configuration/src/analyser/splinter/rules.rs new file mode 100644 index 000000000..9a85f8626 --- /dev/null +++ b/crates/pgls_configuration/src/analyser/splinter/rules.rs @@ -0,0 +1,754 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +use crate::analyser::{RuleConfiguration, RulePlainConfiguration}; +use biome_deserialize_macros::Merge; +use pgls_analyse::RuleFilter; +use pgls_analyser::RuleOptions; +use pgls_diagnostics::{Category, Severity}; +use rustc_hash::FxHashSet; +#[cfg(feature = "schema")] +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + Merge, + Ord, + PartialEq, + PartialOrd, + serde :: Deserialize, + serde :: Serialize, +)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase")] +pub enum RuleGroup { + Performance, + Security, +} +impl RuleGroup { + pub const fn as_str(self) -> &'static str { + match self { + Self::Performance => Performance::GROUP_NAME, + Self::Security => Security::GROUP_NAME, + } + } +} +impl std::str::FromStr for RuleGroup { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s { + Performance::GROUP_NAME => Ok(Self::Performance), + Security::GROUP_NAME => Ok(Self::Security), + _ => Err("This rule group doesn't exist."), + } + } +} +#[derive(Clone, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Rules { + #[doc = r" It enables the lint rules recommended by Postgres Language Server. `true` by default."] + #[serde(skip_serializing_if = "Option::is_none")] + pub recommended: Option, + #[doc = r" It enables ALL rules. The rules that belong to `nursery` won't be enabled."] + #[serde(skip_serializing_if = "Option::is_none")] + pub all: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub performance: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub security: Option, +} +impl Rules { + #[doc = r" Checks if the code coming from [pgls_diagnostics::Diagnostic] corresponds to a rule."] + #[doc = r" Usually the code is built like {group}/{rule_name}"] + pub fn has_rule(group: RuleGroup, rule_name: &str) -> Option<&'static str> { + match group { + RuleGroup::Performance => Performance::has_rule(rule_name), + RuleGroup::Security => Security::has_rule(rule_name), + } + } + #[doc = r" Given a category coming from [Diagnostic](pgls_diagnostics::Diagnostic), this function returns"] + #[doc = r" the [Severity](pgls_diagnostics::Severity) associated to the rule, if the configuration changed it."] + #[doc = r" If the severity is off or not set, then the function returns the default severity of the rule,"] + #[doc = r" which is configured at the rule definition."] + #[doc = r" The function can return `None` if the rule is not properly configured."] + pub fn get_severity_from_code(&self, category: &Category) -> Option { + let mut split_code = category.name().split('/'); + let _category = split_code.next(); + debug_assert_eq!(_category, Some("splinter")); + let group = ::from_str(split_code.next()?).ok()?; + let rule_name = split_code.next()?; + let rule_name = Self::has_rule(group, rule_name)?; + let severity = match group { + RuleGroup::Performance => self + .performance + .as_ref() + .and_then(|group| group.get_rule_configuration(rule_name)) + .filter(|(level, _)| !matches!(level, RulePlainConfiguration::Off)) + .map_or_else( + || Performance::severity(rule_name), + |(level, _)| level.into(), + ), + RuleGroup::Security => self + .security + .as_ref() + .and_then(|group| group.get_rule_configuration(rule_name)) + .filter(|(level, _)| !matches!(level, RulePlainConfiguration::Off)) + .map_or_else(|| Security::severity(rule_name), |(level, _)| level.into()), + }; + Some(severity) + } + #[doc = r" Ensure that `recommended` is set to `true` or implied."] + pub fn set_recommended(&mut self) { + if self.all != Some(true) && self.recommended == Some(false) { + self.recommended = Some(true) + } + if let Some(group) = &mut self.performance { + group.recommended = None; + } + if let Some(group) = &mut self.security { + group.recommended = None; + } + } + pub(crate) const fn is_recommended_false(&self) -> bool { + matches!(self.recommended, Some(false)) + } + pub(crate) const fn is_all_true(&self) -> bool { + matches!(self.all, Some(true)) + } + #[doc = r" It returns the enabled rules by default."] + #[doc = r""] + #[doc = r" The enabled rules are calculated from the difference with the disabled rules."] + pub fn as_enabled_rules(&self) -> FxHashSet> { + let mut enabled_rules = FxHashSet::default(); + let mut disabled_rules = FxHashSet::default(); + if let Some(group) = self.performance.as_ref() { + group.collect_preset_rules( + self.is_all_true(), + !self.is_recommended_false(), + &mut enabled_rules, + ); + enabled_rules.extend(&group.get_enabled_rules()); + disabled_rules.extend(&group.get_disabled_rules()); + } else if self.is_all_true() { + enabled_rules.extend(Performance::all_rules_as_filters()); + } else if !self.is_recommended_false() { + enabled_rules.extend(Performance::recommended_rules_as_filters()); + } + if let Some(group) = self.security.as_ref() { + group.collect_preset_rules( + self.is_all_true(), + !self.is_recommended_false(), + &mut enabled_rules, + ); + enabled_rules.extend(&group.get_enabled_rules()); + disabled_rules.extend(&group.get_disabled_rules()); + } else if self.is_all_true() { + enabled_rules.extend(Security::all_rules_as_filters()); + } else if !self.is_recommended_false() { + enabled_rules.extend(Security::recommended_rules_as_filters()); + } + enabled_rules.difference(&disabled_rules).copied().collect() + } + #[doc = r" It returns the disabled rules by configuration."] + pub fn as_disabled_rules(&self) -> FxHashSet> { + let mut disabled_rules = FxHashSet::default(); + if let Some(group) = self.performance.as_ref() { + disabled_rules.extend(&group.get_disabled_rules()); + } + if let Some(group) = self.security.as_ref() { + disabled_rules.extend(&group.get_disabled_rules()); + } + disabled_rules + } +} +#[derive(Clone, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", default, deny_unknown_fields)] +#[doc = r" A list of rules that belong to this group"] +pub struct Performance { + #[doc = r" It enables the recommended rules for this group"] + #[serde(skip_serializing_if = "Option::is_none")] + pub recommended: Option, + #[doc = r" It enables ALL rules for this group."] + #[serde(skip_serializing_if = "Option::is_none")] + pub all: Option, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_rls_initplan: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub duplicate_index: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub multiple_permissive_policies: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_primary_key: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub table_bloat: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub unindexed_foreign_keys: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub unused_index: Option>, +} +impl Performance { + const GROUP_NAME: &'static str = "performance"; + pub(crate) const GROUP_RULES: &'static [&'static str] = &[ + "authRlsInitplan", + "duplicateIndex", + "multiplePermissivePolicies", + "noPrimaryKey", + "tableBloat", + "unindexedForeignKeys", + "unusedIndex", + ]; + const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[]; + const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6]), + ]; + #[doc = r" Retrieves the recommended rules"] + pub(crate) fn is_recommended_true(&self) -> bool { + matches!(self.recommended, Some(true)) + } + pub(crate) fn is_recommended_unset(&self) -> bool { + self.recommended.is_none() + } + pub(crate) fn is_all_true(&self) -> bool { + matches!(self.all, Some(true)) + } + pub(crate) fn is_all_unset(&self) -> bool { + self.all.is_none() + } + pub(crate) fn get_enabled_rules(&self) -> FxHashSet> { + let mut index_set = FxHashSet::default(); + if let Some(rule) = self.auth_rls_initplan.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0])); + } + } + if let Some(rule) = self.duplicate_index.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1])); + } + } + if let Some(rule) = self.multiple_permissive_policies.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); + } + } + if let Some(rule) = self.no_primary_key.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); + } + } + if let Some(rule) = self.table_bloat.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); + } + } + if let Some(rule) = self.unindexed_foreign_keys.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); + } + } + if let Some(rule) = self.unused_index.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); + } + } + index_set + } + pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { + let mut index_set = FxHashSet::default(); + if let Some(rule) = self.auth_rls_initplan.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0])); + } + } + if let Some(rule) = self.duplicate_index.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1])); + } + } + if let Some(rule) = self.multiple_permissive_policies.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); + } + } + if let Some(rule) = self.no_primary_key.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); + } + } + if let Some(rule) = self.table_bloat.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); + } + } + if let Some(rule) = self.unindexed_foreign_keys.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); + } + } + if let Some(rule) = self.unused_index.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); + } + } + index_set + } + #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] + pub(crate) fn has_rule(rule_name: &str) -> Option<&'static str> { + Some(Self::GROUP_RULES[Self::GROUP_RULES.binary_search(&rule_name).ok()?]) + } + pub(crate) fn recommended_rules_as_filters() -> &'static [RuleFilter<'static>] { + Self::RECOMMENDED_RULES_AS_FILTERS + } + pub(crate) fn all_rules_as_filters() -> &'static [RuleFilter<'static>] { + Self::ALL_RULES_AS_FILTERS + } + #[doc = r" Select preset rules"] + pub(crate) fn collect_preset_rules( + &self, + parent_is_all: bool, + parent_is_recommended: bool, + enabled_rules: &mut FxHashSet>, + ) { + if self.is_all_true() || self.is_all_unset() && parent_is_all { + enabled_rules.extend(Self::all_rules_as_filters()); + } else if self.is_recommended_true() + || self.is_recommended_unset() && self.is_all_unset() && parent_is_recommended + { + enabled_rules.extend(Self::recommended_rules_as_filters()); + } + } + pub(crate) fn severity(rule_name: &str) -> Severity { + match rule_name { + "authRlsInitplan" => Severity::Warning, + "duplicateIndex" => Severity::Warning, + "multiplePermissivePolicies" => Severity::Warning, + "noPrimaryKey" => Severity::Information, + "tableBloat" => Severity::Information, + "unindexedForeignKeys" => Severity::Information, + "unusedIndex" => Severity::Information, + _ => unreachable!(), + } + } + pub(crate) fn get_rule_configuration( + &self, + rule_name: &str, + ) -> Option<(RulePlainConfiguration, Option)> { + match rule_name { + "authRlsInitplan" => self + .auth_rls_initplan + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "duplicateIndex" => self + .duplicate_index + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "multiplePermissivePolicies" => self + .multiple_permissive_policies + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "noPrimaryKey" => self + .no_primary_key + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "tableBloat" => self + .table_bloat + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "unindexedForeignKeys" => self + .unindexed_foreign_keys + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "unusedIndex" => self + .unused_index + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + _ => None, + } + } +} +#[derive(Clone, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", default, deny_unknown_fields)] +#[doc = r" A list of rules that belong to this group"] +pub struct Security { + #[doc = r" It enables the recommended rules for this group"] + #[serde(skip_serializing_if = "Option::is_none")] + pub recommended: Option, + #[doc = r" It enables ALL rules for this group."] + #[serde(skip_serializing_if = "Option::is_none")] + pub all: Option, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_users_exposed: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub extension_in_public: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub extension_versions_outdated: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub fkey_to_auth_unique: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub foreign_table_in_api: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub function_search_path_mutable: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub insecure_queue_exposed_in_api: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub materialized_view_in_api: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub policy_exists_rls_disabled: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub rls_disabled_in_public: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub rls_enabled_no_policy: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub rls_references_user_metadata: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub security_definer_view: Option>, + #[doc = "#title"] + #[serde(skip_serializing_if = "Option::is_none")] + pub unsupported_reg_types: Option>, +} +impl Security { + const GROUP_NAME: &'static str = "security"; + pub(crate) const GROUP_RULES: &'static [&'static str] = &[ + "authUsersExposed", + "extensionInPublic", + "extensionVersionsOutdated", + "fkeyToAuthUnique", + "foreignTableInApi", + "functionSearchPathMutable", + "insecureQueueExposedInApi", + "materializedViewInApi", + "policyExistsRlsDisabled", + "rlsDisabledInPublic", + "rlsEnabledNoPolicy", + "rlsReferencesUserMetadata", + "securityDefinerView", + "unsupportedRegTypes", + ]; + const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[]; + const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13]), + ]; + #[doc = r" Retrieves the recommended rules"] + pub(crate) fn is_recommended_true(&self) -> bool { + matches!(self.recommended, Some(true)) + } + pub(crate) fn is_recommended_unset(&self) -> bool { + self.recommended.is_none() + } + pub(crate) fn is_all_true(&self) -> bool { + matches!(self.all, Some(true)) + } + pub(crate) fn is_all_unset(&self) -> bool { + self.all.is_none() + } + pub(crate) fn get_enabled_rules(&self) -> FxHashSet> { + let mut index_set = FxHashSet::default(); + if let Some(rule) = self.auth_users_exposed.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0])); + } + } + if let Some(rule) = self.extension_in_public.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1])); + } + } + if let Some(rule) = self.extension_versions_outdated.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); + } + } + if let Some(rule) = self.fkey_to_auth_unique.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); + } + } + if let Some(rule) = self.foreign_table_in_api.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); + } + } + if let Some(rule) = self.function_search_path_mutable.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); + } + } + if let Some(rule) = self.insecure_queue_exposed_in_api.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); + } + } + if let Some(rule) = self.materialized_view_in_api.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); + } + } + if let Some(rule) = self.policy_exists_rls_disabled.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); + } + } + if let Some(rule) = self.rls_disabled_in_public.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); + } + } + if let Some(rule) = self.rls_enabled_no_policy.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); + } + } + if let Some(rule) = self.rls_references_user_metadata.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); + } + } + if let Some(rule) = self.security_definer_view.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); + } + } + if let Some(rule) = self.unsupported_reg_types.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); + } + } + index_set + } + pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { + let mut index_set = FxHashSet::default(); + if let Some(rule) = self.auth_users_exposed.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0])); + } + } + if let Some(rule) = self.extension_in_public.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1])); + } + } + if let Some(rule) = self.extension_versions_outdated.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); + } + } + if let Some(rule) = self.fkey_to_auth_unique.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); + } + } + if let Some(rule) = self.foreign_table_in_api.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); + } + } + if let Some(rule) = self.function_search_path_mutable.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); + } + } + if let Some(rule) = self.insecure_queue_exposed_in_api.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); + } + } + if let Some(rule) = self.materialized_view_in_api.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); + } + } + if let Some(rule) = self.policy_exists_rls_disabled.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); + } + } + if let Some(rule) = self.rls_disabled_in_public.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); + } + } + if let Some(rule) = self.rls_enabled_no_policy.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); + } + } + if let Some(rule) = self.rls_references_user_metadata.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); + } + } + if let Some(rule) = self.security_definer_view.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); + } + } + if let Some(rule) = self.unsupported_reg_types.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); + } + } + index_set + } + #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] + pub(crate) fn has_rule(rule_name: &str) -> Option<&'static str> { + Some(Self::GROUP_RULES[Self::GROUP_RULES.binary_search(&rule_name).ok()?]) + } + pub(crate) fn recommended_rules_as_filters() -> &'static [RuleFilter<'static>] { + Self::RECOMMENDED_RULES_AS_FILTERS + } + pub(crate) fn all_rules_as_filters() -> &'static [RuleFilter<'static>] { + Self::ALL_RULES_AS_FILTERS + } + #[doc = r" Select preset rules"] + pub(crate) fn collect_preset_rules( + &self, + parent_is_all: bool, + parent_is_recommended: bool, + enabled_rules: &mut FxHashSet>, + ) { + if self.is_all_true() || self.is_all_unset() && parent_is_all { + enabled_rules.extend(Self::all_rules_as_filters()); + } else if self.is_recommended_true() + || self.is_recommended_unset() && self.is_all_unset() && parent_is_recommended + { + enabled_rules.extend(Self::recommended_rules_as_filters()); + } + } + pub(crate) fn severity(rule_name: &str) -> Severity { + match rule_name { + "authUsersExposed" => Severity::Error, + "extensionInPublic" => Severity::Warning, + "extensionVersionsOutdated" => Severity::Warning, + "fkeyToAuthUnique" => Severity::Error, + "foreignTableInApi" => Severity::Warning, + "functionSearchPathMutable" => Severity::Warning, + "insecureQueueExposedInApi" => Severity::Error, + "materializedViewInApi" => Severity::Warning, + "policyExistsRlsDisabled" => Severity::Error, + "rlsDisabledInPublic" => Severity::Error, + "rlsEnabledNoPolicy" => Severity::Information, + "rlsReferencesUserMetadata" => Severity::Error, + "securityDefinerView" => Severity::Error, + "unsupportedRegTypes" => Severity::Warning, + _ => unreachable!(), + } + } + pub(crate) fn get_rule_configuration( + &self, + rule_name: &str, + ) -> Option<(RulePlainConfiguration, Option)> { + match rule_name { + "authUsersExposed" => self + .auth_users_exposed + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "extensionInPublic" => self + .extension_in_public + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "extensionVersionsOutdated" => self + .extension_versions_outdated + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "fkeyToAuthUnique" => self + .fkey_to_auth_unique + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "foreignTableInApi" => self + .foreign_table_in_api + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "functionSearchPathMutable" => self + .function_search_path_mutable + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "insecureQueueExposedInApi" => self + .insecure_queue_exposed_in_api + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "materializedViewInApi" => self + .materialized_view_in_api + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "policyExistsRlsDisabled" => self + .policy_exists_rls_disabled + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "rlsDisabledInPublic" => self + .rls_disabled_in_public + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "rlsEnabledNoPolicy" => self + .rls_enabled_no_policy + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "rlsReferencesUserMetadata" => self + .rls_references_user_metadata + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "securityDefinerView" => self + .security_definer_view + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + "unsupportedRegTypes" => self + .unsupported_reg_types + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), + _ => None, + } + } +} +#[test] +fn test_order() { + for items in Performance::GROUP_RULES.windows(2) { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); + } + for items in Security::GROUP_RULES.windows(2) { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); + } +} diff --git a/crates/pgls_configuration/src/generated/splinter.rs b/crates/pgls_configuration/src/generated/splinter.rs new file mode 100644 index 000000000..ad6504041 --- /dev/null +++ b/crates/pgls_configuration/src/generated/splinter.rs @@ -0,0 +1,29 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +use crate::analyser::splinter::*; +use pgls_analyse::MetadataRegistry; +use pgls_analyser::LinterRules; +pub fn push_to_analyser_splinter( + rules: &Rules, + metadata: &MetadataRegistry, + analyser_rules: &mut LinterRules, +) { + if let Some(rules) = rules.performance.as_ref() { + for rule_name in Performance::GROUP_RULES { + if let Some((_, Some(rule_options))) = rules.get_rule_configuration(rule_name) { + if let Some(rule_key) = metadata.find_rule("performance", rule_name) { + analyser_rules.push_rule(rule_key, rule_options); + } + } + } + } + if let Some(rules) = rules.security.as_ref() { + for rule_name in Security::GROUP_RULES { + if let Some((_, Some(rule_options))) = rules.get_rule_configuration(rule_name) { + if let Some(rule_key) = metadata.find_rule("security", rule_name) { + analyser_rules.push_rule(rule_key, rule_options); + } + } + } + } +} diff --git a/crates/pgls_configuration/src/linter/rules.rs b/crates/pgls_configuration/src/linter/rules.rs index 4bc3d4c98..6336d88fa 100644 --- a/crates/pgls_configuration/src/linter/rules.rs +++ b/crates/pgls_configuration/src/linter/rules.rs @@ -72,8 +72,8 @@ impl Rules { #[doc = r" The function can return `None` if the rule is not properly configured."] pub fn get_severity_from_code(&self, category: &Category) -> Option { let mut split_code = category.name().split('/'); - let _lint = split_code.next(); - debug_assert_eq!(_lint, Some("lint")); + let _category = split_code.next(); + debug_assert_eq!(_category, Some("lint")); let group = ::from_str(split_code.next()?).ok()?; let rule_name = split_code.next()?; let rule_name = Self::has_rule(group, rule_name)?; diff --git a/crates/pgls_diagnostics_categories/src/categories.rs b/crates/pgls_diagnostics_categories/src/categories.rs index ce18c4d09..eb9e323ac 100644 --- a/crates/pgls_diagnostics_categories/src/categories.rs +++ b/crates/pgls_diagnostics_categories/src/categories.rs @@ -48,28 +48,27 @@ define_categories! { "lint/safety/transactionNesting": "https://pg-language-server.com/latest/reference/rules/transaction-nesting/", // end lint rules // splinter rules start - "splinter/performance/authRlsInitplan": "https://supabase.com/docs/guides/database/database-advisors?lint=0003_auth_rls_initplan", - "splinter/performance/duplicateIndex": "https://supabase.com/docs/guides/database/database-advisors?lint=0009_duplicate_index", - "splinter/performance/multiplePermissivePolicies": "https://supabase.com/docs/guides/database/database-advisors?lint=0006_multiple_permissive_policies", - "splinter/performance/noPrimaryKey": "https://supabase.com/docs/guides/database/database-advisors?lint=0004_no_primary_key", - "splinter/performance/tableBloat": "https://supabase.com/docs/guides/database/database-advisors", - "splinter/performance/unindexedForeignKeys": "https://supabase.com/docs/guides/database/database-advisors?lint=0001_unindexed_foreign_keys", - "splinter/performance/unusedIndex": "https://supabase.com/docs/guides/database/database-advisors?lint=0005_unused_index", - "splinter/security/authUsersExposed": "https://supabase.com/docs/guides/database/database-advisors?lint=0002_auth_users_exposed", - "splinter/security/extensionInPublic": "https://supabase.com/docs/guides/database/database-advisors?lint=0014_extension_in_public", - "splinter/security/extensionVersionsOutdated": "https://supabase.com/docs/guides/database/database-advisors?lint=0022_extension_versions_outdated", - "splinter/security/fkeyToAuthUnique": "https://supabase.com/docs/guides/database/database-advisors", - "splinter/security/foreignTableInApi": "https://supabase.com/docs/guides/database/database-advisors?lint=0017_foreign_table_in_api", - "splinter/security/functionSearchPathMutable": "https://supabase.com/docs/guides/database/database-advisors?lint=0011_function_search_path_mutable", - "splinter/security/insecureQueueExposedInApi": "https://supabase.com/docs/guides/database/database-advisors?lint=0019_insecure_queue_exposed_in_api", - "splinter/security/materializedViewInApi": "https://supabase.com/docs/guides/database/database-advisors?lint=0016_materialized_view_in_api", - "splinter/security/policyExistsRlsDisabled": "https://supabase.com/docs/guides/database/database-advisors?lint=0007_policy_exists_rls_disabled", - "splinter/security/rlsDisabledInPublic": "https://supabase.com/docs/guides/database/database-advisors?lint=0013_rls_disabled_in_public", - "splinter/security/rlsEnabledNoPolicy": "https://supabase.com/docs/guides/database/database-advisors?lint=0008_rls_enabled_no_policy", - "splinter/security/rlsReferencesUserMetadata": "https://supabase.com/docs/guides/database/database-advisors?lint=0015_rls_references_user_metadata", - "splinter/security/securityDefinerView": "https://supabase.com/docs/guides/database/database-advisors?lint=0010_security_definer_view", - "splinter/security/unsupportedRegTypes": "https://supabase.com/docs/guides/database/database-advisors?lint=unsupported_reg_types", - "splinter/unknown/unknown": "https://supabase.com/docs/guides/database/database-advisors", + "splinter/performance/authRlsInitplan": "https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan", + "splinter/performance/duplicateIndex": "https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index", + "splinter/performance/multiplePermissivePolicies": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "splinter/performance/noPrimaryKey": "https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key", + "splinter/performance/tableBloat": "Consider running vacuum full (WARNING: incurs downtime) and tweaking autovacuum settings to reduce bloat.", + "splinter/performance/unindexedForeignKeys": "https://supabase.com/docs/guides/database/database-linter?lint=0001_unindexed_foreign_keys", + "splinter/performance/unusedIndex": "https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index", + "splinter/security/authUsersExposed": "https://supabase.com/docs/guides/database/database-linter?lint=0002_auth_users_exposed", + "splinter/security/extensionInPublic": "https://supabase.com/docs/guides/database/database-linter?lint=0014_extension_in_public", + "splinter/security/extensionVersionsOutdated": "https://supabase.com/docs/guides/database/database-linter?lint=0022_extension_versions_outdated", + "splinter/security/fkeyToAuthUnique": "Drop the foreign key constraint that references the auth schema.", + "splinter/security/foreignTableInApi": "https://supabase.com/docs/guides/database/database-linter?lint=0017_foreign_table_in_api", + "splinter/security/functionSearchPathMutable": "https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable", + "splinter/security/insecureQueueExposedInApi": "https://supabase.com/docs/guides/database/database-linter?lint=0019_insecure_queue_exposed_in_api", + "splinter/security/materializedViewInApi": "https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api", + "splinter/security/policyExistsRlsDisabled": "https://supabase.com/docs/guides/database/database-linter?lint=0007_policy_exists_rls_disabled", + "splinter/security/rlsDisabledInPublic": "https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public", + "splinter/security/rlsEnabledNoPolicy": "https://supabase.com/docs/guides/database/database-linter?lint=0008_rls_enabled_no_policy", + "splinter/security/rlsReferencesUserMetadata": "https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata", + "splinter/security/securityDefinerView": "https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view", + "splinter/security/unsupportedRegTypes": "https://supabase.com/docs/guides/database/database-linter?lint=unsupported_reg_types", // splinter rules end ; // General categories @@ -98,6 +97,5 @@ define_categories! { "splinter", "splinter/performance", "splinter/security", - "splinter/unknown", // Splinter groups end } diff --git a/crates/pgls_splinter/Cargo.toml b/crates/pgls_splinter/Cargo.toml index f07273e2a..4455d4f6e 100644 --- a/crates/pgls_splinter/Cargo.toml +++ b/crates/pgls_splinter/Cargo.toml @@ -11,6 +11,7 @@ repository.workspace = true version = "0.0.0" [dependencies] +pgls_analyse.workspace = true pgls_diagnostics.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/pgls_splinter/src/convert.rs b/crates/pgls_splinter/src/convert.rs index c5d0c7904..81e2e8db5 100644 --- a/crates/pgls_splinter/src/convert.rs +++ b/crates/pgls_splinter/src/convert.rs @@ -103,7 +103,11 @@ fn rule_name_to_category(name: &str, group: &str) -> &'static Category { category!("splinter/security/insecureQueueExposedInApi") } ("security", "fkey_to_auth_unique") => category!("splinter/security/fkeyToAuthUnique"), - _ => category!("splinter/unknown/unknown"), + _ => { + // Log a warning for unknown rules but provide a fallback + eprintln!("Warning: Unknown splinter rule: {}/{}", group, name); + category!("splinter/performance/unindexedForeignKeys") // Fallback to first rule + } } } diff --git a/crates/pgls_splinter/src/lib.rs b/crates/pgls_splinter/src/lib.rs index b32df2329..a42f39439 100644 --- a/crates/pgls_splinter/src/lib.rs +++ b/crates/pgls_splinter/src/lib.rs @@ -1,11 +1,15 @@ mod convert; mod diagnostics; mod query; +pub mod registry; +pub mod rule; +pub mod rules; use sqlx::PgPool; pub use diagnostics::{SplinterAdvices, SplinterDiagnostic}; pub use query::SplinterQueryResult; +pub use rule::SplinterRule; #[derive(Debug)] pub struct SplinterParams<'a> { diff --git a/crates/pgls_splinter/src/registry.rs b/crates/pgls_splinter/src/registry.rs new file mode 100644 index 000000000..64f211b8d --- /dev/null +++ b/crates/pgls_splinter/src/registry.rs @@ -0,0 +1,79 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use pgls_analyse::RegistryVisitor; +#[doc = r" Visit all splinter rules using the visitor pattern"] +#[doc = r" This is called during registry building to collect enabled rules"] +pub fn visit_registry(registry: &mut V) { + registry.record_category::(); +} +#[doc = r" Map rule name (camelCase) to SQL file path"] +#[doc = r" Returns None if rule not found"] +pub fn get_sql_file_path(rule_name: &str) -> Option<&'static str> { + match rule_name { + "authRlsInitplan" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/performance/auth_rls_initplan.sql", + ), + "authUsersExposed" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/auth_users_exposed.sql", + ), + "duplicateIndex" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/performance/duplicate_index.sql", + ), + "extensionInPublic" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/extension_in_public.sql", + ), + "extensionVersionsOutdated" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/extension_versions_outdated.sql", + ), + "fkeyToAuthUnique" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/fkey_to_auth_unique.sql", + ), + "foreignTableInApi" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/foreign_table_in_api.sql", + ), + "functionSearchPathMutable" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/function_search_path_mutable.sql", + ), + "insecureQueueExposedInApi" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/insecure_queue_exposed_in_api.sql", + ), + "materializedViewInApi" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/materialized_view_in_api.sql", + ), + "multiplePermissivePolicies" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/performance/multiple_permissive_policies.sql", + ), + "noPrimaryKey" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/performance/no_primary_key.sql", + ), + "policyExistsRlsDisabled" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/policy_exists_rls_disabled.sql", + ), + "rlsDisabledInPublic" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/rls_disabled_in_public.sql", + ), + "rlsEnabledNoPolicy" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/rls_enabled_no_policy.sql", + ), + "rlsReferencesUserMetadata" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/rls_references_user_metadata.sql", + ), + "securityDefinerView" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/security_definer_view.sql", + ), + "tableBloat" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/performance/table_bloat.sql", + ), + "unindexedForeignKeys" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/performance/unindexed_foreign_keys.sql", + ), + "unsupportedRegTypes" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/unsupported_reg_types.sql", + ), + "unusedIndex" => Some( + "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/performance/unused_index.sql", + ), + _ => None, + } +} diff --git a/crates/pgls_splinter/src/rule.rs b/crates/pgls_splinter/src/rule.rs new file mode 100644 index 000000000..0b1ef1046 --- /dev/null +++ b/crates/pgls_splinter/src/rule.rs @@ -0,0 +1,14 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use pgls_analyse::RuleMeta; +#[doc = r" Trait for splinter (database-level) rules"] +#[doc = r""] +#[doc = r" Splinter rules are different from linter rules:"] +#[doc = r" - They execute SQL queries against the database"] +#[doc = r" - They don't have AST-based execution"] +#[doc = r" - Rule logic is in SQL files, not Rust"] +pub trait SplinterRule: RuleMeta { + #[doc = r" Path to the SQL file containing the rule query"] + fn sql_file_path() -> &'static str; +} diff --git a/crates/pgls_splinter/src/rules/mod.rs b/crates/pgls_splinter/src/rules/mod.rs new file mode 100644 index 000000000..715803f0c --- /dev/null +++ b/crates/pgls_splinter/src/rules/mod.rs @@ -0,0 +1,6 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +pub mod performance; +pub mod security; +::pgls_analyse::declare_category! { pub Splinter { kind : Lint , groups : [self :: performance :: Performance , self :: security :: Security ,] } } diff --git a/crates/pgls_splinter/src/rules/performance/auth_rls_initplan.rs b/crates/pgls_splinter/src/rules/performance/auth_rls_initplan.rs new file mode 100644 index 000000000..fd64ce41f --- /dev/null +++ b/crates/pgls_splinter/src/rules/performance/auth_rls_initplan.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub AuthRlsInitplan { version : "1.0.0" , name : "authRlsInitplan" , severity : pgls_diagnostics :: Severity :: Warning , } } +impl SplinterRule for AuthRlsInitplan { + fn sql_file_path() -> &'static str { + "performance/auth_rls_initplan.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/performance/duplicate_index.rs b/crates/pgls_splinter/src/rules/performance/duplicate_index.rs new file mode 100644 index 000000000..81394496f --- /dev/null +++ b/crates/pgls_splinter/src/rules/performance/duplicate_index.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub DuplicateIndex { version : "1.0.0" , name : "duplicateIndex" , severity : pgls_diagnostics :: Severity :: Warning , } } +impl SplinterRule for DuplicateIndex { + fn sql_file_path() -> &'static str { + "performance/duplicate_index.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/performance/mod.rs b/crates/pgls_splinter/src/rules/performance/mod.rs new file mode 100644 index 000000000..6192266d3 --- /dev/null +++ b/crates/pgls_splinter/src/rules/performance/mod.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +pub mod auth_rls_initplan; +pub mod duplicate_index; +pub mod multiple_permissive_policies; +pub mod no_primary_key; +pub mod table_bloat; +pub mod unindexed_foreign_keys; +pub mod unused_index; +::pgls_analyse::declare_lint_group! { pub Performance { name : "performance" , rules : [self :: auth_rls_initplan :: AuthRlsInitplan , self :: duplicate_index :: DuplicateIndex , self :: multiple_permissive_policies :: MultiplePermissivePolicies , self :: no_primary_key :: NoPrimaryKey , self :: table_bloat :: TableBloat , self :: unindexed_foreign_keys :: UnindexedForeignKeys , self :: unused_index :: UnusedIndex ,] } } diff --git a/crates/pgls_splinter/src/rules/performance/multiple_permissive_policies.rs b/crates/pgls_splinter/src/rules/performance/multiple_permissive_policies.rs new file mode 100644 index 000000000..45d45733f --- /dev/null +++ b/crates/pgls_splinter/src/rules/performance/multiple_permissive_policies.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub MultiplePermissivePolicies { version : "1.0.0" , name : "multiplePermissivePolicies" , severity : pgls_diagnostics :: Severity :: Warning , } } +impl SplinterRule for MultiplePermissivePolicies { + fn sql_file_path() -> &'static str { + "performance/multiple_permissive_policies.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/performance/no_primary_key.rs b/crates/pgls_splinter/src/rules/performance/no_primary_key.rs new file mode 100644 index 000000000..3127f7bf3 --- /dev/null +++ b/crates/pgls_splinter/src/rules/performance/no_primary_key.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub NoPrimaryKey { version : "1.0.0" , name : "noPrimaryKey" , severity : pgls_diagnostics :: Severity :: Information , } } +impl SplinterRule for NoPrimaryKey { + fn sql_file_path() -> &'static str { + "performance/no_primary_key.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/performance/table_bloat.rs b/crates/pgls_splinter/src/rules/performance/table_bloat.rs new file mode 100644 index 000000000..1efe0d43a --- /dev/null +++ b/crates/pgls_splinter/src/rules/performance/table_bloat.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub TableBloat { version : "1.0.0" , name : "tableBloat" , severity : pgls_diagnostics :: Severity :: Information , } } +impl SplinterRule for TableBloat { + fn sql_file_path() -> &'static str { + "performance/table_bloat.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/performance/unindexed_foreign_keys.rs b/crates/pgls_splinter/src/rules/performance/unindexed_foreign_keys.rs new file mode 100644 index 000000000..4e75899b6 --- /dev/null +++ b/crates/pgls_splinter/src/rules/performance/unindexed_foreign_keys.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub UnindexedForeignKeys { version : "1.0.0" , name : "unindexedForeignKeys" , severity : pgls_diagnostics :: Severity :: Information , } } +impl SplinterRule for UnindexedForeignKeys { + fn sql_file_path() -> &'static str { + "performance/unindexed_foreign_keys.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/performance/unused_index.rs b/crates/pgls_splinter/src/rules/performance/unused_index.rs new file mode 100644 index 000000000..afdf18c36 --- /dev/null +++ b/crates/pgls_splinter/src/rules/performance/unused_index.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub UnusedIndex { version : "1.0.0" , name : "unusedIndex" , severity : pgls_diagnostics :: Severity :: Information , } } +impl SplinterRule for UnusedIndex { + fn sql_file_path() -> &'static str { + "performance/unused_index.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/security/auth_users_exposed.rs b/crates/pgls_splinter/src/rules/security/auth_users_exposed.rs new file mode 100644 index 000000000..c159f470d --- /dev/null +++ b/crates/pgls_splinter/src/rules/security/auth_users_exposed.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub AuthUsersExposed { version : "1.0.0" , name : "authUsersExposed" , severity : pgls_diagnostics :: Severity :: Error , } } +impl SplinterRule for AuthUsersExposed { + fn sql_file_path() -> &'static str { + "security/auth_users_exposed.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/security/extension_in_public.rs b/crates/pgls_splinter/src/rules/security/extension_in_public.rs new file mode 100644 index 000000000..e341ddf02 --- /dev/null +++ b/crates/pgls_splinter/src/rules/security/extension_in_public.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub ExtensionInPublic { version : "1.0.0" , name : "extensionInPublic" , severity : pgls_diagnostics :: Severity :: Warning , } } +impl SplinterRule for ExtensionInPublic { + fn sql_file_path() -> &'static str { + "security/extension_in_public.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/security/extension_versions_outdated.rs b/crates/pgls_splinter/src/rules/security/extension_versions_outdated.rs new file mode 100644 index 000000000..3596a6b99 --- /dev/null +++ b/crates/pgls_splinter/src/rules/security/extension_versions_outdated.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub ExtensionVersionsOutdated { version : "1.0.0" , name : "extensionVersionsOutdated" , severity : pgls_diagnostics :: Severity :: Warning , } } +impl SplinterRule for ExtensionVersionsOutdated { + fn sql_file_path() -> &'static str { + "security/extension_versions_outdated.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/security/fkey_to_auth_unique.rs b/crates/pgls_splinter/src/rules/security/fkey_to_auth_unique.rs new file mode 100644 index 000000000..12986b7ef --- /dev/null +++ b/crates/pgls_splinter/src/rules/security/fkey_to_auth_unique.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub FkeyToAuthUnique { version : "1.0.0" , name : "fkeyToAuthUnique" , severity : pgls_diagnostics :: Severity :: Error , } } +impl SplinterRule for FkeyToAuthUnique { + fn sql_file_path() -> &'static str { + "security/fkey_to_auth_unique.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/security/foreign_table_in_api.rs b/crates/pgls_splinter/src/rules/security/foreign_table_in_api.rs new file mode 100644 index 000000000..9bd223d86 --- /dev/null +++ b/crates/pgls_splinter/src/rules/security/foreign_table_in_api.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub ForeignTableInApi { version : "1.0.0" , name : "foreignTableInApi" , severity : pgls_diagnostics :: Severity :: Warning , } } +impl SplinterRule for ForeignTableInApi { + fn sql_file_path() -> &'static str { + "security/foreign_table_in_api.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/security/function_search_path_mutable.rs b/crates/pgls_splinter/src/rules/security/function_search_path_mutable.rs new file mode 100644 index 000000000..0786cffb1 --- /dev/null +++ b/crates/pgls_splinter/src/rules/security/function_search_path_mutable.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub FunctionSearchPathMutable { version : "1.0.0" , name : "functionSearchPathMutable" , severity : pgls_diagnostics :: Severity :: Warning , } } +impl SplinterRule for FunctionSearchPathMutable { + fn sql_file_path() -> &'static str { + "security/function_search_path_mutable.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/security/insecure_queue_exposed_in_api.rs b/crates/pgls_splinter/src/rules/security/insecure_queue_exposed_in_api.rs new file mode 100644 index 000000000..9916e69da --- /dev/null +++ b/crates/pgls_splinter/src/rules/security/insecure_queue_exposed_in_api.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub InsecureQueueExposedInApi { version : "1.0.0" , name : "insecureQueueExposedInApi" , severity : pgls_diagnostics :: Severity :: Error , } } +impl SplinterRule for InsecureQueueExposedInApi { + fn sql_file_path() -> &'static str { + "security/insecure_queue_exposed_in_api.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/security/materialized_view_in_api.rs b/crates/pgls_splinter/src/rules/security/materialized_view_in_api.rs new file mode 100644 index 000000000..8402712cf --- /dev/null +++ b/crates/pgls_splinter/src/rules/security/materialized_view_in_api.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub MaterializedViewInApi { version : "1.0.0" , name : "materializedViewInApi" , severity : pgls_diagnostics :: Severity :: Warning , } } +impl SplinterRule for MaterializedViewInApi { + fn sql_file_path() -> &'static str { + "security/materialized_view_in_api.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/security/mod.rs b/crates/pgls_splinter/src/rules/security/mod.rs new file mode 100644 index 000000000..1d4eb9c0f --- /dev/null +++ b/crates/pgls_splinter/src/rules/security/mod.rs @@ -0,0 +1,18 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +pub mod auth_users_exposed; +pub mod extension_in_public; +pub mod extension_versions_outdated; +pub mod fkey_to_auth_unique; +pub mod foreign_table_in_api; +pub mod function_search_path_mutable; +pub mod insecure_queue_exposed_in_api; +pub mod materialized_view_in_api; +pub mod policy_exists_rls_disabled; +pub mod rls_disabled_in_public; +pub mod rls_enabled_no_policy; +pub mod rls_references_user_metadata; +pub mod security_definer_view; +pub mod unsupported_reg_types; +::pgls_analyse::declare_lint_group! { pub Security { name : "security" , rules : [self :: auth_users_exposed :: AuthUsersExposed , self :: extension_in_public :: ExtensionInPublic , self :: extension_versions_outdated :: ExtensionVersionsOutdated , self :: fkey_to_auth_unique :: FkeyToAuthUnique , self :: foreign_table_in_api :: ForeignTableInApi , self :: function_search_path_mutable :: FunctionSearchPathMutable , self :: insecure_queue_exposed_in_api :: InsecureQueueExposedInApi , self :: materialized_view_in_api :: MaterializedViewInApi , self :: policy_exists_rls_disabled :: PolicyExistsRlsDisabled , self :: rls_disabled_in_public :: RlsDisabledInPublic , self :: rls_enabled_no_policy :: RlsEnabledNoPolicy , self :: rls_references_user_metadata :: RlsReferencesUserMetadata , self :: security_definer_view :: SecurityDefinerView , self :: unsupported_reg_types :: UnsupportedRegTypes ,] } } diff --git a/crates/pgls_splinter/src/rules/security/policy_exists_rls_disabled.rs b/crates/pgls_splinter/src/rules/security/policy_exists_rls_disabled.rs new file mode 100644 index 000000000..eb4bb493d --- /dev/null +++ b/crates/pgls_splinter/src/rules/security/policy_exists_rls_disabled.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub PolicyExistsRlsDisabled { version : "1.0.0" , name : "policyExistsRlsDisabled" , severity : pgls_diagnostics :: Severity :: Error , } } +impl SplinterRule for PolicyExistsRlsDisabled { + fn sql_file_path() -> &'static str { + "security/policy_exists_rls_disabled.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/security/rls_disabled_in_public.rs b/crates/pgls_splinter/src/rules/security/rls_disabled_in_public.rs new file mode 100644 index 000000000..0844e5108 --- /dev/null +++ b/crates/pgls_splinter/src/rules/security/rls_disabled_in_public.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub RlsDisabledInPublic { version : "1.0.0" , name : "rlsDisabledInPublic" , severity : pgls_diagnostics :: Severity :: Error , } } +impl SplinterRule for RlsDisabledInPublic { + fn sql_file_path() -> &'static str { + "security/rls_disabled_in_public.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/security/rls_enabled_no_policy.rs b/crates/pgls_splinter/src/rules/security/rls_enabled_no_policy.rs new file mode 100644 index 000000000..d184ef5dc --- /dev/null +++ b/crates/pgls_splinter/src/rules/security/rls_enabled_no_policy.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub RlsEnabledNoPolicy { version : "1.0.0" , name : "rlsEnabledNoPolicy" , severity : pgls_diagnostics :: Severity :: Information , } } +impl SplinterRule for RlsEnabledNoPolicy { + fn sql_file_path() -> &'static str { + "security/rls_enabled_no_policy.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/security/rls_references_user_metadata.rs b/crates/pgls_splinter/src/rules/security/rls_references_user_metadata.rs new file mode 100644 index 000000000..c93d3aa10 --- /dev/null +++ b/crates/pgls_splinter/src/rules/security/rls_references_user_metadata.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub RlsReferencesUserMetadata { version : "1.0.0" , name : "rlsReferencesUserMetadata" , severity : pgls_diagnostics :: Severity :: Error , } } +impl SplinterRule for RlsReferencesUserMetadata { + fn sql_file_path() -> &'static str { + "security/rls_references_user_metadata.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/security/security_definer_view.rs b/crates/pgls_splinter/src/rules/security/security_definer_view.rs new file mode 100644 index 000000000..b58714b03 --- /dev/null +++ b/crates/pgls_splinter/src/rules/security/security_definer_view.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub SecurityDefinerView { version : "1.0.0" , name : "securityDefinerView" , severity : pgls_diagnostics :: Severity :: Error , } } +impl SplinterRule for SecurityDefinerView { + fn sql_file_path() -> &'static str { + "security/security_definer_view.sql" + } +} diff --git a/crates/pgls_splinter/src/rules/security/unsupported_reg_types.rs b/crates/pgls_splinter/src/rules/security/unsupported_reg_types.rs new file mode 100644 index 000000000..79453c31f --- /dev/null +++ b/crates/pgls_splinter/src/rules/security/unsupported_reg_types.rs @@ -0,0 +1,11 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +use crate::rule::SplinterRule; +use pgls_analyse::RuleMeta; +::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub UnsupportedRegTypes { version : "1.0.0" , name : "unsupportedRegTypes" , severity : pgls_diagnostics :: Severity :: Warning , } } +impl SplinterRule for UnsupportedRegTypes { + fn sql_file_path() -> &'static str { + "security/unsupported_reg_types.sql" + } +} diff --git a/crates/pgls_splinter/vendor/performance/auth_rls_initplan.sql b/crates/pgls_splinter/vendor/performance/auth_rls_initplan.sql new file mode 100644 index 000000000..dd2b7edd5 --- /dev/null +++ b/crates/pgls_splinter/vendor/performance/auth_rls_initplan.sql @@ -0,0 +1,107 @@ +-- meta: name = authRlsInitplan +-- meta: title = Auth RLS Initialization Plan +-- meta: severity = WARN +-- meta: category = PERFORMANCE +-- meta: description = Detects if calls to \`current_setting()\` and \`auth.()\` in RLS policies are being unnecessarily re-evaluated for each row +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan + +( +with policies as ( + select + nsp.nspname as schema_name, + pb.tablename as table_name, + pc.relrowsecurity as is_rls_active, + polname as policy_name, + polpermissive as is_permissive, -- if not, then restrictive + (select array_agg(r::regrole) from unnest(polroles) as x(r)) as roles, + case polcmd + when 'r' then 'SELECT' + when 'a' then 'INSERT' + when 'w' then 'UPDATE' + when 'd' then 'DELETE' + when '*' then 'ALL' + end as command, + qual, + with_check + from + pg_catalog.pg_policy pa + join pg_catalog.pg_class pc + on pa.polrelid = pc.oid + join pg_catalog.pg_namespace nsp + on pc.relnamespace = nsp.oid + join pg_catalog.pg_policies pb + on pc.relname = pb.tablename + and nsp.nspname = pb.schemaname + and pa.polname = pb.policyname +) +select + 'auth_rls_initplan' as "name!", + 'Auth RLS Initialization Plan' as "title!", + 'WARN' as "level!", + 'EXTERNAL' as "facing!", + array['PERFORMANCE'] as "categories!", + 'Detects if calls to \`current_setting()\` and \`auth.()\` in RLS policies are being unnecessarily re-evaluated for each row' as "description!", + format( + 'Table \`%s.%s\` has a row level security policy \`%s\` that re-evaluates current_setting() or auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing \`auth.()\` with \`(select auth.())\`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info.', + schema_name, + table_name, + policy_name + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan' as "remediation!", + jsonb_build_object( + 'schema', schema_name, + 'name', table_name, + 'type', 'table' + ) as "metadata!", + format('auth_rls_init_plan_%s_%s_%s', schema_name, table_name, policy_name) as "cache_key!" +from + policies +where + is_rls_active + -- NOTE: does not include realtime in support of monitoring policies on realtime.messages + and schema_name not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + ) + and ( + -- Example: auth.uid() + ( + qual like '%auth.uid()%' + and lower(qual) not like '%select auth.uid()%' + ) + or ( + qual like '%auth.jwt()%' + and lower(qual) not like '%select auth.jwt()%' + ) + or ( + qual like '%auth.role()%' + and lower(qual) not like '%select auth.role()%' + ) + or ( + qual like '%auth.email()%' + and lower(qual) not like '%select auth.email()%' + ) + or ( + qual like '%current\_setting(%)%' + and lower(qual) not like '%select current\_setting(%)%' + ) + or ( + with_check like '%auth.uid()%' + and lower(with_check) not like '%select auth.uid()%' + ) + or ( + with_check like '%auth.jwt()%' + and lower(with_check) not like '%select auth.jwt()%' + ) + or ( + with_check like '%auth.role()%' + and lower(with_check) not like '%select auth.role()%' + ) + or ( + with_check like '%auth.email()%' + and lower(with_check) not like '%select auth.email()%' + ) + or ( + with_check like '%current\_setting(%)%' + and lower(with_check) not like '%select current\_setting(%)%' + ) + )) diff --git a/crates/pgls_splinter/vendor/performance/duplicate_index.sql b/crates/pgls_splinter/vendor/performance/duplicate_index.sql new file mode 100644 index 000000000..aa469dbf1 --- /dev/null +++ b/crates/pgls_splinter/vendor/performance/duplicate_index.sql @@ -0,0 +1,61 @@ +-- meta: name = duplicateIndex +-- meta: title = Duplicate Index +-- meta: severity = WARN +-- meta: category = PERFORMANCE +-- meta: description = Detects cases where two ore more identical indexes exist. +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index + +( +select + 'duplicate_index' as "name!", + 'Duplicate Index' as "title!", + 'WARN' as "level!", + 'EXTERNAL' as "facing!", + array['PERFORMANCE'] as "categories!", + 'Detects cases where two ore more identical indexes exist.' as "description!", + format( + 'Table \`%s.%s\` has identical indexes %s. Drop all except one of them', + n.nspname, + c.relname, + array_agg(pi.indexname order by pi.indexname) + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index' as "remediation!", + jsonb_build_object( + 'schema', n.nspname, + 'name', c.relname, + 'type', case + when c.relkind = 'r' then 'table' + when c.relkind = 'm' then 'materialized view' + else 'ERROR' + end, + 'indexes', array_agg(pi.indexname order by pi.indexname) + ) as "metadata!", + format( + 'duplicate_index_%s_%s_%s', + n.nspname, + c.relname, + array_agg(pi.indexname order by pi.indexname) + ) as "cache_key!" +from + pg_catalog.pg_indexes pi + join pg_catalog.pg_namespace n + on n.nspname = pi.schemaname + join pg_catalog.pg_class c + on pi.tablename = c.relname + and n.oid = c.relnamespace + left join pg_catalog.pg_depend dep + on c.oid = dep.objid + and dep.deptype = 'e' +where + c.relkind in ('r', 'm') -- tables and materialized views + and n.nspname not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + ) + and dep.objid is null -- exclude tables owned by extensions +group by + n.nspname, + c.relkind, + c.relname, + replace(pi.indexdef, pi.indexname, '') +having + count(*) > 1) diff --git a/crates/pgls_splinter/vendor/performance/multiple_permissive_policies.sql b/crates/pgls_splinter/vendor/performance/multiple_permissive_policies.sql new file mode 100644 index 000000000..ed933a136 --- /dev/null +++ b/crates/pgls_splinter/vendor/performance/multiple_permissive_policies.sql @@ -0,0 +1,79 @@ +-- meta: name = multiplePermissivePolicies +-- meta: title = Multiple Permissive Policies +-- meta: severity = WARN +-- meta: category = PERFORMANCE +-- meta: description = Detects if multiple permissive row level security policies are present on a table for the same \`role\` and \`action\` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query. +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies + +( +select + 'multiple_permissive_policies' as "name!", + 'Multiple Permissive Policies' as "title!", + 'WARN' as "level!", + 'EXTERNAL' as "facing!", + array['PERFORMANCE'] as "categories!", + 'Detects if multiple permissive row level security policies are present on a table for the same \`role\` and \`action\` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.' as "description!", + format( + 'Table \`%s.%s\` has multiple permissive policies for role \`%s\` for action \`%s\`. Policies include \`%s\`', + n.nspname, + c.relname, + r.rolname, + act.cmd, + array_agg(p.polname order by p.polname) + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies' as "remediation!", + jsonb_build_object( + 'schema', n.nspname, + 'name', c.relname, + 'type', 'table' + ) as "metadata!", + format( + 'multiple_permissive_policies_%s_%s_%s_%s', + n.nspname, + c.relname, + r.rolname, + act.cmd + ) as "cache_key!" +from + pg_catalog.pg_policy p + join pg_catalog.pg_class c + on p.polrelid = c.oid + join pg_catalog.pg_namespace n + on c.relnamespace = n.oid + join pg_catalog.pg_roles r + on p.polroles @> array[r.oid] + or p.polroles = array[0::oid] + left join pg_catalog.pg_depend dep + on c.oid = dep.objid + and dep.deptype = 'e', + lateral ( + select x.cmd + from unnest(( + select + case p.polcmd + when 'r' then array['SELECT'] + when 'a' then array['INSERT'] + when 'w' then array['UPDATE'] + when 'd' then array['DELETE'] + when '*' then array['SELECT', 'INSERT', 'UPDATE', 'DELETE'] + else array['ERROR'] + end as actions + )) x(cmd) + ) act(cmd) +where + c.relkind = 'r' -- regular tables + and p.polpermissive -- policy is permissive + and n.nspname not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + ) + and r.rolname not like 'pg_%' + and r.rolname not like 'supabase%admin' + and not r.rolbypassrls + and dep.objid is null -- exclude tables owned by extensions +group by + n.nspname, + c.relname, + r.rolname, + act.cmd +having + count(1) > 1) diff --git a/crates/pgls_splinter/vendor/performance/no_primary_key.sql b/crates/pgls_splinter/vendor/performance/no_primary_key.sql new file mode 100644 index 000000000..4b35000ca --- /dev/null +++ b/crates/pgls_splinter/vendor/performance/no_primary_key.sql @@ -0,0 +1,52 @@ +-- meta: name = noPrimaryKey +-- meta: title = No Primary Key +-- meta: severity = INFO +-- meta: category = PERFORMANCE +-- meta: description = Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale. +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key + +( +select + 'no_primary_key' as "name!", + 'No Primary Key' as "title!", + 'INFO' as "level!", + 'EXTERNAL' as "facing!", + array['PERFORMANCE'] as "categories!", + 'Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale.' as "description!", + format( + 'Table \`%s.%s\` does not have a primary key', + pgns.nspname, + pgc.relname + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key' as "remediation!", + jsonb_build_object( + 'schema', pgns.nspname, + 'name', pgc.relname, + 'type', 'table' + ) as "metadata!", + format( + 'no_primary_key_%s_%s', + pgns.nspname, + pgc.relname + ) as "cache_key!" +from + pg_catalog.pg_class pgc + join pg_catalog.pg_namespace pgns + on pgns.oid = pgc.relnamespace + left join pg_catalog.pg_index pgi + on pgi.indrelid = pgc.oid + left join pg_catalog.pg_depend dep + on pgc.oid = dep.objid + and dep.deptype = 'e' +where + pgc.relkind = 'r' -- regular tables + and pgns.nspname not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + ) + and dep.objid is null -- exclude tables owned by extensions +group by + pgc.oid, + pgns.nspname, + pgc.relname +having + max(coalesce(pgi.indisprimary, false)::int) = 0) diff --git a/crates/pgls_splinter/vendor/performance/table_bloat.sql b/crates/pgls_splinter/vendor/performance/table_bloat.sql new file mode 100644 index 000000000..465e2047a --- /dev/null +++ b/crates/pgls_splinter/vendor/performance/table_bloat.sql @@ -0,0 +1,105 @@ +-- meta: name = tableBloat +-- meta: title = Table Bloat +-- meta: severity = INFO +-- meta: category = PERFORMANCE +-- meta: description = Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster. +-- meta: remediation = Consider running vacuum full (WARNING: incurs downtime) and tweaking autovacuum settings to reduce bloat. + +( +with constants as ( + select current_setting('block_size')::numeric as bs, 23 as hdr, 4 as ma +), + +bloat_info as ( + select + ma, + bs, + schemaname, + tablename, + (datawidth + (hdr + ma - (case when hdr % ma = 0 then ma else hdr % ma end)))::numeric as datahdr, + (maxfracsum * (nullhdr + ma - (case when nullhdr % ma = 0 then ma else nullhdr % ma end))) as nullhdr2 + from ( + select + schemaname, + tablename, + hdr, + ma, + bs, + sum((1 - null_frac) * avg_width) as datawidth, + max(null_frac) as maxfracsum, + hdr + ( + select 1 + count(*) / 8 + from pg_stats s2 + where + null_frac <> 0 + and s2.schemaname = s.schemaname + and s2.tablename = s.tablename + ) as nullhdr + from pg_stats s, constants + group by 1, 2, 3, 4, 5 + ) as foo +), + +table_bloat as ( + select + schemaname, + tablename, + cc.relpages, + bs, + ceil((cc.reltuples * ((datahdr + ma - + (case when datahdr % ma = 0 then ma else datahdr % ma end)) + nullhdr2 + 4)) / (bs - 20::float)) as otta + from + bloat_info + join pg_class cc + on cc.relname = bloat_info.tablename + join pg_namespace nn + on cc.relnamespace = nn.oid + and nn.nspname = bloat_info.schemaname + and nn.nspname <> 'information_schema' + where + cc.relkind = 'r' + and cc.relam = (select oid from pg_am where amname = 'heap') +), + +bloat_data as ( + select + 'table' as type, + schemaname, + tablename as object_name, + round(case when otta = 0 then 0.0 else table_bloat.relpages / otta::numeric end, 1) as bloat, + case when relpages < otta then 0 else (bs * (table_bloat.relpages - otta)::bigint)::bigint end as raw_waste + from + table_bloat +) + +select + 'table_bloat' as "name!", + 'Table Bloat' as "title!", + 'INFO' as "level!", + 'EXTERNAL' as "facing!", + array['PERFORMANCE'] as "categories!", + 'Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster.' as "description!", + format( + 'Table `%s`.`%s` has excessive bloat', + bloat_data.schemaname, + bloat_data.object_name + ) as "detail!", + 'Consider running vacuum full (WARNING: incurs downtime) and tweaking autovacuum settings to reduce bloat.' as "remediation!", + jsonb_build_object( + 'schema', bloat_data.schemaname, + 'name', bloat_data.object_name, + 'type', bloat_data.type + ) as "metadata!", + format( + 'table_bloat_%s_%s', + bloat_data.schemaname, + bloat_data.object_name + ) as "cache_key!" +from + bloat_data +where + bloat > 70.0 + and raw_waste > (20 * 1024 * 1024) -- filter for waste > 200 MB +order by + schemaname, + object_name) diff --git a/crates/pgls_splinter/vendor/performance/unindexed_foreign_keys.sql b/crates/pgls_splinter/vendor/performance/unindexed_foreign_keys.sql new file mode 100644 index 000000000..e98a80dbb --- /dev/null +++ b/crates/pgls_splinter/vendor/performance/unindexed_foreign_keys.sql @@ -0,0 +1,78 @@ +-- meta: name = unindexedForeignKeys +-- meta: title = Unindexed foreign keys +-- meta: severity = INFO +-- meta: category = PERFORMANCE +-- meta: description = Identifies foreign key constraints without a covering index, which can impact database performance. +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0001_unindexed_foreign_keys + +with foreign_keys as ( + select + cl.relnamespace::regnamespace::text as schema_name, + cl.relname as table_name, + cl.oid as table_oid, + ct.conname as fkey_name, + ct.conkey as col_attnums + from + pg_catalog.pg_constraint ct + join pg_catalog.pg_class cl -- fkey owning table + on ct.conrelid = cl.oid + left join pg_catalog.pg_depend d + on d.objid = cl.oid + and d.deptype = 'e' + where + ct.contype = 'f' -- foreign key constraints + and d.objid is null -- exclude tables that are dependencies of extensions + and cl.relnamespace::regnamespace::text not in ( + 'pg_catalog', 'information_schema', 'auth', 'storage', 'vault', 'extensions' + ) +), +index_ as ( + select + pi.indrelid as table_oid, + indexrelid::regclass as index_, + string_to_array(indkey::text, ' ')::smallint[] as col_attnums + from + pg_catalog.pg_index pi + where + indisvalid +) +select + 'unindexed_foreign_keys' as "name!", + 'Unindexed foreign keys' as "title!", + 'INFO' as "level!", + 'EXTERNAL' as "facing!", + array['PERFORMANCE'] as "categories!", + 'Identifies foreign key constraints without a covering index, which can impact database performance.' as "description!", + format( + 'Table `%s.%s` has a foreign key `%s` without a covering index. This can lead to suboptimal query performance.', + fk.schema_name, + fk.table_name, + fk.fkey_name + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0001_unindexed_foreign_keys' as "remediation!", + jsonb_build_object( + 'schema', fk.schema_name, + 'name', fk.table_name, + 'type', 'table', + 'fkey_name', fk.fkey_name, + 'fkey_columns', fk.col_attnums + ) as "metadata!", + format('unindexed_foreign_keys_%s_%s_%s', fk.schema_name, fk.table_name, fk.fkey_name) as "cache_key!" +from + foreign_keys fk + left join index_ idx + on fk.table_oid = idx.table_oid + and fk.col_attnums = idx.col_attnums[1:array_length(fk.col_attnums, 1)] + left join pg_catalog.pg_depend dep + on idx.table_oid = dep.objid + and dep.deptype = 'e' +where + idx.index_ is null + and fk.schema_name not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + ) + and dep.objid is null -- exclude tables owned by extensions +order by + fk.schema_name, + fk.table_name, + fk.fkey_name diff --git a/crates/pgls_splinter/vendor/performance/unused_index.sql b/crates/pgls_splinter/vendor/performance/unused_index.sql new file mode 100644 index 000000000..15fa0a82f --- /dev/null +++ b/crates/pgls_splinter/vendor/performance/unused_index.sql @@ -0,0 +1,49 @@ +-- meta: name = unusedIndex +-- meta: title = Unused Index +-- meta: severity = INFO +-- meta: category = PERFORMANCE +-- meta: description = Detects if an index has never been used and may be a candidate for removal. +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index + +( +select + 'unused_index' as "name!", + 'Unused Index' as "title!", + 'INFO' as "level!", + 'EXTERNAL' as "facing!", + array['PERFORMANCE'] as "categories!", + 'Detects if an index has never been used and may be a candidate for removal.' as "description!", + format( + 'Index \`%s\` on table \`%s.%s\` has not been used', + psui.indexrelname, + psui.schemaname, + psui.relname + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index' as "remediation!", + jsonb_build_object( + 'schema', psui.schemaname, + 'name', psui.relname, + 'type', 'table' + ) as "metadata!", + format( + 'unused_index_%s_%s_%s', + psui.schemaname, + psui.relname, + psui.indexrelname + ) as "cache_key!" + +from + pg_catalog.pg_stat_user_indexes psui + join pg_catalog.pg_index pi + on psui.indexrelid = pi.indexrelid + left join pg_catalog.pg_depend dep + on psui.relid = dep.objid + and dep.deptype = 'e' +where + psui.idx_scan = 0 + and not pi.indisunique + and not pi.indisprimary + and dep.objid is null -- exclude tables owned by extensions + and psui.schemaname not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + )) diff --git a/crates/pgls_splinter/vendor/security/auth_users_exposed.sql b/crates/pgls_splinter/vendor/security/auth_users_exposed.sql new file mode 100644 index 000000000..e73592b8c --- /dev/null +++ b/crates/pgls_splinter/vendor/security/auth_users_exposed.sql @@ -0,0 +1,95 @@ +-- meta: name = authUsersExposed +-- meta: title = Exposed Auth Users +-- meta: severity = ERROR +-- meta: category = SECURITY +-- meta: description = Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security. +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0002_auth_users_exposed + +( +select + 'auth_users_exposed' as "name!", + 'Exposed Auth Users' as "title!", + 'ERROR' as "level!", + 'EXTERNAL' as "facing!", + array['SECURITY'] as "categories!", + 'Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security.' as "description!", + format( + 'View/Materialized View "%s" in the public schema may expose \`auth.users\` data to anon or authenticated roles.', + c.relname + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0002_auth_users_exposed' as "remediation!", + jsonb_build_object( + 'schema', n.nspname, + 'name', c.relname, + 'type', 'view', + 'exposed_to', array_remove(array_agg(DISTINCT case when pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') then 'anon' when pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') then 'authenticated' end), null) + ) as "metadata!", + format('auth_users_exposed_%s_%s', n.nspname, c.relname) as "cache_key!" +from + -- Identify the oid for auth.users + pg_catalog.pg_class auth_users_pg_class + join pg_catalog.pg_namespace auth_users_pg_namespace + on auth_users_pg_class.relnamespace = auth_users_pg_namespace.oid + and auth_users_pg_class.relname = 'users' + and auth_users_pg_namespace.nspname = 'auth' + -- Depends on auth.users + join pg_catalog.pg_depend d + on d.refobjid = auth_users_pg_class.oid + join pg_catalog.pg_rewrite r + on r.oid = d.objid + join pg_catalog.pg_class c + on c.oid = r.ev_class + join pg_catalog.pg_namespace n + on n.oid = c.relnamespace + join pg_catalog.pg_class pg_class_auth_users + on d.refobjid = pg_class_auth_users.oid +where + d.deptype = 'n' + and ( + pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') + or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') + ) + and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) + -- Exclude self + and c.relname <> '0002_auth_users_exposed' + -- There are 3 insecure configurations + and + ( + -- Materialized views don't support RLS so this is insecure by default + (c.relkind in ('m')) -- m for materialized view + or + -- Standard View, accessible to anon or authenticated that is security_definer + ( + c.relkind = 'v' -- v for view + -- Exclude security invoker views + and not ( + lower(coalesce(c.reloptions::text,'{}'))::text[] + && array[ + 'security_invoker=1', + 'security_invoker=true', + 'security_invoker=yes', + 'security_invoker=on' + ] + ) + ) + or + -- Standard View, security invoker, but no RLS enabled on auth.users + ( + c.relkind in ('v') -- v for view + -- is security invoker + and ( + lower(coalesce(c.reloptions::text,'{}'))::text[] + && array[ + 'security_invoker=1', + 'security_invoker=true', + 'security_invoker=yes', + 'security_invoker=on' + ] + ) + and not pg_class_auth_users.relrowsecurity + ) + ) +group by + n.nspname, + c.relname, + c.oid) diff --git a/crates/pgls_splinter/vendor/security/extension_in_public.sql b/crates/pgls_splinter/vendor/security/extension_in_public.sql new file mode 100644 index 000000000..ce2b83101 --- /dev/null +++ b/crates/pgls_splinter/vendor/security/extension_in_public.sql @@ -0,0 +1,40 @@ +-- meta: name = extensionInPublic +-- meta: title = Extension in Public +-- meta: severity = WARN +-- meta: category = SECURITY +-- meta: description = Detects extensions installed in the \`public\` schema. +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0014_extension_in_public + +( +select + 'extension_in_public' as "name!", + 'Extension in Public' as "title!", + 'WARN' as "level!", + 'EXTERNAL' as "facing!", + array['SECURITY'] as "categories!", + 'Detects extensions installed in the \`public\` schema.' as "description!", + format( + 'Extension \`%s\` is installed in the public schema. Move it to another schema.', + pe.extname + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0014_extension_in_public' as "remediation!", + jsonb_build_object( + 'schema', pe.extnamespace::regnamespace, + 'name', pe.extname, + 'type', 'extension' + ) as "metadata!", + format( + 'extension_in_public_%s', + pe.extname + ) as "cache_key!" +from + pg_catalog.pg_extension pe +where + -- plpgsql is installed by default in public and outside user control + -- confirmed safe + pe.extname not in ('plpgsql') + -- Scoping this to public is not optimal. Ideally we would use the postgres + -- search path. That currently isn't available via SQL. In other lints + -- we have used has_schema_privilege('anon', 'extensions', 'USAGE') but that + -- is not appropriate here as it would evaluate true for the extensions schema + and pe.extnamespace::regnamespace::text = 'public') diff --git a/crates/pgls_splinter/vendor/security/extension_versions_outdated.sql b/crates/pgls_splinter/vendor/security/extension_versions_outdated.sql new file mode 100644 index 000000000..ad83574d2 --- /dev/null +++ b/crates/pgls_splinter/vendor/security/extension_versions_outdated.sql @@ -0,0 +1,45 @@ +-- meta: name = extensionVersionsOutdated +-- meta: title = Extension Versions Outdated +-- meta: severity = WARN +-- meta: category = SECURITY +-- meta: description = Detects extensions that are not using the default (recommended) version. +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0022_extension_versions_outdated + +( +select + 'extension_versions_outdated' as "name!", + 'Extension Versions Outdated' as "title!", + 'WARN' as "level!", + 'EXTERNAL' as "facing!", + array['SECURITY'] as "categories!", + 'Detects extensions that are not using the default (recommended) version.' as "description!", + format( + 'Extension `%s` is using version `%s` but version `%s` is available. Using outdated extension versions may expose the database to security vulnerabilities.', + ext.name, + ext.installed_version, + ext.default_version + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0022_extension_versions_outdated' as "remediation!", + jsonb_build_object( + 'extension_name', ext.name, + 'installed_version', ext.installed_version, + 'default_version', ext.default_version + ) as "metadata!", + format( + 'extension_versions_outdated_%s_%s', + ext.name, + ext.installed_version + ) as "cache_key!" +from + pg_catalog.pg_available_extensions ext +join + -- ignore versions not in pg_available_extension_versions + -- e.g. residue of pg_upgrade + pg_catalog.pg_available_extension_versions extv + on extv.name = ext.name and extv.installed +where + ext.installed_version is not null + and ext.default_version is not null + and ext.installed_version != ext.default_version +order by + ext.name) diff --git a/crates/pgls_splinter/vendor/security/fkey_to_auth_unique.sql b/crates/pgls_splinter/vendor/security/fkey_to_auth_unique.sql new file mode 100644 index 000000000..183f9ca9a --- /dev/null +++ b/crates/pgls_splinter/vendor/security/fkey_to_auth_unique.sql @@ -0,0 +1,49 @@ +-- meta: name = fkeyToAuthUnique +-- meta: title = Foreign Key to Auth Unique Constraint +-- meta: severity = ERROR +-- meta: category = SECURITY +-- meta: description = Detects user defined foreign keys to unique constraints in the auth schema. +-- meta: remediation = Drop the foreign key constraint that references the auth schema. + +( +select + 'fkey_to_auth_unique' as "name!", + 'Foreign Key to Auth Unique Constraint' as "title!", + 'ERROR' as "level!", + 'EXTERNAL' as "facing!", + array['SECURITY'] as "categories!", + 'Detects user defined foreign keys to unique constraints in the auth schema.' as "description!", + format( + 'Table `%s`.`%s` has a foreign key `%s` referencing an auth unique constraint', + n.nspname, -- referencing schema + c_rel.relname, -- referencing table + c.conname -- fkey name + ) as "detail!", + 'Drop the foreign key constraint that references the auth schema.' as "remediation!", + jsonb_build_object( + 'schema', n.nspname, + 'name', c_rel.relname, + 'foreign_key', c.conname + ) as "metadata!", + format( + 'fkey_to_auth_unique_%s_%s_%s', + n.nspname, -- referencing schema + c_rel.relname, -- referencing table + c.conname + ) as "cache_key!" +from + pg_catalog.pg_constraint c + join pg_catalog.pg_class c_rel + on c.conrelid = c_rel.oid + join pg_catalog.pg_namespace n + on c_rel.relnamespace = n.oid + join pg_catalog.pg_class ref_rel + on c.confrelid = ref_rel.oid + join pg_catalog.pg_namespace cn + on ref_rel.relnamespace = cn.oid + join pg_catalog.pg_index i + on c.conindid = i.indexrelid +where c.contype = 'f' + and cn.nspname = 'auth' + and i.indisunique + and not i.indisprimary) diff --git a/crates/pgls_splinter/vendor/security/foreign_table_in_api.sql b/crates/pgls_splinter/vendor/security/foreign_table_in_api.sql new file mode 100644 index 000000000..9e205606e --- /dev/null +++ b/crates/pgls_splinter/vendor/security/foreign_table_in_api.sql @@ -0,0 +1,49 @@ +-- meta: name = foreignTableInApi +-- meta: title = Foreign Table in API +-- meta: severity = WARN +-- meta: category = SECURITY +-- meta: description = Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies. +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0017_foreign_table_in_api + +( +select + 'foreign_table_in_api' as "name!", + 'Foreign Table in API' as "title!", + 'WARN' as "level!", + 'EXTERNAL' as "facing!", + array['SECURITY'] as "categories!", + 'Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies.' as "description!", + format( + 'Foreign table \`%s.%s\` is accessible over APIs', + n.nspname, + c.relname + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0017_foreign_table_in_api' as "remediation!", + jsonb_build_object( + 'schema', n.nspname, + 'name', c.relname, + 'type', 'foreign table' + ) as "metadata!", + format( + 'foreign_table_in_api_%s_%s', + n.nspname, + c.relname + ) as "cache_key!" +from + pg_catalog.pg_class c + join pg_catalog.pg_namespace n + on n.oid = c.relnamespace + left join pg_catalog.pg_depend dep + on c.oid = dep.objid + and dep.deptype = 'e' +where + c.relkind = 'f' + and ( + pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') + or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') + ) + and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) + and n.nspname not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + ) + and dep.objid is null) diff --git a/crates/pgls_splinter/vendor/security/function_search_path_mutable.sql b/crates/pgls_splinter/vendor/security/function_search_path_mutable.sql new file mode 100644 index 000000000..efe47ffc7 --- /dev/null +++ b/crates/pgls_splinter/vendor/security/function_search_path_mutable.sql @@ -0,0 +1,50 @@ +-- meta: name = functionSearchPathMutable +-- meta: title = Function Search Path Mutable +-- meta: severity = WARN +-- meta: category = SECURITY +-- meta: description = Detects functions where the search_path parameter is not set. +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable + +( +select + 'function_search_path_mutable' as "name!", + 'Function Search Path Mutable' as "title!", + 'WARN' as "level!", + 'EXTERNAL' as "facing!", + array['SECURITY'] as "categories!", + 'Detects functions where the search_path parameter is not set.' as "description!", + format( + 'Function \`%s.%s\` has a role mutable search_path', + n.nspname, + p.proname + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable' as "remediation!", + jsonb_build_object( + 'schema', n.nspname, + 'name', p.proname, + 'type', 'function' + ) as "metadata!", + format( + 'function_search_path_mutable_%s_%s_%s', + n.nspname, + p.proname, + md5(p.prosrc) -- required when function is polymorphic + ) as "cache_key!" +from + pg_catalog.pg_proc p + join pg_catalog.pg_namespace n + on p.pronamespace = n.oid + left join pg_catalog.pg_depend dep + on p.oid = dep.objid + and dep.deptype = 'e' +where + n.nspname not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + ) + and dep.objid is null -- exclude functions owned by extensions + -- Search path not set + and not exists ( + select 1 + from unnest(coalesce(p.proconfig, '{}')) as config + where config like 'search_path=%' + )) diff --git a/crates/pgls_splinter/vendor/security/insecure_queue_exposed_in_api.sql b/crates/pgls_splinter/vendor/security/insecure_queue_exposed_in_api.sql new file mode 100644 index 000000000..2a35f5e42 --- /dev/null +++ b/crates/pgls_splinter/vendor/security/insecure_queue_exposed_in_api.sql @@ -0,0 +1,46 @@ +-- meta: name = insecureQueueExposedInApi +-- meta: title = Insecure Queue Exposed in API +-- meta: severity = ERROR +-- meta: category = SECURITY +-- meta: description = Detects cases where an insecure Queue is exposed over Data APIs +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0019_insecure_queue_exposed_in_api + +( +select + 'insecure_queue_exposed_in_api' as "name!", + 'Insecure Queue Exposed in API' as "title!", + 'ERROR' as "level!", + 'EXTERNAL' as "facing!", + array['SECURITY'] as "categories!", + 'Detects cases where an insecure Queue is exposed over Data APIs' as "description!", + format( + 'Table \`%s.%s\` is public, but RLS has not been enabled.', + n.nspname, + c.relname + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0019_insecure_queue_exposed_in_api' as "remediation!", + jsonb_build_object( + 'schema', n.nspname, + 'name', c.relname, + 'type', 'table' + ) as "metadata!", + format( + 'rls_disabled_in_public_%s_%s', + n.nspname, + c.relname + ) as "cache_key!" +from + pg_catalog.pg_class c + join pg_catalog.pg_namespace n + on c.relnamespace = n.oid +where + c.relkind in ('r', 'I') -- regular or partitioned tables + and not c.relrowsecurity -- RLS is disabled + and ( + pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') + or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') + ) + and n.nspname = 'pgmq' -- tables in the pgmq schema + and c.relname like 'q_%' -- only queue tables + -- Constant requirements + and 'pgmq_public' = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))) diff --git a/crates/pgls_splinter/vendor/security/materialized_view_in_api.sql b/crates/pgls_splinter/vendor/security/materialized_view_in_api.sql new file mode 100644 index 000000000..9c1546614 --- /dev/null +++ b/crates/pgls_splinter/vendor/security/materialized_view_in_api.sql @@ -0,0 +1,49 @@ +-- meta: name = materializedViewInApi +-- meta: title = Materialized View in API +-- meta: severity = WARN +-- meta: category = SECURITY +-- meta: description = Detects materialized views that are accessible over the Data APIs. +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api + +( +select + 'materialized_view_in_api' as "name!", + 'Materialized View in API' as "title!", + 'WARN' as "level!", + 'EXTERNAL' as "facing!", + array['SECURITY'] as "categories!", + 'Detects materialized views that are accessible over the Data APIs.' as "description!", + format( + 'Materialized view \`%s.%s\` is selectable by anon or authenticated roles', + n.nspname, + c.relname + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api' as "remediation!", + jsonb_build_object( + 'schema', n.nspname, + 'name', c.relname, + 'type', 'materialized view' + ) as "metadata!", + format( + 'materialized_view_in_api_%s_%s', + n.nspname, + c.relname + ) as "cache_key!" +from + pg_catalog.pg_class c + join pg_catalog.pg_namespace n + on n.oid = c.relnamespace + left join pg_catalog.pg_depend dep + on c.oid = dep.objid + and dep.deptype = 'e' +where + c.relkind = 'm' + and ( + pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') + or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') + ) + and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) + and n.nspname not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + ) + and dep.objid is null) diff --git a/crates/pgls_splinter/vendor/security/policy_exists_rls_disabled.sql b/crates/pgls_splinter/vendor/security/policy_exists_rls_disabled.sql new file mode 100644 index 000000000..9a1ae1256 --- /dev/null +++ b/crates/pgls_splinter/vendor/security/policy_exists_rls_disabled.sql @@ -0,0 +1,52 @@ +-- meta: name = policyExistsRlsDisabled +-- meta: title = Policy Exists RLS Disabled +-- meta: severity = ERROR +-- meta: category = SECURITY +-- meta: description = Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table. +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0007_policy_exists_rls_disabled + +( +select + 'policy_exists_rls_disabled' as "name!", + 'Policy Exists RLS Disabled' as "title!", + 'ERROR' as "level!", + 'EXTERNAL' as "facing!", + array['SECURITY'] as "categories!", + 'Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table.' as "description!", + format( + 'Table \`%s.%s\` has RLS policies but RLS is not enabled on the table. Policies include %s.', + n.nspname, + c.relname, + array_agg(p.polname order by p.polname) + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0007_policy_exists_rls_disabled' as "remediation!", + jsonb_build_object( + 'schema', n.nspname, + 'name', c.relname, + 'type', 'table' + ) as "metadata!", + format( + 'policy_exists_rls_disabled_%s_%s', + n.nspname, + c.relname + ) as "cache_key!" +from + pg_catalog.pg_policy p + join pg_catalog.pg_class c + on p.polrelid = c.oid + join pg_catalog.pg_namespace n + on c.relnamespace = n.oid + left join pg_catalog.pg_depend dep + on c.oid = dep.objid + and dep.deptype = 'e' +where + c.relkind = 'r' -- regular tables + and n.nspname not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + ) + -- RLS is disabled + and not c.relrowsecurity + and dep.objid is null -- exclude tables owned by extensions +group by + n.nspname, + c.relname) diff --git a/crates/pgls_splinter/vendor/security/rls_disabled_in_public.sql b/crates/pgls_splinter/vendor/security/rls_disabled_in_public.sql new file mode 100644 index 000000000..ae9fc3162 --- /dev/null +++ b/crates/pgls_splinter/vendor/security/rls_disabled_in_public.sql @@ -0,0 +1,47 @@ +-- meta: name = rlsDisabledInPublic +-- meta: title = RLS Disabled in Public +-- meta: severity = ERROR +-- meta: category = SECURITY +-- meta: description = Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public + +( +select + 'rls_disabled_in_public' as "name!", + 'RLS Disabled in Public' as "title!", + 'ERROR' as "level!", + 'EXTERNAL' as "facing!", + array['SECURITY'] as "categories!", + 'Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST' as "description!", + format( + 'Table \`%s.%s\` is public, but RLS has not been enabled.', + n.nspname, + c.relname + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public' as "remediation!", + jsonb_build_object( + 'schema', n.nspname, + 'name', c.relname, + 'type', 'table' + ) as "metadata!", + format( + 'rls_disabled_in_public_%s_%s', + n.nspname, + c.relname + ) as "cache_key!" +from + pg_catalog.pg_class c + join pg_catalog.pg_namespace n + on c.relnamespace = n.oid +where + c.relkind = 'r' -- regular tables + -- RLS is disabled + and not c.relrowsecurity + and ( + pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') + or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') + ) + and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) + and n.nspname not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + )) diff --git a/crates/pgls_splinter/vendor/security/rls_enabled_no_policy.sql b/crates/pgls_splinter/vendor/security/rls_enabled_no_policy.sql new file mode 100644 index 000000000..d1d34c0a1 --- /dev/null +++ b/crates/pgls_splinter/vendor/security/rls_enabled_no_policy.sql @@ -0,0 +1,52 @@ +-- meta: name = rlsEnabledNoPolicy +-- meta: title = RLS Enabled No Policy +-- meta: severity = INFO +-- meta: category = SECURITY +-- meta: description = Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created. +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0008_rls_enabled_no_policy + +( +select + 'rls_enabled_no_policy' as "name!", + 'RLS Enabled No Policy' as "title!", + 'INFO' as "level!", + 'EXTERNAL' as "facing!", + array['SECURITY'] as "categories!", + 'Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created.' as "description!", + format( + 'Table \`%s.%s\` has RLS enabled, but no policies exist', + n.nspname, + c.relname + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0008_rls_enabled_no_policy' as "remediation!", + jsonb_build_object( + 'schema', n.nspname, + 'name', c.relname, + 'type', 'table' + ) as "metadata!", + format( + 'rls_enabled_no_policy_%s_%s', + n.nspname, + c.relname + ) as "cache_key!" +from + pg_catalog.pg_class c + left join pg_catalog.pg_policy p + on p.polrelid = c.oid + join pg_catalog.pg_namespace n + on c.relnamespace = n.oid + left join pg_catalog.pg_depend dep + on c.oid = dep.objid + and dep.deptype = 'e' +where + c.relkind = 'r' -- regular tables + and n.nspname not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + ) + -- RLS is enabled + and c.relrowsecurity + and p.polname is null + and dep.objid is null -- exclude tables owned by extensions +group by + n.nspname, + c.relname) diff --git a/crates/pgls_splinter/vendor/security/rls_references_user_metadata.sql b/crates/pgls_splinter/vendor/security/rls_references_user_metadata.sql new file mode 100644 index 000000000..5dc91ca6f --- /dev/null +++ b/crates/pgls_splinter/vendor/security/rls_references_user_metadata.sql @@ -0,0 +1,61 @@ +-- meta: name = rlsReferencesUserMetadata +-- meta: title = RLS references user metadata +-- meta: severity = ERROR +-- meta: category = SECURITY +-- meta: description = Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy. +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata + +( +with policies as ( + select + nsp.nspname as schema_name, + pb.tablename as table_name, + polname as policy_name, + qual, + with_check + from + pg_catalog.pg_policy pa + join pg_catalog.pg_class pc + on pa.polrelid = pc.oid + join pg_catalog.pg_namespace nsp + on pc.relnamespace = nsp.oid + join pg_catalog.pg_policies pb + on pc.relname = pb.tablename + and nsp.nspname = pb.schemaname + and pa.polname = pb.policyname +) +select + 'rls_references_user_metadata' as "name!", + 'RLS references user metadata' as "title!", + 'ERROR' as "level!", + 'EXTERNAL' as "facing!", + array['SECURITY'] as "categories!", + 'Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy.' as "description!", + format( + 'Table \`%s.%s\` has a row level security policy \`%s\` that references Supabase Auth \`user_metadata\`. \`user_metadata\` is editable by end users and should never be used in a security context.', + schema_name, + table_name, + policy_name + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata' as "remediation!", + jsonb_build_object( + 'schema', schema_name, + 'name', table_name, + 'type', 'table' + ) as "metadata!", + format('rls_references_user_metadata_%s_%s_%s', schema_name, table_name, policy_name) as "cache_key!" +from + policies +where + schema_name not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + ) + and ( + -- Example: auth.jwt() -> 'user_metadata' + -- False positives are possible, but it isn't practical to string match + -- If false positive rate is too high, this expression can iterate + qual like '%auth.jwt()%user_metadata%' + or qual like '%current_setting(%request.jwt.claims%)%user_metadata%' + or with_check like '%auth.jwt()%user_metadata%' + or with_check like '%current_setting(%request.jwt.claims%)%user_metadata%' + )) diff --git a/crates/pgls_splinter/vendor/security/security_definer_view.sql b/crates/pgls_splinter/vendor/security/security_definer_view.sql new file mode 100644 index 000000000..102141fc2 --- /dev/null +++ b/crates/pgls_splinter/vendor/security/security_definer_view.sql @@ -0,0 +1,59 @@ +-- meta: name = securityDefinerView +-- meta: title = Security Definer View +-- meta: severity = ERROR +-- meta: category = SECURITY +-- meta: description = Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view + +( +select + 'security_definer_view' as "name!", + 'Security Definer View' as "title!", + 'ERROR' as "level!", + 'EXTERNAL' as "facing!", + array['SECURITY'] as "categories!", + 'Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user' as "description!", + format( + 'View \`%s.%s\` is defined with the SECURITY DEFINER property', + n.nspname, + c.relname + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view' as "remediation!", + jsonb_build_object( + 'schema', n.nspname, + 'name', c.relname, + 'type', 'view' + ) as "metadata!", + format( + 'security_definer_view_%s_%s', + n.nspname, + c.relname + ) as "cache_key!" +from + pg_catalog.pg_class c + join pg_catalog.pg_namespace n + on n.oid = c.relnamespace + left join pg_catalog.pg_depend dep + on c.oid = dep.objid + and dep.deptype = 'e' +where + c.relkind = 'v' + and ( + pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') + or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') + ) + and substring(pg_catalog.version() from 'PostgreSQL ([0-9]+)') >= '15' -- security invoker was added in pg15 + and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) + and n.nspname not in ( + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + ) + and dep.objid is null -- exclude views owned by extensions + and not ( + lower(coalesce(c.reloptions::text,'{}'))::text[] + && array[ + 'security_invoker=1', + 'security_invoker=true', + 'security_invoker=yes', + 'security_invoker=on' + ] + )) diff --git a/crates/pgls_splinter/vendor/security/unsupported_reg_types.sql b/crates/pgls_splinter/vendor/security/unsupported_reg_types.sql new file mode 100644 index 000000000..859c86005 --- /dev/null +++ b/crates/pgls_splinter/vendor/security/unsupported_reg_types.sql @@ -0,0 +1,49 @@ +-- meta: name = unsupportedRegTypes +-- meta: title = Unsupported reg types +-- meta: severity = WARN +-- meta: category = SECURITY +-- meta: description = Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade. +-- meta: remediation = https://supabase.com/docs/guides/database/database-linter?lint=unsupported_reg_types + +( +select + 'unsupported_reg_types' as "name!", + 'Unsupported reg types' as "title!", + 'WARN' as "level!", + 'EXTERNAL' as "facing!", + array['SECURITY'] as "categories!", + 'Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade.' as "description!", + format( + 'Table \`%s.%s\` has a column \`%s\` with unsupported reg* type \`%s\`.', + n.nspname, + c.relname, + a.attname, + t.typname + ) as "detail!", + 'https://supabase.com/docs/guides/database/database-linter?lint=unsupported_reg_types' as "remediation!", + jsonb_build_object( + 'schema', n.nspname, + 'name', c.relname, + 'column', a.attname, + 'type', 'table' + ) as "metadata!", + format( + 'unsupported_reg_types_%s_%s_%s', + n.nspname, + c.relname, + a.attname + ) AS cache_key +from + pg_catalog.pg_attribute a + join pg_catalog.pg_class c + on a.attrelid = c.oid + join pg_catalog.pg_namespace n + on c.relnamespace = n.oid + join pg_catalog.pg_type t + on a.atttypid = t.oid + join pg_catalog.pg_namespace tn + on t.typnamespace = tn.oid +where + tn.nspname = 'pg_catalog' + and t.typname in ('regcollation', 'regconfig', 'regdictionary', 'regnamespace', 'regoper', 'regoperator', 'regproc', 'regprocedure') + and n.nspname not in ('pg_catalog', 'information_schema', 'pgsodium')) diff --git a/xtask/codegen/Cargo.toml b/xtask/codegen/Cargo.toml index 8b8bbeb9c..c5e95ebe4 100644 --- a/xtask/codegen/Cargo.toml +++ b/xtask/codegen/Cargo.toml @@ -16,6 +16,7 @@ pgls_analyse = { workspace = true } pgls_analyser = { workspace = true } pgls_diagnostics = { workspace = true } pgls_env = { workspace = true } +pgls_splinter = { workspace = true } pgls_workspace = { workspace = true, features = ["schema"] } proc-macro2 = { workspace = true, features = ["span-locations"] } pulldown-cmark = { version = "0.12.2" } diff --git a/xtask/codegen/src/generate_splinter.rs b/xtask/codegen/src/generate_splinter.rs index b44a66aed..d0fba86b4 100644 --- a/xtask/codegen/src/generate_splinter.rs +++ b/xtask/codegen/src/generate_splinter.rs @@ -1,176 +1,426 @@ use anyhow::{Context, Result}; use biome_string_case::Case; +use quote::{format_ident, quote}; use std::collections::BTreeMap; use std::fs; -use xtask::{glue::fs2, project_root}; +use std::path::{Path, PathBuf}; +use xtask::{glue::fs2, project_root, Mode}; + +use crate::update; + +/// Metadata extracted from SQL file comments +#[derive(Debug, Clone)] +struct SqlRuleMetadata { + /// Rule name in camelCase (from meta comment) + name: String, + /// Rule name in snake_case (from filename) + snake_name: String, + /// Human-readable title + title: String, + /// Severity level (INFO, WARN, ERROR) + severity: String, + /// Category (PERFORMANCE, SECURITY, etc.) + category: String, + /// Description of what the rule detects + description: String, + /// Remediation URL or text + remediation: String, + /// Path to SQL file relative to vendor/ + sql_file_path: PathBuf, +} -/// Generate splinter categories from the SQL files (both generic and Supabase-specific) +/// Generate splinter rules, registry, and categories from individual SQL files pub fn generate_splinter() -> Result<()> { + let vendor_dir = project_root().join("crates/pgls_splinter/vendor"); + + // Scan for SQL files in performance/ and security/ directories let mut all_rules = BTreeMap::new(); - // Process generic rules - let generic_sql_path = project_root().join("crates/pgls_splinter/vendor/splinter_generic.sql"); - if generic_sql_path.exists() { - let sql_content = fs::read_to_string(&generic_sql_path) - .with_context(|| format!("Failed to read SQL file at {generic_sql_path:?}"))?; - let rules = extract_rules_from_sql(&sql_content)?; - all_rules.extend(rules); - } + for category in &["performance", "security"] { + let category_dir = vendor_dir.join(category); + if !category_dir.exists() { + continue; + } + + for entry in fs::read_dir(&category_dir)? { + let entry = entry?; + let path = entry.path(); - // Process Supabase-specific rules - let supabase_sql_path = - project_root().join("crates/pgls_splinter/vendor/splinter_supabase.sql"); - if supabase_sql_path.exists() { - let sql_content = fs::read_to_string(&supabase_sql_path) - .with_context(|| format!("Failed to read SQL file at {supabase_sql_path:?}"))?; - let rules = extract_rules_from_sql(&sql_content)?; - all_rules.extend(rules); + if path.extension().and_then(|s| s.to_str()) == Some("sql") { + let metadata = extract_metadata_from_sql(&path, category)?; + all_rules.insert(metadata.snake_name.clone(), metadata); + } + } } - update_categories_file(all_rules)?; + // Generate Rust rule files + generate_rule_trait()?; + generate_rule_files(&all_rules)?; + generate_registry(&all_rules)?; + + // Update categories file (keep existing logic for backward compat) + update_categories_file(&all_rules)?; Ok(()) } -/// Extract rule information from the SQL file -fn extract_rules_from_sql(content: &str) -> Result> { - let mut rules = BTreeMap::new(); +/// Extract metadata from SQL file comment headers +fn extract_metadata_from_sql(sql_path: &Path, category: &str) -> Result { + let content = fs::read_to_string(sql_path) + .with_context(|| format!("Failed to read SQL file: {sql_path:?}"))?; - let lines: Vec<&str> = content.lines().collect(); + let mut name = None; + let mut title = None; + let mut severity = None; + let mut meta_category = None; + let mut description = None; + let mut remediation = None; - for (i, line) in lines.iter().enumerate() { + // Parse metadata comments + for line in content.lines() { let line = line.trim(); + if !line.starts_with("--") { + break; // Stop at first non-comment line + } - // Look for pattern: 'rule_name' as "name!", - if line.contains(" as \"name!\"") { - if let Some(name) = extract_string_literal(line) { - // Look ahead for categories and remediation URL - let mut categories = None; - let mut remediation_url = None; - - for next_line in lines[i..].iter().take(30) { - let next_line = next_line.trim(); - - // Extract categories from pattern: array['CATEGORY'] as "categories!", - if next_line.contains(" as \"categories!\"") { - categories = extract_categories(next_line); - } - - // Extract remediation URL from pattern: 'url' as "remediation!", - if next_line.contains(" as \"remediation!\"") { - remediation_url = extract_string_literal(next_line); - } - - // Stop once we have both - if categories.is_some() && remediation_url.is_some() { - break; - } - } - - let cats = categories - .with_context(|| format!("Failed to find categories for rule '{name}'"))?; - - // Convert old database-linter URLs to database-advisors - let updated_url = remediation_url - .map(|url| url.replace("/database-linter", "/database-advisors")) - .or(Some( - "https://supabase.com/docs/guides/database/database-advisors".to_string(), - )); - - rules.insert( - name.clone(), - RuleInfo { - snake_case: name.clone(), - camel_case: snake_to_camel_case(&name), - categories: cats, - url: updated_url, - }, - ); + if line.starts_with("-- meta:") { + let meta_line = &line[8..].trim(); // Remove "-- meta:" + + if let Some(value) = extract_meta_value(meta_line, "name") { + name = Some(value); + } else if let Some(value) = extract_meta_value(meta_line, "title") { + title = Some(value); + } else if let Some(value) = extract_meta_value(meta_line, "severity") { + severity = Some(value); + } else if let Some(value) = extract_meta_value(meta_line, "category") { + meta_category = Some(value); + } else if let Some(value) = extract_meta_value(meta_line, "description") { + description = Some(value); + } else if let Some(value) = extract_meta_value(meta_line, "remediation") { + remediation = Some(value); } } } - // Add the "unknown" fallback rule - rules.insert( - "unknown".to_string(), - RuleInfo { - snake_case: "unknown".to_string(), - camel_case: "unknown".to_string(), - categories: vec!["UNKNOWN".to_string()], - url: Some("https://supabase.com/docs/guides/database/database-advisors".to_string()), - }, - ); - - Ok(rules) + // Get snake_case name from filename + let snake_name = sql_path + .file_stem() + .and_then(|s| s.to_str()) + .context("Invalid filename")? + .to_string(); + + // Build metadata + let name = name.context("Missing 'name' in metadata comments")?; + let title = title.context("Missing 'title' in metadata comments")?; + let severity = severity.context("Missing 'severity' in metadata comments")?; + let category_from_meta = meta_category.context("Missing 'category' in metadata comments")?; + let description = description.context("Missing 'description' in metadata comments")?; + let remediation = remediation.unwrap_or_else(|| { + "https://supabase.com/docs/guides/database/database-advisors".to_string() + }); + + // Verify category matches directory + if category_from_meta.to_lowercase() != category { + anyhow::bail!( + "Category mismatch: file in {category}/ but metadata says {category_from_meta}" + ); + } + + let sql_file_path = PathBuf::from(category).join(format!("{}.sql", snake_name)); + + Ok(SqlRuleMetadata { + name, + snake_name, + title, + severity, + category: category_from_meta, + description, + remediation, + sql_file_path, + }) +} + +/// Extract value from metadata line like "name = value" +fn extract_meta_value(line: &str, key: &str) -> Option { + if let Some(pos) = line.find(&format!("{key} =")) { + let value_start = pos + key.len() + " =".len(); + let value = line[value_start..].trim(); + return Some(value.to_string()); + } + None +} + +/// Generate src/rule.rs with SplinterRule trait +fn generate_rule_trait() -> Result<()> { + let rule_path = project_root().join("crates/pgls_splinter/src/rule.rs"); + + let content = quote! { + //! Generated file, do not edit by hand, see `xtask/codegen` + + use pgls_analyse::RuleMeta; + + /// Trait for splinter (database-level) rules + /// + /// Splinter rules are different from linter rules: + /// - They execute SQL queries against the database + /// - They don't have AST-based execution + /// - Rule logic is in SQL files, not Rust + pub trait SplinterRule: RuleMeta { + /// Path to the SQL file containing the rule query + fn sql_file_path() -> &'static str; + } + }; + + let formatted = xtask::reformat(content)?; + update(&rule_path, &formatted, &Mode::Overwrite)?; + + Ok(()) } -/// Extract a string literal from a line like "'some_string' as ..." -fn extract_string_literal(line: &str) -> Option { - let trimmed = line.trim(); +/// Generate rule files in src/rules/{category}/{rule_name}.rs +fn generate_rule_files(rules: &BTreeMap) -> Result<()> { + let rules_dir = project_root().join("crates/pgls_splinter/src/rules"); + + // Group rules by category + let mut rules_by_category: BTreeMap> = BTreeMap::new(); + for rule in rules.values() { + rules_by_category + .entry(rule.category.to_lowercase()) + .or_default() + .push(rule); + } + + // Generate category mod files and rule files + for (category, category_rules) in &rules_by_category { + let category_dir = rules_dir.join(category); + fs2::create_dir_all(&category_dir)?; - if let Some(start_single) = trimmed.find('\'') { - if let Some(end) = trimmed[start_single + 1..].find('\'') { - return Some(trimmed[start_single + 1..start_single + 1 + end].to_string()); + // Generate individual rule files + for rule in category_rules { + generate_rule_file(&category_dir, rule)?; } + + // Generate category mod.rs + generate_category_mod(&category_dir, category, category_rules)?; } - None + // Generate main rules mod.rs + generate_rules_mod(&rules_dir, &rules_by_category)?; + + Ok(()) } -/// Extract categories from a line like "array['CATEGORY'] as "categories!"," -fn extract_categories(line: &str) -> Option> { - let trimmed = line.trim(); - - // Look for array['...'] - if let Some(start) = trimmed.find("array[") { - if let Some(end) = trimmed[start..].find(']') { - let array_content = &trimmed[start + 6..start + end]; - - // Extract all string literals within the array - let categories: Vec = array_content - .split(',') - .filter_map(|s| { - let s = s.trim(); - if let Some(start_quote) = s.find('\'') { - if let Some(end_quote) = s[start_quote + 1..].find('\'') { - return Some( - s[start_quote + 1..start_quote + 1 + end_quote].to_string(), - ); - } - } - None - }) - .collect(); - - if !categories.is_empty() { - return Some(categories); +/// Generate individual rule file +fn generate_rule_file(category_dir: &Path, metadata: &SqlRuleMetadata) -> Result<()> { + let rule_file = category_dir.join(format!("{}.rs", metadata.snake_name)); + + let struct_name = Case::Pascal.convert(&metadata.snake_name); + let struct_name = format_ident!("{}", struct_name); + + // These will be used as string literals in the quote! + let title = &metadata.title; + let description = &metadata.description; + let name = &metadata.name; // camelCase name + let category_upper = metadata.category.to_uppercase(); + let category_ident = format_ident!("{}", category_upper); + let sql_path = metadata.sql_file_path.display().to_string(); + + // Parse severity - this will be a Rust expression + let severity = match metadata.severity.as_str() { + "INFO" => quote! { pgls_diagnostics::Severity::Information }, + "WARN" => quote! { pgls_diagnostics::Severity::Warning }, + "ERROR" => quote! { pgls_diagnostics::Severity::Error }, + _ => quote! { pgls_diagnostics::Severity::Information }, + }; + + let content = quote! { + //! Generated file, do not edit by hand, see `xtask/codegen` + + use crate::rule::SplinterRule; + use pgls_analyse::RuleMeta; + + ::pgls_analyse::declare_rule! { + /// #title + /// + /// #description + pub #struct_name { + version: "1.0.0", + name: #name, + severity: #severity, } } - } - None + impl SplinterRule for #struct_name { + fn sql_file_path() -> &'static str { + #sql_path + } + } + }; + + let formatted = xtask::reformat(content)?; + update(&rule_file, &formatted, &Mode::Overwrite)?; + + Ok(()) } -/// Convert snake_case to camelCase -fn snake_to_camel_case(s: &str) -> String { - Case::Camel.convert(s) +/// Generate category mod.rs that exports all rules in the category +fn generate_category_mod( + category_dir: &Path, + category: &str, + rules: &[&SqlRuleMetadata], +) -> Result<()> { + let mod_file = category_dir.join("mod.rs"); + + let category_title = Case::Pascal.convert(category); + let category_struct = format_ident!("{}", category_title); + + // Generate mod declarations + let mod_names: Vec<_> = rules + .iter() + .map(|r| format_ident!("{}", r.snake_name)) + .collect(); + + // Generate rule paths for declare_lint_group! + let rule_paths: Vec<_> = rules + .iter() + .map(|r| { + let mod_name = format_ident!("{}", r.snake_name); + let struct_name = format_ident!("{}", Case::Pascal.convert(&r.snake_name)); + quote! { self::#mod_name::#struct_name } + }) + .collect(); + + let content = quote! { + //! Generated file, do not edit by hand, see `xtask/codegen` + + #( pub mod #mod_names; )* + + ::pgls_analyse::declare_lint_group! { + pub #category_struct { + name: #category, + rules: [ + #( #rule_paths, )* + ] + } + } + }; + + let formatted = xtask::reformat(content)?; + update(&mod_file, &formatted, &Mode::Overwrite)?; + + Ok(()) } -/// Check if a string is a valid URL (simple check for http/https) -fn is_valid_url(s: &str) -> bool { - s.starts_with("http://") || s.starts_with("https://") +/// Generate main rules/mod.rs +fn generate_rules_mod( + rules_dir: &Path, + rules_by_category: &BTreeMap>, +) -> Result<()> { + let mod_file = rules_dir.join("mod.rs"); + + let category_mods: Vec<_> = rules_by_category + .keys() + .map(|cat| { + let mod_name = format_ident!("{}", cat); + quote! { pub mod #mod_name; } + }) + .collect(); + + // Generate group paths for declare_category! + let group_paths: Vec<_> = rules_by_category + .keys() + .map(|cat| { + let mod_name = format_ident!("{}", cat); + let group_name = format_ident!("{}", Case::Pascal.convert(cat)); + quote! { self::#mod_name::#group_name } + }) + .collect(); + + let content = quote! { + //! Generated file, do not edit by hand, see `xtask/codegen` + + #( #category_mods )* + + ::pgls_analyse::declare_category! { + pub Splinter { + kind: Lint, + groups: [ + #( #group_paths, )* + ] + } + } + }; + + let formatted = xtask::reformat(content)?; + update(&mod_file, &formatted, &Mode::Overwrite)?; + + Ok(()) } -struct RuleInfo { - #[allow(dead_code)] - snake_case: String, - camel_case: String, - categories: Vec, - url: Option, +/// Generate src/registry.rs with visit_registry() and get_sql_file_path() +fn generate_registry(rules: &BTreeMap) -> Result<()> { + let registry_path = project_root().join("crates/pgls_splinter/src/registry.rs"); + + // Group rules by category for organized output + let mut rules_by_category: BTreeMap> = BTreeMap::new(); + for rule in rules.values() { + rules_by_category + .entry(rule.category.to_lowercase()) + .or_default() + .push(rule); + } + + // Record the top-level category (which contains all groups) + let mut record_calls = vec![quote! { + registry.record_category::(); + }]; + + // Generate match arms for SQL file path mapping + let sql_path_arms: Vec<_> = rules + .values() + .map(|rule| { + let name = &rule.name; + let vendor_path = project_root() + .join("crates/pgls_splinter/vendor") + .join(&rule.sql_file_path); + let path_str = vendor_path.display().to_string(); + + quote! { + #name => Some(#path_str) + } + }) + .collect(); + + let content = quote! { + //! Generated file, do not edit by hand, see `xtask/codegen` + + use pgls_analyse::RegistryVisitor; + + /// Visit all splinter rules using the visitor pattern + /// This is called during registry building to collect enabled rules + pub fn visit_registry(registry: &mut V) { + #( #record_calls )* + } + + /// Map rule name (camelCase) to SQL file path + /// Returns None if rule not found + pub fn get_sql_file_path(rule_name: &str) -> Option<&'static str> { + match rule_name { + #( #sql_path_arms, )* + _ => None, + } + } + }; + + let formatted = xtask::reformat(content)?; + update(®istry_path, &formatted, &Mode::Overwrite)?; + + Ok(()) } /// Update the categories.rs file with splinter rules -fn update_categories_file(rules: BTreeMap) -> Result<()> { +/// This maintains backward compatibility with existing category system +fn update_categories_file(rules: &BTreeMap) -> Result<()> { let categories_path = project_root().join("crates/pgls_diagnostics_categories/src/categories.rs"); @@ -179,30 +429,16 @@ fn update_categories_file(rules: BTreeMap) -> Result<()> { // Generate splinter rule entries grouped by category let mut splinter_rules: Vec<(String, String)> = rules .values() - .flat_map(|rule| { - // For each rule, create entries for all its categories - // In practice, splinter rules have only one category - rule.categories.iter().map(|category| { - let group = category.to_lowercase(); - - // Use extracted URL if it's a valid URL, otherwise fallback to default - let url = rule - .url - .as_ref() - .filter(|u| is_valid_url(u)) - .map(|u| u.as_str()) - .unwrap_or("https://supabase.com/docs/guides/database/database-advisors"); - - ( - group.clone(), - format!( - " \"splinter/{}/{}\": \"{}\",", - group, rule.camel_case, url - ), - ) - }) + .map(|rule| { + let group = rule.category.to_lowercase(); + let url = &rule.remediation; + + ( + group.clone(), + format!(" \"splinter/{}/{}\": \"{}\",", group, rule.name, url), + ) }) - .collect::>(); + .collect(); // Sort by group, then by entry splinter_rules.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1))); From 821dd3a776f8a4b9cb6fee99746940a4a35f4d67 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 15 Dec 2025 11:15:42 +0100 Subject: [PATCH 07/14] fix: update bun.lock and remove unused mut --- xtask/codegen/src/generate_splinter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xtask/codegen/src/generate_splinter.rs b/xtask/codegen/src/generate_splinter.rs index d0fba86b4..3291f4fcf 100644 --- a/xtask/codegen/src/generate_splinter.rs +++ b/xtask/codegen/src/generate_splinter.rs @@ -371,7 +371,7 @@ fn generate_registry(rules: &BTreeMap) -> Result<()> { } // Record the top-level category (which contains all groups) - let mut record_calls = vec![quote! { + let record_calls = vec![quote! { registry.record_category::(); }]; From 03797590adb7b0bbd955c147113caa0048c114bc Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 15 Dec 2025 13:34:25 +0100 Subject: [PATCH 08/14] chore: rebase onto refactor/rules with test import fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picked up test import fixes from base branch and applied codegen updates. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- crates/pgls_configuration/src/linter/rules.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/pgls_configuration/src/linter/rules.rs b/crates/pgls_configuration/src/linter/rules.rs index 6336d88fa..4bc3d4c98 100644 --- a/crates/pgls_configuration/src/linter/rules.rs +++ b/crates/pgls_configuration/src/linter/rules.rs @@ -72,8 +72,8 @@ impl Rules { #[doc = r" The function can return `None` if the rule is not properly configured."] pub fn get_severity_from_code(&self, category: &Category) -> Option { let mut split_code = category.name().split('/'); - let _category = split_code.next(); - debug_assert_eq!(_category, Some("lint")); + let _lint = split_code.next(); + debug_assert_eq!(_lint, Some("lint")); let group = ::from_str(split_code.next()?).ok()?; let rule_name = split_code.next()?; let rule_name = Self::has_rule(group, rule_name)?; From cc43d1f7cb64836d4c562347689e9113ec2abc28 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 15 Dec 2025 13:53:41 +0100 Subject: [PATCH 09/14] fix: use relative paths in splinter registry codegen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed SQL file path generation to use relative paths from crate root instead of absolute paths, fixing CI codegen check failures. Also removed unused splinter/unknown category from TypeScript types. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- crates/pgls_splinter/src/registry.rs | 84 +++++-------------- .../backend-jsonrpc/src/workspace.ts | 4 +- .../backend-jsonrpc/src/workspace.ts | 4 +- xtask/codegen/src/generate_splinter.rs | 8 +- 4 files changed, 26 insertions(+), 74 deletions(-) diff --git a/crates/pgls_splinter/src/registry.rs b/crates/pgls_splinter/src/registry.rs index 64f211b8d..911b22891 100644 --- a/crates/pgls_splinter/src/registry.rs +++ b/crates/pgls_splinter/src/registry.rs @@ -11,69 +11,27 @@ pub fn visit_registry(registry: &mut V) { #[doc = r" Returns None if rule not found"] pub fn get_sql_file_path(rule_name: &str) -> Option<&'static str> { match rule_name { - "authRlsInitplan" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/performance/auth_rls_initplan.sql", - ), - "authUsersExposed" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/auth_users_exposed.sql", - ), - "duplicateIndex" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/performance/duplicate_index.sql", - ), - "extensionInPublic" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/extension_in_public.sql", - ), - "extensionVersionsOutdated" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/extension_versions_outdated.sql", - ), - "fkeyToAuthUnique" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/fkey_to_auth_unique.sql", - ), - "foreignTableInApi" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/foreign_table_in_api.sql", - ), - "functionSearchPathMutable" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/function_search_path_mutable.sql", - ), - "insecureQueueExposedInApi" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/insecure_queue_exposed_in_api.sql", - ), - "materializedViewInApi" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/materialized_view_in_api.sql", - ), - "multiplePermissivePolicies" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/performance/multiple_permissive_policies.sql", - ), - "noPrimaryKey" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/performance/no_primary_key.sql", - ), - "policyExistsRlsDisabled" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/policy_exists_rls_disabled.sql", - ), - "rlsDisabledInPublic" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/rls_disabled_in_public.sql", - ), - "rlsEnabledNoPolicy" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/rls_enabled_no_policy.sql", - ), - "rlsReferencesUserMetadata" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/rls_references_user_metadata.sql", - ), - "securityDefinerView" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/security_definer_view.sql", - ), - "tableBloat" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/performance/table_bloat.sql", - ), - "unindexedForeignKeys" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/performance/unindexed_foreign_keys.sql", - ), - "unsupportedRegTypes" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/security/unsupported_reg_types.sql", - ), - "unusedIndex" => Some( - "/Users/psteinroe/Developer/postgres-language-server.git/refactor/rules/crates/pgls_splinter/vendor/performance/unused_index.sql", - ), + "authRlsInitplan" => Some("vendor/performance/auth_rls_initplan.sql"), + "authUsersExposed" => Some("vendor/security/auth_users_exposed.sql"), + "duplicateIndex" => Some("vendor/performance/duplicate_index.sql"), + "extensionInPublic" => Some("vendor/security/extension_in_public.sql"), + "extensionVersionsOutdated" => Some("vendor/security/extension_versions_outdated.sql"), + "fkeyToAuthUnique" => Some("vendor/security/fkey_to_auth_unique.sql"), + "foreignTableInApi" => Some("vendor/security/foreign_table_in_api.sql"), + "functionSearchPathMutable" => Some("vendor/security/function_search_path_mutable.sql"), + "insecureQueueExposedInApi" => Some("vendor/security/insecure_queue_exposed_in_api.sql"), + "materializedViewInApi" => Some("vendor/security/materialized_view_in_api.sql"), + "multiplePermissivePolicies" => Some("vendor/performance/multiple_permissive_policies.sql"), + "noPrimaryKey" => Some("vendor/performance/no_primary_key.sql"), + "policyExistsRlsDisabled" => Some("vendor/security/policy_exists_rls_disabled.sql"), + "rlsDisabledInPublic" => Some("vendor/security/rls_disabled_in_public.sql"), + "rlsEnabledNoPolicy" => Some("vendor/security/rls_enabled_no_policy.sql"), + "rlsReferencesUserMetadata" => Some("vendor/security/rls_references_user_metadata.sql"), + "securityDefinerView" => Some("vendor/security/security_definer_view.sql"), + "tableBloat" => Some("vendor/performance/table_bloat.sql"), + "unindexedForeignKeys" => Some("vendor/performance/unindexed_foreign_keys.sql"), + "unsupportedRegTypes" => Some("vendor/security/unsupported_reg_types.sql"), + "unusedIndex" => Some("vendor/performance/unused_index.sql"), _ => None, } } diff --git a/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts b/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts index 4771e6c4b..180fc6b60 100644 --- a/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts +++ b/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts @@ -116,7 +116,6 @@ export type Category = | "splinter/security/rlsReferencesUserMetadata" | "splinter/security/securityDefinerView" | "splinter/security/unsupportedRegTypes" - | "splinter/unknown/unknown" | "stdin" | "check" | "configuration" @@ -136,8 +135,7 @@ export type Category = | "lint/safety" | "splinter" | "splinter/performance" - | "splinter/security" - | "splinter/unknown"; + | "splinter/security"; export interface Location { path?: Resource_for_String; sourceCode?: string; diff --git a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts index 4771e6c4b..180fc6b60 100644 --- a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts +++ b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts @@ -116,7 +116,6 @@ export type Category = | "splinter/security/rlsReferencesUserMetadata" | "splinter/security/securityDefinerView" | "splinter/security/unsupportedRegTypes" - | "splinter/unknown/unknown" | "stdin" | "check" | "configuration" @@ -136,8 +135,7 @@ export type Category = | "lint/safety" | "splinter" | "splinter/performance" - | "splinter/security" - | "splinter/unknown"; + | "splinter/security"; export interface Location { path?: Resource_for_String; sourceCode?: string; diff --git a/xtask/codegen/src/generate_splinter.rs b/xtask/codegen/src/generate_splinter.rs index 3291f4fcf..b3fc58de0 100644 --- a/xtask/codegen/src/generate_splinter.rs +++ b/xtask/codegen/src/generate_splinter.rs @@ -380,13 +380,11 @@ fn generate_registry(rules: &BTreeMap) -> Result<()> { .values() .map(|rule| { let name = &rule.name; - let vendor_path = project_root() - .join("crates/pgls_splinter/vendor") - .join(&rule.sql_file_path); - let path_str = vendor_path.display().to_string(); + // Use relative path from crate root + let relative_path = format!("vendor/{}", rule.sql_file_path.display()); quote! { - #name => Some(#path_str) + #name => Some(#relative_path) } }) .collect(); From 85b1802a33b369113791565419c10541f42c101c Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 15 Dec 2025 14:07:07 +0100 Subject: [PATCH 10/14] fix: remove unused RuleMeta import and apply clippy fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed unused `use pgls_analyse::RuleMeta;` import from generated splinter rule files and applied clippy format! macro fix. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../pgls_splinter/src/rules/performance/auth_rls_initplan.rs | 1 - crates/pgls_splinter/src/rules/performance/duplicate_index.rs | 1 - .../src/rules/performance/multiple_permissive_policies.rs | 1 - crates/pgls_splinter/src/rules/performance/no_primary_key.rs | 1 - crates/pgls_splinter/src/rules/performance/table_bloat.rs | 1 - .../src/rules/performance/unindexed_foreign_keys.rs | 1 - crates/pgls_splinter/src/rules/performance/unused_index.rs | 1 - crates/pgls_splinter/src/rules/security/auth_users_exposed.rs | 1 - crates/pgls_splinter/src/rules/security/extension_in_public.rs | 1 - .../src/rules/security/extension_versions_outdated.rs | 1 - crates/pgls_splinter/src/rules/security/fkey_to_auth_unique.rs | 1 - .../pgls_splinter/src/rules/security/foreign_table_in_api.rs | 1 - .../src/rules/security/function_search_path_mutable.rs | 1 - .../src/rules/security/insecure_queue_exposed_in_api.rs | 1 - .../src/rules/security/materialized_view_in_api.rs | 1 - .../src/rules/security/policy_exists_rls_disabled.rs | 1 - .../pgls_splinter/src/rules/security/rls_disabled_in_public.rs | 1 - .../pgls_splinter/src/rules/security/rls_enabled_no_policy.rs | 1 - .../src/rules/security/rls_references_user_metadata.rs | 1 - .../pgls_splinter/src/rules/security/security_definer_view.rs | 1 - .../pgls_splinter/src/rules/security/unsupported_reg_types.rs | 1 - xtask/codegen/src/generate_splinter.rs | 3 +-- 22 files changed, 1 insertion(+), 23 deletions(-) diff --git a/crates/pgls_splinter/src/rules/performance/auth_rls_initplan.rs b/crates/pgls_splinter/src/rules/performance/auth_rls_initplan.rs index fd64ce41f..6b733217c 100644 --- a/crates/pgls_splinter/src/rules/performance/auth_rls_initplan.rs +++ b/crates/pgls_splinter/src/rules/performance/auth_rls_initplan.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub AuthRlsInitplan { version : "1.0.0" , name : "authRlsInitplan" , severity : pgls_diagnostics :: Severity :: Warning , } } impl SplinterRule for AuthRlsInitplan { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/performance/duplicate_index.rs b/crates/pgls_splinter/src/rules/performance/duplicate_index.rs index 81394496f..85077c948 100644 --- a/crates/pgls_splinter/src/rules/performance/duplicate_index.rs +++ b/crates/pgls_splinter/src/rules/performance/duplicate_index.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub DuplicateIndex { version : "1.0.0" , name : "duplicateIndex" , severity : pgls_diagnostics :: Severity :: Warning , } } impl SplinterRule for DuplicateIndex { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/performance/multiple_permissive_policies.rs b/crates/pgls_splinter/src/rules/performance/multiple_permissive_policies.rs index 45d45733f..e53653482 100644 --- a/crates/pgls_splinter/src/rules/performance/multiple_permissive_policies.rs +++ b/crates/pgls_splinter/src/rules/performance/multiple_permissive_policies.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub MultiplePermissivePolicies { version : "1.0.0" , name : "multiplePermissivePolicies" , severity : pgls_diagnostics :: Severity :: Warning , } } impl SplinterRule for MultiplePermissivePolicies { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/performance/no_primary_key.rs b/crates/pgls_splinter/src/rules/performance/no_primary_key.rs index 3127f7bf3..584aa0474 100644 --- a/crates/pgls_splinter/src/rules/performance/no_primary_key.rs +++ b/crates/pgls_splinter/src/rules/performance/no_primary_key.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub NoPrimaryKey { version : "1.0.0" , name : "noPrimaryKey" , severity : pgls_diagnostics :: Severity :: Information , } } impl SplinterRule for NoPrimaryKey { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/performance/table_bloat.rs b/crates/pgls_splinter/src/rules/performance/table_bloat.rs index 1efe0d43a..734cb8acd 100644 --- a/crates/pgls_splinter/src/rules/performance/table_bloat.rs +++ b/crates/pgls_splinter/src/rules/performance/table_bloat.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub TableBloat { version : "1.0.0" , name : "tableBloat" , severity : pgls_diagnostics :: Severity :: Information , } } impl SplinterRule for TableBloat { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/performance/unindexed_foreign_keys.rs b/crates/pgls_splinter/src/rules/performance/unindexed_foreign_keys.rs index 4e75899b6..aa59e3e09 100644 --- a/crates/pgls_splinter/src/rules/performance/unindexed_foreign_keys.rs +++ b/crates/pgls_splinter/src/rules/performance/unindexed_foreign_keys.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub UnindexedForeignKeys { version : "1.0.0" , name : "unindexedForeignKeys" , severity : pgls_diagnostics :: Severity :: Information , } } impl SplinterRule for UnindexedForeignKeys { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/performance/unused_index.rs b/crates/pgls_splinter/src/rules/performance/unused_index.rs index afdf18c36..1563989d0 100644 --- a/crates/pgls_splinter/src/rules/performance/unused_index.rs +++ b/crates/pgls_splinter/src/rules/performance/unused_index.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub UnusedIndex { version : "1.0.0" , name : "unusedIndex" , severity : pgls_diagnostics :: Severity :: Information , } } impl SplinterRule for UnusedIndex { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/security/auth_users_exposed.rs b/crates/pgls_splinter/src/rules/security/auth_users_exposed.rs index c159f470d..25d76b2df 100644 --- a/crates/pgls_splinter/src/rules/security/auth_users_exposed.rs +++ b/crates/pgls_splinter/src/rules/security/auth_users_exposed.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub AuthUsersExposed { version : "1.0.0" , name : "authUsersExposed" , severity : pgls_diagnostics :: Severity :: Error , } } impl SplinterRule for AuthUsersExposed { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/security/extension_in_public.rs b/crates/pgls_splinter/src/rules/security/extension_in_public.rs index e341ddf02..d4b85b743 100644 --- a/crates/pgls_splinter/src/rules/security/extension_in_public.rs +++ b/crates/pgls_splinter/src/rules/security/extension_in_public.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub ExtensionInPublic { version : "1.0.0" , name : "extensionInPublic" , severity : pgls_diagnostics :: Severity :: Warning , } } impl SplinterRule for ExtensionInPublic { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/security/extension_versions_outdated.rs b/crates/pgls_splinter/src/rules/security/extension_versions_outdated.rs index 3596a6b99..22ff28507 100644 --- a/crates/pgls_splinter/src/rules/security/extension_versions_outdated.rs +++ b/crates/pgls_splinter/src/rules/security/extension_versions_outdated.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub ExtensionVersionsOutdated { version : "1.0.0" , name : "extensionVersionsOutdated" , severity : pgls_diagnostics :: Severity :: Warning , } } impl SplinterRule for ExtensionVersionsOutdated { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/security/fkey_to_auth_unique.rs b/crates/pgls_splinter/src/rules/security/fkey_to_auth_unique.rs index 12986b7ef..e4cfced1d 100644 --- a/crates/pgls_splinter/src/rules/security/fkey_to_auth_unique.rs +++ b/crates/pgls_splinter/src/rules/security/fkey_to_auth_unique.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub FkeyToAuthUnique { version : "1.0.0" , name : "fkeyToAuthUnique" , severity : pgls_diagnostics :: Severity :: Error , } } impl SplinterRule for FkeyToAuthUnique { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/security/foreign_table_in_api.rs b/crates/pgls_splinter/src/rules/security/foreign_table_in_api.rs index 9bd223d86..eda8a8576 100644 --- a/crates/pgls_splinter/src/rules/security/foreign_table_in_api.rs +++ b/crates/pgls_splinter/src/rules/security/foreign_table_in_api.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub ForeignTableInApi { version : "1.0.0" , name : "foreignTableInApi" , severity : pgls_diagnostics :: Severity :: Warning , } } impl SplinterRule for ForeignTableInApi { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/security/function_search_path_mutable.rs b/crates/pgls_splinter/src/rules/security/function_search_path_mutable.rs index 0786cffb1..0e67b7d10 100644 --- a/crates/pgls_splinter/src/rules/security/function_search_path_mutable.rs +++ b/crates/pgls_splinter/src/rules/security/function_search_path_mutable.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub FunctionSearchPathMutable { version : "1.0.0" , name : "functionSearchPathMutable" , severity : pgls_diagnostics :: Severity :: Warning , } } impl SplinterRule for FunctionSearchPathMutable { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/security/insecure_queue_exposed_in_api.rs b/crates/pgls_splinter/src/rules/security/insecure_queue_exposed_in_api.rs index 9916e69da..077801ea2 100644 --- a/crates/pgls_splinter/src/rules/security/insecure_queue_exposed_in_api.rs +++ b/crates/pgls_splinter/src/rules/security/insecure_queue_exposed_in_api.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub InsecureQueueExposedInApi { version : "1.0.0" , name : "insecureQueueExposedInApi" , severity : pgls_diagnostics :: Severity :: Error , } } impl SplinterRule for InsecureQueueExposedInApi { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/security/materialized_view_in_api.rs b/crates/pgls_splinter/src/rules/security/materialized_view_in_api.rs index 8402712cf..2f7f71e72 100644 --- a/crates/pgls_splinter/src/rules/security/materialized_view_in_api.rs +++ b/crates/pgls_splinter/src/rules/security/materialized_view_in_api.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub MaterializedViewInApi { version : "1.0.0" , name : "materializedViewInApi" , severity : pgls_diagnostics :: Severity :: Warning , } } impl SplinterRule for MaterializedViewInApi { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/security/policy_exists_rls_disabled.rs b/crates/pgls_splinter/src/rules/security/policy_exists_rls_disabled.rs index eb4bb493d..daabdb6ad 100644 --- a/crates/pgls_splinter/src/rules/security/policy_exists_rls_disabled.rs +++ b/crates/pgls_splinter/src/rules/security/policy_exists_rls_disabled.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub PolicyExistsRlsDisabled { version : "1.0.0" , name : "policyExistsRlsDisabled" , severity : pgls_diagnostics :: Severity :: Error , } } impl SplinterRule for PolicyExistsRlsDisabled { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/security/rls_disabled_in_public.rs b/crates/pgls_splinter/src/rules/security/rls_disabled_in_public.rs index 0844e5108..2ab01fde4 100644 --- a/crates/pgls_splinter/src/rules/security/rls_disabled_in_public.rs +++ b/crates/pgls_splinter/src/rules/security/rls_disabled_in_public.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub RlsDisabledInPublic { version : "1.0.0" , name : "rlsDisabledInPublic" , severity : pgls_diagnostics :: Severity :: Error , } } impl SplinterRule for RlsDisabledInPublic { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/security/rls_enabled_no_policy.rs b/crates/pgls_splinter/src/rules/security/rls_enabled_no_policy.rs index d184ef5dc..1b1576d48 100644 --- a/crates/pgls_splinter/src/rules/security/rls_enabled_no_policy.rs +++ b/crates/pgls_splinter/src/rules/security/rls_enabled_no_policy.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub RlsEnabledNoPolicy { version : "1.0.0" , name : "rlsEnabledNoPolicy" , severity : pgls_diagnostics :: Severity :: Information , } } impl SplinterRule for RlsEnabledNoPolicy { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/security/rls_references_user_metadata.rs b/crates/pgls_splinter/src/rules/security/rls_references_user_metadata.rs index c93d3aa10..9b2f24b3c 100644 --- a/crates/pgls_splinter/src/rules/security/rls_references_user_metadata.rs +++ b/crates/pgls_splinter/src/rules/security/rls_references_user_metadata.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub RlsReferencesUserMetadata { version : "1.0.0" , name : "rlsReferencesUserMetadata" , severity : pgls_diagnostics :: Severity :: Error , } } impl SplinterRule for RlsReferencesUserMetadata { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/security/security_definer_view.rs b/crates/pgls_splinter/src/rules/security/security_definer_view.rs index b58714b03..53a11c04f 100644 --- a/crates/pgls_splinter/src/rules/security/security_definer_view.rs +++ b/crates/pgls_splinter/src/rules/security/security_definer_view.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub SecurityDefinerView { version : "1.0.0" , name : "securityDefinerView" , severity : pgls_diagnostics :: Severity :: Error , } } impl SplinterRule for SecurityDefinerView { fn sql_file_path() -> &'static str { diff --git a/crates/pgls_splinter/src/rules/security/unsupported_reg_types.rs b/crates/pgls_splinter/src/rules/security/unsupported_reg_types.rs index 79453c31f..a5672c772 100644 --- a/crates/pgls_splinter/src/rules/security/unsupported_reg_types.rs +++ b/crates/pgls_splinter/src/rules/security/unsupported_reg_types.rs @@ -2,7 +2,6 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { # [doc = r" #title"] # [doc = r""] # [doc = r" #description"] pub UnsupportedRegTypes { version : "1.0.0" , name : "unsupportedRegTypes" , severity : pgls_diagnostics :: Severity :: Warning , } } impl SplinterRule for UnsupportedRegTypes { fn sql_file_path() -> &'static str { diff --git a/xtask/codegen/src/generate_splinter.rs b/xtask/codegen/src/generate_splinter.rs index b3fc58de0..833ce9240 100644 --- a/xtask/codegen/src/generate_splinter.rs +++ b/xtask/codegen/src/generate_splinter.rs @@ -126,7 +126,7 @@ fn extract_metadata_from_sql(sql_path: &Path, category: &str) -> Result Result //! Generated file, do not edit by hand, see `xtask/codegen` use crate::rule::SplinterRule; - use pgls_analyse::RuleMeta; ::pgls_analyse::declare_rule! { /// #title From a197a12d1ed40b1097911c87113a6a18d0c997d6 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 15 Dec 2025 14:19:44 +0100 Subject: [PATCH 11/14] fix: apply clippy format! macro fix in convert.rs --- crates/pgls_splinter/src/convert.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/pgls_splinter/src/convert.rs b/crates/pgls_splinter/src/convert.rs index 81e2e8db5..d0ac8b597 100644 --- a/crates/pgls_splinter/src/convert.rs +++ b/crates/pgls_splinter/src/convert.rs @@ -105,7 +105,7 @@ fn rule_name_to_category(name: &str, group: &str) -> &'static Category { ("security", "fkey_to_auth_unique") => category!("splinter/security/fkeyToAuthUnique"), _ => { // Log a warning for unknown rules but provide a fallback - eprintln!("Warning: Unknown splinter rule: {}/{}", group, name); + eprintln!("Warning: Unknown splinter rule: {group}/{name}"); category!("splinter/performance/unindexedForeignKeys") // Fallback to first rule } } From 5f46e5370fab11fc5339fb04d60d275756d2f7fc Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 15 Dec 2025 14:44:35 +0100 Subject: [PATCH 12/14] fix: panic on unknown splinter rule instead of fallback --- crates/pgls_splinter/src/convert.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/pgls_splinter/src/convert.rs b/crates/pgls_splinter/src/convert.rs index d0ac8b597..0707a7135 100644 --- a/crates/pgls_splinter/src/convert.rs +++ b/crates/pgls_splinter/src/convert.rs @@ -103,11 +103,7 @@ fn rule_name_to_category(name: &str, group: &str) -> &'static Category { category!("splinter/security/insecureQueueExposedInApi") } ("security", "fkey_to_auth_unique") => category!("splinter/security/fkeyToAuthUnique"), - _ => { - // Log a warning for unknown rules but provide a fallback - eprintln!("Warning: Unknown splinter rule: {group}/{name}"); - category!("splinter/performance/unindexedForeignKeys") // Fallback to first rule - } + _ => panic!("Unknown splinter rule: {group}/{name}") } } From df44cfd7e7b9088dcd220264e2916fdcc19ca4aa Mon Sep 17 00:00:00 2001 From: psteinroe Date: Mon, 15 Dec 2025 14:51:01 +0100 Subject: [PATCH 13/14] fix: apply formatting --- crates/pgls_splinter/src/convert.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/pgls_splinter/src/convert.rs b/crates/pgls_splinter/src/convert.rs index 0707a7135..a28d75d0e 100644 --- a/crates/pgls_splinter/src/convert.rs +++ b/crates/pgls_splinter/src/convert.rs @@ -103,7 +103,7 @@ fn rule_name_to_category(name: &str, group: &str) -> &'static Category { category!("splinter/security/insecureQueueExposedInApi") } ("security", "fkey_to_auth_unique") => category!("splinter/security/fkeyToAuthUnique"), - _ => panic!("Unknown splinter rule: {group}/{name}") + _ => panic!("Unknown splinter rule: {group}/{name}"), } } From 81b13c488bcf6b3c5df507221162eadb150a916a Mon Sep 17 00:00:00 2001 From: psteinroe Date: Tue, 16 Dec 2025 18:12:58 +0100 Subject: [PATCH 14/14] fix: remove PLAN.md to resolve merge conflict --- PLAN.md | 524 -------------------------------------------------------- 1 file changed, 524 deletions(-) delete mode 100644 PLAN.md diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 72c8b279f..000000000 --- a/PLAN.md +++ /dev/null @@ -1,524 +0,0 @@ -# Splinter Integration Plan - -## Goal -Integrate splinter into the codegen/rule setup used for the analyser, providing a consistent API (both internally and user-facing) for all types of analysers/linters. - -## Architecture Vision - -### Crate Responsibilities - -**`pgls_analyse`** - Generic framework for all analyzer types -- Generic traits: `RuleMeta`, `RuleGroup`, `GroupCategory`, `RegistryVisitor` -- Shared types: `RuleMetadata`, `RuleCategory` -- Configuration traits (execution-agnostic) -- Macros: `declare_rule!`, `declare_group!`, `declare_category!` - -**`pgls_linter`** (renamed from `pgls_analyser`) - AST-based source code linting -- `LinterRule` trait (extends `RuleMeta`) -- `LinterRuleContext` (wraps AST nodes) -- `LinterDiagnostic` (span-based diagnostics) -- `LinterRuleRegistry` (type-erased executors) - -**`pgls_splinter`** - Database-level linting -- `SplinterRule` trait (extends `RuleMeta`) -- `SplinterRuleRegistry` (metadata-based) -- `SplinterDiagnostic` (db-object-based diagnostics) -- Generated rule types from SQL files - -**`pgls_configuration`** -- `analyser/linter/` - Generated from `pgls_linter` -- `analyser/splinter/` - Generated from `pgls_splinter` -- Per-rule configuration for both - -## Implementation Phases - -### Phase 1: Refactor pgls_analyse ⏳ IN PROGRESS -Extract AST-specific code into pgls_linter, keep only generic framework in pgls_analyse. - -**Tasks:** -- [x] Analyze current `pgls_analyse` exports -- [x] Identify AST-specific vs generic code -- [x] Create new modules in `pgls_analyser`: - - [x] `linter_rule.rs` - LinterRule trait, LinterDiagnostic - - [x] `linter_context.rs` - LinterRuleContext, AnalysedFileContext - - [x] `linter_options.rs` - LinterOptions, LinterRules - - [x] `linter_registry.rs` - LinterRuleRegistry, LinterRegistryVisitor -- [x] Create `pgls_analyse/src/metadata.rs` - Generic traits only -- [x] Update `pgls_analyse/src/registry.rs` - Keep MetadataRegistry only -- [x] Update `pgls_analyse/src/lib.rs` - Export generic framework -- [x] Update `pgls_analyser/src/lib.rs` - Use new modules -- [x] Fix imports in filter.rs (RuleMeta instead of Rule) -- [x] Update generated files (options.rs, registry.rs) -- [x] Fix imports in all rule files -- [x] Add rustc-hash dependency -- [x] Verify compilation completes - **RESOLVED** -- [x] Separate visitor concerns from executor creation -- [x] Update codegen to generate factory function -- [x] Fix all import paths across workspace -- [x] Verify full workspace compiles -- [x] Optimize executor creation (zero-cost abstraction) -- [ ] Run tests - -**Resolution:** -Separated two concerns: -1. **Visitor pattern** (generic): Collects rule keys that match the filter - - Implementation in `LinterRuleRegistryBuilder::record_rule` - - Only requires `R: RuleMeta` (satisfies trait) -2. **Executor mapping** (AST-specific): Maps rule keys directly to executors - - Function `get_linter_rule_executor` in `registry.rs` - - Generated by codegen with full type information - - Zero-cost abstraction: no Box, no dyn, no closures - -**Final Implementation:** -- `LinterRuleRegistryBuilder` stores `Vec` from visitor traversal -- `record_rule` only collects keys (generic, no LinterRule bounds) -- `build()` calls `get_linter_rule_executor` for each key -- Generated match statement returns executors directly (no heap allocation) - -**Performance:** -- ✅ No `Box` - returns values directly -- ✅ No closure overhead - simple match statement -- ✅ No dynamic dispatch - static dispatch only -- ✅ Clean codegen - 33 rules map to executors efficiently - -**Design Decisions:** -- ✅ Keep `RuleDiagnostic` generic or make it linter-specific? → **Move to pgls_linter as LinterDiagnostic** (Option A) - - Rationale: Fundamentally different location models (spans vs db objects) - - LinterDiagnostic: span-based - - SplinterDiagnostic: db-object-based - -**Code Classification:** - -AST-specific (move to pgls_analyser): -- `Rule` trait -- `RuleContext` -- `RuleDiagnostic` → `LinterDiagnostic` -- `AnalysedFileContext` -- `RegistryRuleParams` -- `RuleRegistry`, `RuleRegistryBuilder` (AST execution) -- `AnalyserOptions`, `AnalyserRules` (rule options storage) - -Generic (keep in pgls_analyse): -- `RuleMeta` trait -- `RuleMetadata` struct -- `RuleGroup` trait -- `GroupCategory` trait -- `RegistryVisitor` trait -- `RuleCategory` enum -- `RuleSource` enum -- `RuleFilter`, `AnalysisFilter`, `RuleKey`, `GroupKey` -- `MetadataRegistry` -- Macros: `declare_rule!`, `declare_lint_rule!`, `declare_lint_group!`, `declare_category!` - ---- - -### Phase 2: Enhance pgls_splinter ✅ COMPLETED -Add rule type generation and registry similar to linter. - -**Tasks:** -- [x] Create `pgls_splinter/src/rule.rs` with `SplinterRule` trait -- [x] Create `pgls_splinter/src/rules/` directory structure -- [x] Generate rule types from SQL files -- [x] Generate registry with `visit_registry()` function -- [x] Split monolithic SQL files into individual rule files with metadata -- [x] Create codegen to extract metadata from SQL comments -- [x] Generate get_sql_file_path() function for SQL file mapping -- [ ] Update diagnostics to use generated categories (deferred) -- [ ] Update runtime to build dynamic queries (deferred to Phase 3) - -**Structure:** -``` -pgls_splinter/src/ - rules/ - performance/ - unindexed_foreign_keys.rs # Generated - auth_rls_initplan.rs # Generated - ... # 7 total - mod.rs # Generated group - security/ - auth_users_exposed.rs # Generated - ... # 14 total - mod.rs # Generated group - mod.rs # Generated category - rule.rs # Generated SplinterRule trait - registry.rs # Generated visit_registry() + get_sql_file_path() - -pgls_splinter/vendor/ - performance/ - *.sql # 7 individual SQL files with metadata - security/ - *.sql # 14 individual SQL files with metadata -``` - -**Implementation Summary:** -- Implemented Option C (Hybrid Approach) from initial analysis -- SQL files remain source of truth with metadata comments -- Codegen extracts metadata and generates Rust structures -- `SplinterRule` trait extends `RuleMeta` with `sql_file_path()` method -- Registry provides centralized rule discovery via visitor pattern -- Category structure: `Splinter` (Lint) → `Performance`/`Security` (groups) → individual rules -- Successfully compiles without errors - ---- - -### Phase 3: Integrate configuration and documentation 📋 PLANNED -Complete integration of splinter into the configuration and documentation systems. - -**Tasks:** -- [ ] **Configuration Generation**: - - [ ] Create `pgls_configuration/src/analyser/splinter/` directory - - [ ] Generate splinter configuration types (groups, rules) - - [ ] Update `generate_configuration.rs` to visit splinter registry - - [ ] Generate `crates/pgls_configuration/src/generated/splinter.rs` - - [ ] Update `analyser/mod.rs` to export splinter config - - [ ] Add splinter to RuleSelector enum - -- [ ] **Documentation Enhancement** (FUTURE): - - [ ] Add SQL query examples to splinter rule docs (similar to linter) - - [ ] Extract SQL from vendor/*.sql files into doc comments - - [ ] Add usage examples and remediation steps - - [ ] Generate rule documentation via docs_codegen - -- [ ] **Runtime Integration** (DEFERRED): - - [ ] Update `run_splinter()` to use visitor pattern with AnalysisFilter - - [ ] Build dynamic SQL queries from enabled rules only - - [ ] Remove hardcoded SQL query execution - - [ ] Remove hardcoded category mapping in convert.rs - -- [ ] **Testing**: - - [ ] Run `just gen-lint` successfully - - [ ] Verify linter configuration still works - - [ ] Verify splinter configuration generates - - [ ] Test enabling/disabling splinter rules via config - - [ ] Verify full workspace compiles - -**Codegen Outputs After Phase 3:** -``` -Linter: - - crates/pgls_analyser/src/registry.rs (generated) - - crates/pgls_analyser/src/options.rs (generated) - - crates/pgls_configuration/src/analyser/linter/ (generated) - - crates/pgls_configuration/src/generated/linter.rs (generated) - -Splinter: - - crates/pgls_splinter/src/rules/ (generated - ✅ DONE) - - crates/pgls_splinter/src/rule.rs (generated - ✅ DONE) - - crates/pgls_splinter/src/registry.rs (generated - ✅ DONE) - - crates/pgls_configuration/src/analyser/splinter/ (TODO) - - crates/pgls_configuration/src/generated/splinter.rs (TODO) -``` - -**Notes:** -- Runtime integration (dynamic SQL query building) is deferred as it requires more complex changes to `run_splinter()` -- Documentation enhancement with SQL examples is marked as FUTURE work -- Focus Phase 3 on configuration integration to enable rule enable/disable via config files - ---- - -### Phase 4: Rename pgls_analyser → pgls_linter 📋 PLANNED -Final rename to clarify purpose. - -**Tasks:** -- [ ] Rename crate in Cargo.toml -- [ ] Update all imports -- [ ] Update documentation -- [ ] Update CLAUDE.md / AGENTS.md -- [ ] Verify tests pass - ---- - -### Phase 5: Runtime & Documentation Enhancements 📋 FUTURE -Advanced features for splinter integration (optional future work). - ---- - -#### **Part A: Configuration Structure Design** - -**Goal:** Create a splinter configuration structure that mirrors linter but shares common code. - -**Shared Components** (from `analyser/mod.rs`): -- `RuleConfiguration` - Configuration wrapper with severity levels -- `RulePlainConfiguration` - Severity enum (Warn, Error, Info, Off) -- Merge/Deserialize traits - -**New Splinter-Specific Types** (to generate in Phase 3): - -```rust -// analyser/splinter/mod.rs -pub struct SplinterConfiguration { - /// Enable/disable splinter linting - pub enabled: bool, - - /// Splinter rules configuration - pub rules: Rules, - - /// Ignore/include patterns (shared with linter) - pub ignore: StringSet, - pub include: StringSet, -} - -// analyser/splinter/rules.rs (GENERATED) -pub enum RuleGroup { - Performance, - Security, -} - -pub struct Rules { - /// Enable recommended rules - pub recommended: Option, - - /// Enable all rules - pub all: Option, - - /// Performance group - pub performance: Option, - - /// Security group - pub security: Option, -} - -// Performance group (GENERATED) -pub struct Performance { - pub recommended: Option, - pub all: Option, - - // Individual rules - note: using RuleConfiguration<()> since no options - pub unindexed_foreign_keys: Option>, - pub auth_rls_initplan: Option>, - pub duplicate_index: Option>, - pub multiple_permissive_policies: Option>, - pub no_primary_key: Option>, - pub table_bloat: Option>, - pub unused_index: Option>, -} - -impl Performance { - const GROUP_NAME: &'static str = "performance"; - const GROUP_RULES: &'static [&'static str] = &[ - "unindexedForeignKeys", - "authRlsInitplan", - // ... all 7 rules - ]; - - // Methods mirroring Safety group - pub fn has_rule(rule_name: &str) -> Option<&'static str> { /* ... */ } - pub fn severity(rule_name: &str) -> Severity { /* ... */ } - pub fn all_rules_as_filters() -> impl Iterator> { /* ... */ } - pub fn recommended_rules_as_filters() -> impl Iterator> { /* ... */ } - pub fn collect_preset_rules(&self, ...) { /* ... */ } - pub fn get_enabled_rules(&self) -> FxHashSet> { /* ... */ } - pub fn get_disabled_rules(&self) -> FxHashSet> { /* ... */ } -} - -// Security group (GENERATED) - same structure, 14 rules -pub struct Security { /* ... */ } -``` - -**Config File Example:** -```json -{ - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "safety": { - "addSerialColumn": "error" - } - } - }, - "splinter": { - "enabled": true, - "rules": { - "recommended": true, - "performance": { - "unindexedForeignKeys": "warn", - "noPrimaryKey": "off" - }, - "security": { - "all": true, - "authUsersExposed": "error" - } - } - } -} -``` - -**Code Sharing Strategy:** -- ✅ Reuse `RuleConfiguration` with `T = ()` for splinter (no rule options) -- ✅ Reuse severity conversion logic -- ✅ Same `as_enabled_rules()` / `as_disabled_rules()` pattern -- ✅ Same methods for each group (has_rule, severity, etc.) -- ⚠️ RuleGroup enum is separate (linter has Safety, splinter has Performance/Security) -- ⚠️ RuleSelector needs updating to handle both "lint/" and "splinter/" prefixes - ---- - -#### **Part B: Dynamic SQL Query Building** - -**Tasks:** -- [ ] Modify `run_splinter()` signature: - ```rust - pub async fn run_splinter( - params: SplinterParams<'_>, - filter: &AnalysisFilter, // NEW - ) -> Result, sqlx::Error> - ``` - -- [ ] Use visitor pattern to collect enabled rules: - ```rust - struct SplinterRuleCollector<'a> { - filter: &'a AnalysisFilter<'a>, - enabled_rules: Vec, // Rule names in camelCase - } - - impl RegistryVisitor for SplinterRuleCollector<'_> { - fn record_rule(&mut self) { - if self.filter.match_rule::() { - self.enabled_rules.push(R::METADATA.name.to_string()); - } - } - } - ``` - -- [ ] Build dynamic SQL query: - ```rust - let mut collector = SplinterRuleCollector { filter, enabled_rules: Vec::new() }; - crate::registry::visit_registry(&mut collector); - - // Map rule names to SQL file paths - let mut sql_queries = Vec::new(); - for rule_name in &collector.enabled_rules { - if let Some(sql_path) = crate::registry::get_sql_file_path(rule_name) { - let sql = std::fs::read_to_string(sql_path)?; - sql_queries.push(sql); - } - } - - // Combine with UNION ALL (only if enabled rules exist) - if sql_queries.is_empty() { - return Ok(Vec::new()); - } - - let combined_sql = sql_queries.join("\nUNION ALL\n"); - let results = sqlx::query_as::<_, SplinterQueryResult>(&combined_sql) - .fetch_all(params.conn) - .await?; - ``` - -- [ ] Remove hardcoded functions: - - Delete `load_generic_splinter_results()` - - Delete `load_supabase_splinter_results()` - - Remove `check_supabase_roles()` logic (rules are filtered by config) - -- [ ] Update convert.rs: - - Use generated category from `RuleMeta::METADATA.category` instead of `rule_name_to_category()` - - Or better: get category from diagnostic category system - ---- - -#### **Part C: Enhanced Documentation** - -**Tasks:** -- [ ] Extract SQL queries into rule doc comments: - ```rust - // In generate_splinter.rs codegen - let sql_content = std::fs::read_to_string(&sql_path)?; - let sql_query = extract_sql_query(&sql_content)?; // Remove metadata comments - - let content = quote! { - /// #title - /// - /// #description - /// - /// ## SQL Query - /// - /// ```sql - /// #sql_query - /// ``` - /// - /// ## Remediation - /// - /// #remediation - pub #struct_name { ... } - }; - ``` - -- [ ] Add example SQL snippets showing what triggers the rule -- [ ] Update docs_codegen to process splinter rules -- [ ] Generate markdown documentation for website - ---- - -#### **Benefits Summary:** - -**Performance:** -- 🚀 Only execute SQL for enabled rules (vs. running all 21 rules) -- 🚀 Skip expensive queries when rules are disabled -- 🚀 Example: User disables 18/21 rules → only 3 SQL queries execute - -**Consistency:** -- ✅ Same enable/disable mechanism as linter -- ✅ Same configuration file structure -- ✅ Same visitor pattern for rule discovery - -**Maintainability:** -- ✅ No hardcoded SQL queries in Rust -- ✅ SQL files remain source of truth -- ✅ Adding new rules = add SQL file + run codegen -- ✅ No manual category mapping needed - -**Documentation:** -- ✅ Rule docs show actual SQL query -- ✅ Better understanding of what each rule does -- ✅ Easier to debug and customize - ---- - -#### **Migration Path:** - -**Phase 3** (Configuration Integration): -1. Generate splinter configuration types -2. Wire into config system -3. Users can enable/disable via config (but all enabled rules still run) - -**Phase 5** (This Phase - Runtime Optimization): -1. Update `run_splinter()` to use filter -2. Build dynamic SQL queries -3. Performance benefit: only enabled rules execute - -This allows incremental rollout - config works in Phase 3, optimization comes in Phase 5. - ---- - -## Progress Tracking - -### Current Status -- [x] Requirements gathering & design -- [x] Architecture proposal (Option C - Hybrid Approach) -- [x] Phase 1: Refactor pgls_analyse - **COMPLETED** -- [x] Phase 2: Enhance pgls_splinter - **COMPLETED** -- [ ] Phase 3: Integrate configuration - **NEXT** -- [ ] Phase 4: Rename to pgls_linter -- [ ] Phase 5: Runtime & Docs (FUTURE) - -### Open Questions -None currently - -### Decisions Made -1. Use `LinterRule` (not `ASTRule` or `SourceCodeRule`) for clarity -2. Use `SplinterRule` for database-level rules -3. Keep codegen in xtask (not build.rs) for consistency -4. Mirror file structure between linter and splinter - ---- - -## Testing Strategy -- [ ] Existing linter tests continue to pass -- [ ] Splinter rules generate correctly from SQL -- [ ] Configuration schema validates -- [ ] Integration test: enable/disable rules via config -- [ ] Integration test: severity overrides work - ---- - -Last updated: 2025-12-14