From 0e3228934ffbf980dba6c9785f7192a56261befa Mon Sep 17 00:00:00 2001 From: psteinroe Date: Tue, 4 Nov 2025 10:06:14 +0100 Subject: [PATCH 1/4] refactor: config codegen --- crates/pgls_configuration/src/generated.rs | 3 - .../src/generated/linter.rs | 19 - crates/pgls_configuration/src/lib.rs | 18 +- .../src/{analyser => }/linter/mod.rs | 24 +- .../src/{analyser => }/linter/rules.rs | 19 +- .../mod.rs => rules/configuration.rs} | 92 -- crates/pgls_configuration/src/rules/mod.rs | 9 + .../pgls_configuration/src/rules/selector.rs | 92 ++ crates/pgls_workspace/src/settings.rs | 6 +- xtask/codegen/src/generate_configuration.rs | 1248 ++++++++++------- xtask/codegen/src/lib.rs | 2 +- 11 files changed, 886 insertions(+), 646 deletions(-) delete mode 100644 crates/pgls_configuration/src/generated.rs delete mode 100644 crates/pgls_configuration/src/generated/linter.rs rename crates/pgls_configuration/src/{analyser => }/linter/mod.rs (69%) rename crates/pgls_configuration/src/{analyser => }/linter/rules.rs (98%) rename crates/pgls_configuration/src/{analyser/mod.rs => rules/configuration.rs} (75%) create mode 100644 crates/pgls_configuration/src/rules/mod.rs create mode 100644 crates/pgls_configuration/src/rules/selector.rs diff --git a/crates/pgls_configuration/src/generated.rs b/crates/pgls_configuration/src/generated.rs deleted file mode 100644 index 3bae7b808..000000000 --- a/crates/pgls_configuration/src/generated.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod linter; - -pub use linter::push_to_analyser_rules; diff --git a/crates/pgls_configuration/src/generated/linter.rs b/crates/pgls_configuration/src/generated/linter.rs deleted file mode 100644 index 4f4ece03d..000000000 --- a/crates/pgls_configuration/src/generated/linter.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Generated file, do not edit by hand, see `xtask/codegen` - -use crate::analyser::linter::*; -use pgls_analyse::{AnalyserRules, MetadataRegistry}; -pub fn push_to_analyser_rules( - rules: &Rules, - metadata: &MetadataRegistry, - analyser_rules: &mut AnalyserRules, -) { - if let Some(rules) = rules.safety.as_ref() { - for rule_name in Safety::GROUP_RULES { - if let Some((_, Some(rule_options))) = rules.get_rule_configuration(rule_name) { - if let Some(rule_key) = metadata.find_rule("safety", rule_name) { - analyser_rules.push_rule(rule_key, rule_options); - } - } - } - } -} diff --git a/crates/pgls_configuration/src/lib.rs b/crates/pgls_configuration/src/lib.rs index 0f6707b05..63c0a2e53 100644 --- a/crates/pgls_configuration/src/lib.rs +++ b/crates/pgls_configuration/src/lib.rs @@ -2,13 +2,13 @@ //! //! The configuration is divided by "tool". -pub mod analyser; pub mod database; pub mod diagnostics; pub mod files; -pub mod generated; +pub mod linter; pub mod migrations; pub mod plpgsql_check; +pub mod rules; pub mod typecheck; pub mod vcs; @@ -16,13 +16,7 @@ pub use crate::diagnostics::ConfigurationDiagnostic; use std::path::PathBuf; -pub use crate::generated::push_to_analyser_rules; use crate::vcs::{PartialVcsConfiguration, VcsConfiguration, partial_vcs_configuration}; -pub use analyser::{ - LinterConfiguration, PartialLinterConfiguration, RuleConfiguration, RuleFixConfiguration, - RulePlainConfiguration, RuleSelector, RuleWithFixOptions, RuleWithOptions, Rules, - partial_linter_configuration, -}; use biome_deserialize::StringSet; use biome_deserialize_macros::{Merge, Partial}; use bpaf::Bpaf; @@ -30,6 +24,10 @@ use database::{ DatabaseConfiguration, PartialDatabaseConfiguration, partial_database_configuration, }; use files::{FilesConfiguration, PartialFilesConfiguration, partial_files_configuration}; +pub use linter::{ + LinterConfiguration, PartialLinterConfiguration, Rules, partial_linter_configuration, + push_to_analyser_rules, +}; use migrations::{ MigrationsConfiguration, PartialMigrationsConfiguration, partial_migrations_configuration, }; @@ -38,6 +36,10 @@ use plpgsql_check::{ PartialPlPgSqlCheckConfiguration, PlPgSqlCheckConfiguration, partial_pl_pg_sql_check_configuration, }; +pub use rules::{ + RuleConfiguration, RuleFixConfiguration, RulePlainConfiguration, RuleSelector, + RuleWithFixOptions, RuleWithOptions, +}; use serde::{Deserialize, Serialize}; pub use typecheck::{ PartialTypecheckConfiguration, TypecheckConfiguration, partial_typecheck_configuration, diff --git a/crates/pgls_configuration/src/analyser/linter/mod.rs b/crates/pgls_configuration/src/linter/mod.rs similarity index 69% rename from crates/pgls_configuration/src/analyser/linter/mod.rs rename to crates/pgls_configuration/src/linter/mod.rs index 20535a2e7..76e463518 100644 --- a/crates/pgls_configuration/src/analyser/linter/mod.rs +++ b/crates/pgls_configuration/src/linter/mod.rs @@ -1,41 +1,37 @@ -mod rules; +//! Generated file, do not edit by hand, see `xtask/codegen` +#![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] +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 LinterConfiguration { - /// if `false`, it disables the feature and the linter won't be executed. `true` by default + #[doc = r" if `false`, it disables the feature and the linter won't be executed. `true` by default"] #[partial(bpaf(hide))] pub enabled: bool, - - /// List of rules + #[doc = r" List of rules"] #[partial(bpaf(pure(Default::default()), optional, hide))] pub rules: Rules, - - /// A list of Unix shell style patterns. The formatter will ignore files/folders that will - /// match these patterns. + #[doc = r" A list of Unix shell style patterns. The formatter will ignore files/folders that will"] + #[doc = r" match these patterns."] #[partial(bpaf(hide))] pub ignore: StringSet, - - /// A list of Unix shell style patterns. The formatter will include files/folders that will - /// match these patterns. + #[doc = r" A list of Unix shell style patterns. The formatter will include files/folders that will"] + #[doc = r" match these patterns."] #[partial(bpaf(hide))] pub include: StringSet, } - impl LinterConfiguration { pub const fn is_disabled(&self) -> bool { !self.enabled } } - impl Default for LinterConfiguration { fn default() -> Self { Self { @@ -46,12 +42,10 @@ impl Default for LinterConfiguration { } } } - impl PartialLinterConfiguration { 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/linter/rules.rs b/crates/pgls_configuration/src/linter/rules.rs similarity index 98% rename from crates/pgls_configuration/src/analyser/linter/rules.rs rename to crates/pgls_configuration/src/linter/rules.rs index 78d671b9b..5e27b92d1 100644 --- a/crates/pgls_configuration/src/analyser/linter/rules.rs +++ b/crates/pgls_configuration/src/linter/rules.rs @@ -1,6 +1,7 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -use crate::analyser::{RuleConfiguration, RulePlainConfiguration}; +#![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_diagnostics::{Category, Severity}; @@ -902,6 +903,22 @@ impl Safety { } } } +#[doc = r" Push the configured rules to the analyser"] +pub fn push_to_analyser_rules( + rules: &Rules, + metadata: &pgls_analyse::MetadataRegistry, + analyser_rules: &mut pgls_analyse::AnalyserRules, +) { + if let Some(rules) = rules.safety.as_ref() { + for rule_name in Safety::GROUP_RULES { + if let Some((_, Some(rule_options))) = rules.get_rule_configuration(rule_name) { + if let Some(rule_key) = metadata.find_rule("safety", rule_name) { + analyser_rules.push_rule(rule_key, rule_options); + } + } + } + } +} #[test] fn test_order() { for items in Safety::GROUP_RULES.windows(2) { diff --git a/crates/pgls_configuration/src/analyser/mod.rs b/crates/pgls_configuration/src/rules/configuration.rs similarity index 75% rename from crates/pgls_configuration/src/analyser/mod.rs rename to crates/pgls_configuration/src/rules/configuration.rs index b6ac67e91..a3e43f63c 100644 --- a/crates/pgls_configuration/src/analyser/mod.rs +++ b/crates/pgls_configuration/src/rules/configuration.rs @@ -1,15 +1,10 @@ -pub mod linter; - -pub use crate::analyser::linter::*; use biome_deserialize::Merge; use biome_deserialize_macros::Deserializable; -use pgls_analyse::RuleFilter; use pgls_analyse::options::RuleOptions; use pgls_diagnostics::Severity; #[cfg(feature = "schema")] use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::str::FromStr; #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[cfg_attr(feature = "schema", derive(JsonSchema))] @@ -300,90 +295,3 @@ impl Merge for RuleWithFixOptions { self.options = other.options; } } - -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -pub enum RuleSelector { - Group(linter::RuleGroup), - Rule(linter::RuleGroup, &'static str), -} - -impl From for RuleFilter<'static> { - fn from(value: RuleSelector) -> Self { - match value { - RuleSelector::Group(group) => RuleFilter::Group(group.as_str()), - RuleSelector::Rule(group, name) => RuleFilter::Rule(group.as_str(), name), - } - } -} - -impl<'a> From<&'a RuleSelector> for RuleFilter<'static> { - fn from(value: &'a RuleSelector) -> Self { - match value { - RuleSelector::Group(group) => RuleFilter::Group(group.as_str()), - RuleSelector::Rule(group, name) => RuleFilter::Rule(group.as_str(), name), - } - } -} - -impl FromStr for RuleSelector { - type Err = &'static str; - fn from_str(selector: &str) -> Result { - let selector = selector.strip_prefix("lint/").unwrap_or(selector); - if let Some((group_name, rule_name)) = selector.split_once('/') { - let group = linter::RuleGroup::from_str(group_name)?; - if let Some(rule_name) = Rules::has_rule(group, rule_name) { - Ok(RuleSelector::Rule(group, rule_name)) - } else { - Err("This rule doesn't exist.") - } - } else { - match linter::RuleGroup::from_str(selector) { - Ok(group) => Ok(RuleSelector::Group(group)), - Err(_) => Err( - "This group doesn't exist. Use the syntax `/` to specify a rule.", - ), - } - } - } -} - -impl serde::Serialize for RuleSelector { - fn serialize(&self, serializer: S) -> Result { - match self { - RuleSelector::Group(group) => serializer.serialize_str(group.as_str()), - RuleSelector::Rule(group, rule_name) => { - let group_name = group.as_str(); - serializer.serialize_str(&format!("{group_name}/{rule_name}")) - } - } - } -} - -impl<'de> serde::Deserialize<'de> for RuleSelector { - fn deserialize>(deserializer: D) -> Result { - struct Visitor; - impl serde::de::Visitor<'_> for Visitor { - type Value = RuleSelector; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("/") - } - fn visit_str(self, v: &str) -> Result { - match RuleSelector::from_str(v) { - Ok(result) => Ok(result), - Err(error) => Err(serde::de::Error::custom(error)), - } - } - } - deserializer.deserialize_str(Visitor) - } -} - -#[cfg(feature = "schema")] -impl schemars::JsonSchema for RuleSelector { - fn schema_name() -> String { - "RuleCode".to_string() - } - fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - String::json_schema(r#gen) - } -} diff --git a/crates/pgls_configuration/src/rules/mod.rs b/crates/pgls_configuration/src/rules/mod.rs new file mode 100644 index 000000000..1fb0cb0ab --- /dev/null +++ b/crates/pgls_configuration/src/rules/mod.rs @@ -0,0 +1,9 @@ +pub(crate) mod configuration; +pub(crate) mod selector; + +pub use configuration::{ + RuleAssistConfiguration, RuleAssistPlainConfiguration, RuleAssistWithOptions, + RuleConfiguration, RuleFixConfiguration, RulePlainConfiguration, RuleWithFixOptions, + RuleWithOptions, +}; +pub use selector::RuleSelector; diff --git a/crates/pgls_configuration/src/rules/selector.rs b/crates/pgls_configuration/src/rules/selector.rs new file mode 100644 index 000000000..1851403a8 --- /dev/null +++ b/crates/pgls_configuration/src/rules/selector.rs @@ -0,0 +1,92 @@ +use pgls_analyse::RuleFilter; + +use std::str::FromStr; + +use crate::{Rules, linter::RuleGroup}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub enum RuleSelector { + Group(RuleGroup), + Rule(RuleGroup, &'static str), +} + +impl From for RuleFilter<'static> { + fn from(value: RuleSelector) -> Self { + match value { + RuleSelector::Group(group) => RuleFilter::Group(group.as_str()), + RuleSelector::Rule(group, name) => RuleFilter::Rule(group.as_str(), name), + } + } +} + +impl<'a> From<&'a RuleSelector> for RuleFilter<'static> { + fn from(value: &'a RuleSelector) -> Self { + match value { + RuleSelector::Group(group) => RuleFilter::Group(group.as_str()), + RuleSelector::Rule(group, name) => RuleFilter::Rule(group.as_str(), name), + } + } +} + +impl FromStr for RuleSelector { + type Err = &'static str; + fn from_str(selector: &str) -> Result { + let selector = selector.strip_prefix("lint/").unwrap_or(selector); + if let Some((group_name, rule_name)) = selector.split_once('/') { + let group = RuleGroup::from_str(group_name)?; + if let Some(rule_name) = Rules::has_rule(group, rule_name) { + Ok(RuleSelector::Rule(group, rule_name)) + } else { + Err("This rule doesn't exist.") + } + } else { + match RuleGroup::from_str(selector) { + Ok(group) => Ok(RuleSelector::Group(group)), + Err(_) => Err( + "This group doesn't exist. Use the syntax `/` to specify a rule.", + ), + } + } + } +} + +impl serde::Serialize for RuleSelector { + fn serialize(&self, serializer: S) -> Result { + match self { + RuleSelector::Group(group) => serializer.serialize_str(group.as_str()), + RuleSelector::Rule(group, rule_name) => { + let group_name = group.as_str(); + serializer.serialize_str(&format!("{group_name}/{rule_name}")) + } + } + } +} + +impl<'de> serde::Deserialize<'de> for RuleSelector { + fn deserialize>(deserializer: D) -> Result { + struct Visitor; + impl serde::de::Visitor<'_> for Visitor { + type Value = RuleSelector; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("/") + } + fn visit_str(self, v: &str) -> Result { + match RuleSelector::from_str(v) { + Ok(result) => Ok(result), + Err(error) => Err(serde::de::Error::custom(error)), + } + } + } + deserializer.deserialize_str(Visitor) + } +} + +#[cfg(feature = "schema")] +impl schemars::JsonSchema for RuleSelector { + fn schema_name() -> String { + "RuleCode".to_string() + } + fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + String::json_schema(r#gen) + } +} diff --git a/crates/pgls_workspace/src/settings.rs b/crates/pgls_workspace/src/settings.rs index 08b29f7e3..9125b7642 100644 --- a/crates/pgls_workspace/src/settings.rs +++ b/crates/pgls_workspace/src/settings.rs @@ -282,7 +282,7 @@ impl Settings { } /// Returns linter rules. - pub fn as_linter_rules(&self) -> Option> { + pub fn as_linter_rules(&self) -> Option> { self.linter.rules.as_ref().map(Cow::Borrowed) } @@ -414,7 +414,7 @@ pub struct LinterSettings { pub enabled: bool, /// List of rules - pub rules: Option, + pub rules: Option, /// List of ignored paths/files to match pub ignored_files: Matcher, @@ -427,7 +427,7 @@ impl Default for LinterSettings { fn default() -> Self { Self { enabled: true, - rules: Some(pgls_configuration::analyser::linter::Rules::default()), + rules: Some(pgls_configuration::linter::Rules::default()), ignored_files: Matcher::empty(), included_files: Matcher::empty(), } diff --git a/xtask/codegen/src/generate_configuration.rs b/xtask/codegen/src/generate_configuration.rs index 741fc8f06..d9c677619 100644 --- a/xtask/codegen/src/generate_configuration.rs +++ b/xtask/codegen/src/generate_configuration.rs @@ -6,17 +6,85 @@ use proc_macro2::{Ident, Literal, Span, TokenStream}; use pulldown_cmark::{Event, Parser, Tag, TagEnd}; use quote::quote; use std::collections::BTreeMap; -use std::path::Path; +use std::path::PathBuf; use xtask::*; -#[derive(Default)] -struct LintRulesVisitor { +/// Configuration for a tool that produces rules +struct ToolConfig { + name: &'static str, + category: RuleCategory, +} + +impl ToolConfig { + const fn new(name: &'static str, category: RuleCategory) -> Self { + Self { name, category } + } + + /// Derived: Directory name under pgls_configuration/src/ + fn config_dir(&self) -> &str { + self.name + } + + /// Derived: Crate name that contains the rules + fn crate_name(&self) -> String { + format!("pgls_{}", self.name) + } + + /// Derived: The main struct name (Rules, Actions, or Transformations) + fn struct_name(&self) -> &str { + match self.category { + RuleCategory::Lint => "Rules", + RuleCategory::Action => "Actions", + RuleCategory::Transformation => "Transformations", + } + } + + /// Derived: The generated file name (rules.rs, actions.rs, or transformations.rs) + fn generated_file(&self) -> &str { + match self.category { + RuleCategory::Lint => "rules.rs", + RuleCategory::Action => "actions.rs", + RuleCategory::Transformation => "transformations.rs", + } + } + + /// Derived: Configuration struct name (LinterConfiguration, AssistsConfiguration, etc.) + fn config_struct_name(&self) -> String { + format!("{}Configuration", to_capitalized(self.name)) + } + + /// Derived: Partial configuration struct name + fn partial_config_struct_name(&self) -> String { + format!("Partial{}", self.config_struct_name()) + } +} + +/// All supported tools +const TOOLS: &[ToolConfig] = &[ + ToolConfig::new("linter", RuleCategory::Lint), + ToolConfig::new("assists", RuleCategory::Action), + ToolConfig::new("splinter", RuleCategory::Lint), + ToolConfig::new("pglinter", RuleCategory::Lint), +]; + +/// Visitor that collects rules for a specific category +struct CategoryRulesVisitor { + category: RuleCategory, groups: BTreeMap<&'static str, BTreeMap<&'static str, RuleMetadata>>, } -impl RegistryVisitor for LintRulesVisitor { +impl CategoryRulesVisitor { + fn new(category: RuleCategory) -> Self { + Self { + category, + groups: BTreeMap::new(), + } + } +} + +impl RegistryVisitor for CategoryRulesVisitor { fn record_category(&mut self) { - if matches!(C::CATEGORY, RuleCategory::Lint) { + if C::CATEGORY == self.category { C::record_groups(self); } } @@ -32,30 +100,139 @@ impl RegistryVisitor for LintRulesVisitor { } } +/// Generate all rule configurations pub fn generate_rules_configuration(mode: Mode) -> Result<()> { - let linter_config_root = project_root().join("crates/pgls_configuration/src/analyser/linter"); - let push_rules_directory = project_root().join("crates/pgls_configuration/src/generated"); - - let mut lint_visitor = LintRulesVisitor::default(); - pgls_analyser::visit_registry(&mut lint_visitor); - - generate_for_groups( - lint_visitor.groups, - linter_config_root.as_path(), - push_rules_directory.as_path(), - &mode, - RuleCategory::Lint, - )?; + // Currently we only generate for the linter tool + generate_tool_configuration(mode, "linter")?; Ok(()) } -fn generate_for_groups( +/// Main entry point for generating tool configuration +pub fn generate_tool_configuration(mode: Mode, tool_name: &str) -> Result<()> { + let tool = TOOLS + .iter() + .find(|t| t.name == tool_name) + .ok_or_else(|| anyhow::anyhow!("Unknown tool: {}", tool_name))?; + + let config_root = project_root().join("crates/pgls_configuration/src"); + let tool_dir = config_root.join(tool.config_dir()); + + // Collect rules from the tool's crate + let mut visitor = CategoryRulesVisitor::new(tool.category); + + // For now, only linter is implemented + match tool.name { + "linter" => pgls_analyser::visit_registry(&mut visitor), + "assists" => unimplemented!("Assists rules not yet implemented"), + "splinter" => unimplemented!("Splinter rules not yet implemented"), + "pglinter" => unimplemented!("PGLinter rules not yet implemented"), + _ => unreachable!(), + } + + // Generate configuration files based on category + let (mod_content, rules_content) = match tool.category { + RuleCategory::Lint => generate_lint_config(tool, visitor.groups)?, + RuleCategory::Action => generate_action_config(tool, visitor.groups)?, + RuleCategory::Transformation => { + unimplemented!("Transformation category generation not yet implemented") + } + }; + + // Write generated files + update(&tool_dir.join("mod.rs"), &mod_content, &mode)?; + update(&tool_dir.join(tool.generated_file()), &rules_content, &mode)?; + + Ok(()) +} + +/// Generate configuration files for Lint category tools +fn generate_lint_config( + tool: &ToolConfig, groups: BTreeMap<&'static str, BTreeMap<&'static str, RuleMetadata>>, - root: &Path, - push_directory: &Path, - mode: &Mode, - kind: RuleCategory, -) -> Result<()> { +) -> Result<(String, String)> { + let mod_file = generate_lint_mod_file(tool); + let rules_file = generate_lint_rules_file(tool, groups)?; + Ok((mod_file, rules_file)) +} + +/// Generate the mod.rs file for a Lint tool +fn generate_lint_mod_file(tool: &ToolConfig) -> String { + let config_struct = Ident::new(&tool.config_struct_name(), Span::call_site()); + let partial_config_struct = Ident::new(&tool.partial_config_struct_name(), Span::call_site()); + let generated_file = tool.generated_file().trim_end_matches(".rs"); + let generated_file_ident = Ident::new(generated_file, Span::call_site()); + + let content = quote! { + //! Generated file, do not edit by hand, see `xtask/codegen` + + mod #generated_file_ident; + + use biome_deserialize::StringSet; + use biome_deserialize_macros::{Merge, Partial}; + use bpaf::Bpaf; + pub use #generated_file_ident::*; + 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 #config_struct { + /// if `false`, it disables the feature and the linter 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 formatter will ignore files/folders that will + /// match these patterns. + #[partial(bpaf(hide))] + pub ignore: StringSet, + + /// A list of Unix shell style patterns. The formatter will include files/folders that will + /// match these patterns. + #[partial(bpaf(hide))] + pub include: StringSet, + } + + impl #config_struct { + pub const fn is_disabled(&self) -> bool { + !self.enabled + } + } + + impl Default for #config_struct { + fn default() -> Self { + Self { + enabled: true, + rules: Default::default(), + ignore: Default::default(), + include: Default::default(), + } + } + } + + impl #partial_config_struct { + pub const fn is_disabled(&self) -> bool { + matches!(self.enabled, Some(false)) + } + + pub fn get_rules(&self) -> Rules { + self.rules.clone().unwrap_or_default() + } + } + }; + + xtask::reformat(content.to_string()).unwrap() +} + +/// Generate the rules.rs file for a Lint tool +fn generate_lint_rules_file( + tool: &ToolConfig, + groups: BTreeMap<&'static str, BTreeMap<&'static str, RuleMetadata>>, +) -> Result { let mut struct_groups = Vec::with_capacity(groups.len()); let mut group_pascal_idents = Vec::with_capacity(groups.len()); let mut group_idents = Vec::with_capacity(groups.len()); @@ -73,27 +250,20 @@ fn generate_for_groups( quote! { !self.is_recommended_false() }, ) }; - group_as_default_rules.push(if kind == RuleCategory::Lint { - quote! { - if let Some(group) = self.#group_ident.as_ref() { - group.collect_preset_rules( - #global_all, - #global_recommended, - &mut enabled_rules, - ); - enabled_rules.extend(&group.get_enabled_rules()); - disabled_rules.extend(&group.get_disabled_rules()); - } else if #global_all { - enabled_rules.extend(#group_pascal_ident::all_rules_as_filters()); - } else if #global_recommended { - enabled_rules.extend(#group_pascal_ident::recommended_rules_as_filters()); - } - } - } else { - quote! { - if let Some(group) = self.#group_ident.as_ref() { - enabled_rules.extend(&group.get_enabled_rules()); - } + + group_as_default_rules.push(quote! { + if let Some(group) = self.#group_ident.as_ref() { + group.collect_preset_rules( + #global_all, + #global_recommended, + &mut enabled_rules, + ); + enabled_rules.extend(&group.get_enabled_rules()); + disabled_rules.extend(&group.get_disabled_rules()); + } else if #global_all { + enabled_rules.extend(#group_pascal_ident::all_rules_as_filters()); + } else if #global_recommended { + enabled_rules.extend(#group_pascal_ident::recommended_rules_as_filters()); } }); @@ -106,42 +276,78 @@ fn generate_for_groups( group_pascal_idents.push(group_pascal_ident); group_idents.push(group_ident); group_strings.push(Literal::string(group)); - struct_groups.push(generate_group_struct(group, &rules, kind)); + struct_groups.push(generate_lint_group_struct(group, &rules)); } - let severity_fn = if kind == RuleCategory::Action { - quote! { - /// Given a category coming from [Diagnostic](pgls_diagnostics::Diagnostic), this function returns - /// the [Severity](pgls_diagnostics::Severity) associated to the rule, if the configuration changed it. - /// If the severity is off or not set, then the function returns the default severity of the rule: - /// [Severity::Error] for recommended rules and [Severity::Warning] for other rules. - /// - /// If not, the function returns [None]. - pub fn get_severity_from_code(&self, category: &Category) -> Option { - let mut split_code = category.name().split('/'); + let rules_struct_content = quote! { + //! 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_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 { + #( #group_pascal_idents ),* + } - let _lint = split_code.next(); - debug_assert_eq!(_lint, Some("assists")); + impl RuleGroup { + pub const fn as_str(self) -> &'static str { + match self { + #( Self::#group_pascal_idents => #group_pascal_idents::GROUP_NAME, )* + } + } + } - let group = ::from_str(split_code.next()?).ok()?; - let rule_name = split_code.next()?; - let rule_name = Self::has_rule(group, rule_name)?; + impl std::str::FromStr for RuleGroup { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s { + #( #group_pascal_idents::GROUP_NAME => Ok(Self::#group_pascal_idents), )* + _ => 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 { + /// It enables the lint rules recommended by Postgres Language Server. `true` by default. + #[serde(skip_serializing_if = "Option::is_none")] + pub recommended: Option, + + /// 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 #group_idents: Option<#group_pascal_idents>, + )* + } + + impl Rules { + /// Checks if the code coming from [pgls_diagnostics::Diagnostic] corresponds to a rule. + /// Usually the code is built like {group}/{rule_name} + pub fn has_rule( + group: RuleGroup, + rule_name: &str, + ) -> Option<&'static str> { match group { #( - RuleGroup::#group_pascal_idents => self - .#group_idents - .as_ref() - .and_then(|group| group.get_rule_configuration(rule_name)) - .filter(|(level, _)| !matches!(level, RuleAssistPlainConfiguration::Off)) - .map(|(level, _)| level.into()) + RuleGroup::#group_pascal_idents => #group_pascal_idents::has_rule(rule_name), )* } } - } - } else { - quote! { - /// Given a category coming from [Diagnostic](pgls_diagnostics::Diagnostic), this function returns /// the [Severity](pgls_diagnostics::Severity) associated to the rule, if the configuration changed it. /// If the severity is off or not set, then the function returns the default severity of the rule, @@ -172,305 +378,83 @@ fn generate_for_groups( Some(severity) } - } - }; - - let use_rule_configuration = if kind == RuleCategory::Action { - quote! { - use crate::analyser::{RuleAssistConfiguration, RuleAssistPlainConfiguration}; - use pgls_analyse::{RuleFilter, options::RuleOptions}; - } - } else { - quote! { - use crate::analyser::{RuleConfiguration, RulePlainConfiguration}; - use pgls_analyse::{RuleFilter, options::RuleOptions}; - } - }; - - let groups = if kind == RuleCategory::Action { - quote! { - #use_rule_configuration - use biome_deserialize_macros::Merge; - use pgls_diagnostics::{Category, Severity}; - use rustc_hash::FxHashSet; - use serde::{Deserialize, Serialize}; - #[cfg(feature = "schema")] - use schemars::JsonSchema; - - #[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 { - #( #group_pascal_idents ),* - } - impl RuleGroup { - pub const fn as_str(self) -> &'static str { - match self { - #( Self::#group_pascal_idents => #group_pascal_idents::GROUP_NAME, )* - } - } - } - impl std::str::FromStr for RuleGroup { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - #( #group_pascal_idents::GROUP_NAME => Ok(Self::#group_pascal_idents), )* - _ => 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 Actions { - #( - #[serde(skip_serializing_if = "Option::is_none")] - pub #group_idents: Option<#group_pascal_idents>, - )* - } - - impl Actions { - /// Checks if the code coming from [pgls_diagnostics::Diagnostic] corresponds to a rule. - /// Usually the code is built like {group}/{rule_name} - pub fn has_rule( - group: RuleGroup, - rule_name: &str, - ) -> Option<&'static str> { - match group { - #( - RuleGroup::#group_pascal_idents => #group_pascal_idents::has_rule(rule_name), - )* - } - } - - #severity_fn - - /// It returns the enabled rules by default. - /// - /// 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(); - #( #group_as_default_rules )* - enabled_rules + /// 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) } - - /// It returns the disabled rules by configuration. - pub fn as_disabled_rules(&self) -> FxHashSet> { - let mut disabled_rules = FxHashSet::default(); - #( #group_as_disabled_rules )* - disabled_rules - } - } - - #( #struct_groups )* - - #[test] - fn test_order() { #( - for items in #group_pascal_idents::GROUP_RULES.windows(2) { - assert!(items[0] < items[1], "{} < {}", items[0], items[1]); + if let Some(group) = &mut self.#group_idents { + group.recommended = None; } )* } - } - } else { - quote! { - #use_rule_configuration - use biome_deserialize_macros::Merge; - use pgls_diagnostics::{Category, Severity}; - use rustc_hash::FxHashSet; - use serde::{Deserialize, Serialize}; - #[cfg(feature = "schema")] - use schemars::JsonSchema; - - #[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 { - #( #group_pascal_idents ),* - } - impl RuleGroup { - pub const fn as_str(self) -> &'static str { - match self { - #( Self::#group_pascal_idents => #group_pascal_idents::GROUP_NAME, )* - } - } - } - impl std::str::FromStr for RuleGroup { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - #( #group_pascal_idents::GROUP_NAME => Ok(Self::#group_pascal_idents), )* - _ => 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 { - /// It enables the lint rules recommended by Postgres Language Server. `true` by default. - #[serde(skip_serializing_if = "Option::is_none")] - pub recommended: Option, - - /// 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 #group_idents: Option<#group_pascal_idents>, - )* + pub(crate) const fn is_recommended_false(&self) -> bool { + matches!(self.recommended, Some(false)) } - impl Rules { - /// Checks if the code coming from [pgls_diagnostics::Diagnostic] corresponds to a rule. - /// Usually the code is built like {group}/{rule_name} - pub fn has_rule( - group: RuleGroup, - rule_name: &str, - ) -> Option<&'static str> { - match group { - #( - RuleGroup::#group_pascal_idents => #group_pascal_idents::has_rule(rule_name), - )* - } - } - - #severity_fn - - /// 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.#group_idents { - group.recommended = None; - } - )* - } - - // Note: In top level, it is only considered _not_ recommended - // when the recommended option is false - 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)) - } - - /// It returns the enabled rules by default. - /// - /// 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(); - #( #group_as_default_rules )* + pub(crate) const fn is_all_true(&self) -> bool { + matches!(self.all, Some(true)) + } - enabled_rules.difference(&disabled_rules).copied().collect() - } + /// It returns the enabled rules by default. + /// + /// 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(); + #( #group_as_default_rules )* - /// It returns the disabled rules by configuration. - pub fn as_disabled_rules(&self) -> FxHashSet> { - let mut disabled_rules = FxHashSet::default(); - #( #group_as_disabled_rules )* - disabled_rules - } + enabled_rules.difference(&disabled_rules).copied().collect() } - #( #struct_groups )* - - #[test] - fn test_order() { - #( - for items in #group_pascal_idents::GROUP_RULES.windows(2) { - assert!(items[0] < items[1], "{} < {}", items[0], items[1]); - } - )* + /// It returns the disabled rules by configuration. + pub fn as_disabled_rules(&self) -> FxHashSet> { + let mut disabled_rules = FxHashSet::default(); + #( #group_as_disabled_rules )* + disabled_rules } } - }; - let push_rules = match kind { - RuleCategory::Lint => { - quote! { - use crate::analyser::linter::*; - use pgls_analyse::{AnalyserRules, MetadataRegistry}; - - pub fn push_to_analyser_rules( - rules: &Rules, - metadata: &MetadataRegistry, - analyser_rules: &mut AnalyserRules, - ) { - #( - if let Some(rules) = rules.#group_idents.as_ref() { - for rule_name in #group_pascal_idents::GROUP_RULES { - if let Some((_, Some(rule_options))) = rules.get_rule_configuration(rule_name) { - if let Some(rule_key) = metadata.find_rule(#group_strings, rule_name) { - analyser_rules.push_rule(rule_key, rule_options); - } - } + #( #struct_groups )* + + /// Push the configured rules to the analyser + pub fn push_to_analyser_rules( + rules: &Rules, + metadata: &pgls_analyse::MetadataRegistry, + analyser_rules: &mut pgls_analyse::AnalyserRules, + ) { + #( + if let Some(rules) = rules.#group_idents.as_ref() { + for rule_name in #group_pascal_idents::GROUP_RULES { + if let Some((_, Some(rule_options))) = rules.get_rule_configuration(rule_name) { + if let Some(rule_key) = metadata.find_rule(#group_strings, rule_name) { + analyser_rules.push_rule(rule_key, rule_options); } } - )* + } } - } + )* } - RuleCategory::Action => { - quote! { - use crate::analyser::assists::*; - use pgls_analyse::{AnalyserRules, MetadataRegistry}; - - pub fn push_to_analyser_assists( - rules: &Actions, - metadata: &MetadataRegistry, - analyser_rules: &mut AnalyserRules, - ) { - #( - if let Some(rules) = rules.#group_idents.as_ref() { - for rule_name in #group_pascal_idents::GROUP_RULES { - if let Some((_, Some(rule_options))) = rules.get_rule_configuration(rule_name) { - if let Some(rule_key) = metadata.find_rule(#group_strings, rule_name) { - analyser_rules.push_rule(rule_key, rule_options); - } - } - } - } - )* + + #[test] + fn test_order() { + #( + for items in #group_pascal_idents::GROUP_RULES.windows(2) { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); } - } + )* } - RuleCategory::Transformation => unimplemented!(), }; - let configuration = groups.to_string(); - let push_rules = push_rules.to_string(); - - let file_name = match kind { - RuleCategory::Lint => &push_directory.join("linter.rs"), - RuleCategory::Action => &push_directory.join("assists.rs"), - RuleCategory::Transformation => unimplemented!(), - }; - - let path = if kind == RuleCategory::Action { - &root.join("actions.rs") - } else { - &root.join("rules.rs") - }; - update(path, &xtask::reformat(configuration)?, mode)?; - update(file_name, &xtask::reformat(push_rules)?, mode)?; - - Ok(()) + Ok(xtask::reformat(rules_struct_content.to_string())?) } -fn generate_group_struct( +/// Generate a group struct for lint rules +fn generate_lint_group_struct( group: &str, rules: &BTreeMap<&'static str, RuleMetadata>, - kind: RuleCategory, ) -> TokenStream { let mut lines_recommended_rule_as_filter = Vec::new(); let mut lines_all_rule_as_filter = Vec::new(); @@ -482,76 +466,30 @@ fn generate_group_struct( let mut get_severity_lines = Vec::new(); for (index, (rule, metadata)) in rules.iter().enumerate() { - let summary = { - let mut docs = String::new(); - let parser = Parser::new(metadata.docs); - for event in parser { - match event { - Event::Text(text) => { - docs.push_str(text.as_ref()); - } - Event::Code(text) => { - // Escape `[` and `<` to obtain valid Markdown - docs.push_str(text.replace('[', "\\[").replace('<', "\\<").as_ref()); - } - Event::SoftBreak => { - docs.push(' '); - } - - Event::Start(Tag::Paragraph) => {} - Event::End(TagEnd::Paragraph) => { - break; - } - - Event::Start(tag) => match tag { - Tag::Strong | Tag::Paragraph => { - continue; - } - - _ => panic!("Unimplemented tag {:?}", { tag }), - }, - - Event::End(tag) => match tag { - TagEnd::Strong | TagEnd::Paragraph => { - continue; - } - _ => panic!("Unimplemented tag {:?}", { tag }), - }, - - _ => { - panic!("Unimplemented event {:?}", { event }) - } - } - } - docs - }; - + let summary = extract_summary_from_docs(metadata.docs); let rule_position = Literal::u8_unsuffixed(index as u8); let rule_identifier = quote::format_ident!("{}", Case::Snake.convert(rule)); - let rule_config_type = quote::format_ident!( - "{}", - if kind == RuleCategory::Action { - "RuleAssistConfiguration" - } else { - "RuleConfiguration" - } - ); let rule_name = Ident::new(&to_capitalized(rule), Span::call_site()); + if metadata.recommended { lines_recommended_rule_as_filter.push(quote! { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[#rule_position]) }); } + lines_all_rule_as_filter.push(quote! { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[#rule_position]) }); + lines_rule.push(quote! { #rule }); + let rule_option_type = quote! { pgls_analyser::options::#rule_name }; - let rule_option = quote! { Option<#rule_config_type<#rule_option_type>> }; + let rule_option = quote! { Option> }; + schema_lines_rules.push(quote! { #[doc = #summary] #[serde(skip_serializing_if = "Option::is_none")] @@ -568,6 +506,7 @@ fn generate_group_struct( } } }); + rule_disabled_check_line.push(quote! { if let Some(rule) = self.#rule_identifier.as_ref() { if rule.is_disabled() { @@ -598,17 +537,100 @@ fn generate_group_struct( let group_pascal_ident = Ident::new(&to_capitalized(group), Span::call_site()); - let get_configuration_function = if kind == RuleCategory::Action { - quote! { - pub(crate) fn get_rule_configuration(&self, rule_name: &str) -> Option<(RuleAssistPlainConfiguration, Option)> { + quote! { + #[derive(Clone, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)] + #[cfg_attr(feature = "schema", derive(JsonSchema))] + #[serde(rename_all = "camelCase", default, deny_unknown_fields)] + /// A list of rules that belong to this group + pub struct #group_pascal_ident { + /// It enables the recommended rules for this group + #[serde(skip_serializing_if = "Option::is_none")] + pub recommended: Option, + + /// It enables ALL rules for this group. + #[serde(skip_serializing_if = "Option::is_none")] + pub all: Option, + + #( #schema_lines_rules ),* + } + + impl #group_pascal_ident { + const GROUP_NAME: &'static str = #group; + pub(crate) const GROUP_RULES: &'static [&'static str] = &[ + #( #lines_rule ),* + ]; + + const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ + #( #lines_recommended_rule_as_filter ),* + ]; + + const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ + #( #lines_all_rule_as_filter ),* + ]; + + /// 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(); + #( #rule_enabled_check_line )* + index_set + } + + pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { + let mut index_set = FxHashSet::default(); + #( #rule_disabled_check_line )* + index_set + } + + /// 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 + } + + /// 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 { - #( #get_rule_configuration_line ),*, - _ => None + #( #get_severity_lines ),*, + _ => unreachable!() } } - } - } else { - quote! { + pub(crate) fn get_rule_configuration(&self, rule_name: &str) -> Option<(RulePlainConfiguration, Option)> { match rule_name { #( #get_rule_configuration_line ),*, @@ -616,141 +638,359 @@ fn generate_group_struct( } } } - }; + } +} - if kind == RuleCategory::Action { - quote! { - #[derive(Clone, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)] - #[cfg_attr(feature = "schema", derive(JsonSchema))] - #[serde(rename_all = "camelCase", default, deny_unknown_fields)] - /// A list of rules that belong to this group - pub struct #group_pascal_ident { +/// Extract the first paragraph from markdown documentation as a summary +fn extract_summary_from_docs(docs: &str) -> String { + let mut summary = String::new(); + let parser = Parser::new(docs); - #( #schema_lines_rules ),* + for event in parser { + match event { + Event::Text(text) => { + summary.push_str(text.as_ref()); + } + Event::Code(text) => { + // Escape `[` and `<` to obtain valid Markdown + summary.push_str(text.replace('[', "\\[").replace('<', "\\<").as_ref()); } + Event::SoftBreak => { + summary.push(' '); + } + Event::Start(Tag::Paragraph) => {} + Event::End(TagEnd::Paragraph) => { + break; + } + Event::Start(tag) => match tag { + Tag::Strong | Tag::Paragraph => continue, + _ => panic!("Unimplemented tag {:?}", tag), + }, + Event::End(tag) => match tag { + TagEnd::Strong | TagEnd::Paragraph => continue, + _ => panic!("Unimplemented tag {:?}", tag), + }, + _ => panic!("Unimplemented event {:?}", event), + } + } - impl #group_pascal_ident { + summary +} - const GROUP_NAME: &'static str = #group; - pub(crate) const GROUP_RULES: &'static [&'static str] = &[ - #( #lines_rule ),* - ]; +/// Generate configuration files for Action category tools (assists) +fn generate_action_config( + tool: &ToolConfig, + groups: BTreeMap<&'static str, BTreeMap<&'static str, RuleMetadata>>, +) -> Result<(String, String)> { + let mod_file = generate_action_mod_file(tool); + let actions_file = generate_action_actions_file(tool, groups)?; + Ok((mod_file, actions_file)) +} - pub(crate) fn get_enabled_rules(&self) -> FxHashSet> { - let mut index_set = FxHashSet::default(); - #( #rule_enabled_check_line )* - index_set - } +/// Generate the mod.rs file for an Action tool +fn generate_action_mod_file(tool: &ToolConfig) -> String { + let config_struct = Ident::new(&tool.config_struct_name(), Span::call_site()); + let partial_config_struct = Ident::new(&tool.partial_config_struct_name(), Span::call_site()); + let generated_file = tool.generated_file().trim_end_matches(".rs"); + let generated_file_ident = Ident::new(generated_file, Span::call_site()); + + let content = quote! { + //! Generated file, do not edit by hand, see `xtask/codegen` + + mod #generated_file_ident; + + use biome_deserialize::StringSet; + use biome_deserialize_macros::{Deserializable, Merge, Partial}; + use bpaf::Bpaf; + pub use #generated_file_ident::*; + use serde::{Deserialize, Serialize}; + + #[derive(Clone, Debug, Deserialize, Eq, Partial, PartialEq, Serialize)] + #[partial(derive(Bpaf, Clone, Deserializable, Eq, Merge, PartialEq))] + #[partial(cfg_attr(feature = "schema", derive(schemars::JsonSchema)))] + #[partial(serde(deny_unknown_fields, rename_all = "camelCase"))] + pub struct #config_struct { + /// Whether assists should be enabled via LSP. + #[partial(bpaf(long("assists-enabled"), argument("true|false")))] + pub enabled: bool, + + /// List of actions + #[partial(bpaf(pure(Default::default()), optional, hide))] + pub actions: Actions, + + /// A list of Unix shell style patterns. The assists will ignore files/folders that will + /// match these patterns. + #[partial(bpaf(hide))] + pub ignore: StringSet, + + /// A list of Unix shell style patterns. The assists will include files/folders that will + /// match these patterns. + #[partial(bpaf(hide))] + pub include: StringSet, + } - /// 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()?]) + impl Default for #config_struct { + fn default() -> Self { + Self { + enabled: true, + actions: Actions::default(), + ignore: Default::default(), + include: Default::default(), } - - #get_configuration_function } } - } else { - quote! { - #[derive(Clone, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)] - #[cfg_attr(feature = "schema", derive(JsonSchema))] - #[serde(rename_all = "camelCase", default, deny_unknown_fields)] - /// A list of rules that belong to this group - pub struct #group_pascal_ident { - /// It enables the recommended rules for this group - #[serde(skip_serializing_if = "Option::is_none")] - pub recommended: Option, - /// It enables ALL rules for this group. - #[serde(skip_serializing_if = "Option::is_none")] - pub all: Option, + impl #partial_config_struct { + pub const fn is_disabled(&self) -> bool { + matches!(self.enabled, Some(false)) + } - #( #schema_lines_rules ),* + pub fn get_actions(&self) -> Actions { + self.actions.clone().unwrap_or_default() } + } + }; - impl #group_pascal_ident { + xtask::reformat(content.to_string()).unwrap() +} - const GROUP_NAME: &'static str = #group; - pub(crate) const GROUP_RULES: &'static [&'static str] = &[ - #( #lines_rule ),* - ]; +/// Generate the actions.rs file for an Action tool +fn generate_action_actions_file( + tool: &ToolConfig, + groups: BTreeMap<&'static str, BTreeMap<&'static str, RuleMetadata>>, +) -> Result { + let mut struct_groups = Vec::with_capacity(groups.len()); + let mut group_pascal_idents = Vec::with_capacity(groups.len()); + let mut group_idents = Vec::with_capacity(groups.len()); + let mut group_strings = Vec::with_capacity(groups.len()); + let mut group_as_enabled_rules = Vec::with_capacity(groups.len()); - const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ - #( #lines_recommended_rule_as_filter ),* - ]; + for (group, rules) in groups { + let group_pascal_ident = quote::format_ident!("{}", &Case::Pascal.convert(group)); + let group_ident = quote::format_ident!("{}", group); - const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ - #( #lines_all_rule_as_filter ),* - ]; + group_as_enabled_rules.push(quote! { + if let Some(group) = self.#group_ident.as_ref() { + enabled_rules.extend(&group.get_enabled_rules()); + } + }); - /// Retrieves the recommended rules - pub(crate) fn is_recommended_true(&self) -> bool { - // we should inject recommended rules only when they are set to "true" - matches!(self.recommended, Some(true)) - } + group_pascal_idents.push(group_pascal_ident); + group_idents.push(group_ident); + group_strings.push(Literal::string(group)); + struct_groups.push(generate_action_group_struct(group, &rules)); + } - pub(crate) fn is_recommended_unset(&self) -> bool { - self.recommended.is_none() - } + let actions_struct_content = quote! { + //! Generated file, do not edit by hand, see `xtask/codegen` + + use crate::rules::{RuleAssistConfiguration, RuleAssistPlainConfiguration}; + use biome_deserialize_macros::{Deserializable, Merge}; + use pgls_analyse::{RuleFilter, options::RuleOptions}; + use pgls_diagnostics::{Category, Severity}; + use rustc_hash::FxHashSet; + #[cfg(feature = "schema")] + use schemars::JsonSchema; + use serde::{Deserialize, Serialize}; + + #[derive( + Clone, + Copy, + Debug, + Deserializable, + Eq, + Hash, + Merge, + Ord, + PartialEq, + PartialOrd, + serde::Deserialize, + serde::Serialize, + )] + #[cfg_attr(feature = "schema", derive(JsonSchema))] + #[serde(rename_all = "camelCase")] + pub enum RuleGroup { + #( #group_pascal_idents ),* + } - pub(crate) fn is_all_true(&self) -> bool { - matches!(self.all, Some(true)) + impl RuleGroup { + pub const fn as_str(self) -> &'static str { + match self { + #( Self::#group_pascal_idents => #group_pascal_idents::GROUP_NAME, )* } + } + } - pub(crate) fn is_all_unset(&self) -> bool { - self.all.is_none() + impl std::str::FromStr for RuleGroup { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s { + #( #group_pascal_idents::GROUP_NAME => Ok(Self::#group_pascal_idents), )* + _ => Err("This rule group doesn't exist.") } + } + } - pub(crate) fn get_enabled_rules(&self) -> FxHashSet> { - let mut index_set = FxHashSet::default(); - #( #rule_enabled_check_line )* - index_set - } + #[derive(Clone, Debug, Default, Deserialize, Deserializable, Eq, Merge, PartialEq, Serialize)] + #[cfg_attr(feature = "schema", derive(JsonSchema))] + #[serde(rename_all = "camelCase", deny_unknown_fields)] + pub struct Actions { + #( + #[deserializable(rename = #group_strings)] + #[serde(skip_serializing_if = "Option::is_none")] + pub #group_idents: Option<#group_pascal_idents>, + )* + } - pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { - let mut index_set = FxHashSet::default(); - #( #rule_disabled_check_line )* - index_set + impl Actions { + /// Checks if the code coming from [pgls_diagnostics::Diagnostic] corresponds to a rule. + /// Usually the code is built like {group}/{rule_name} + pub fn has_rule( + group: RuleGroup, + rule_name: &str, + ) -> Option<&'static str> { + match group { + #( + RuleGroup::#group_pascal_idents => #group_pascal_idents::has_rule(rule_name), + )* } + } - /// 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()?]) - } + /// Given a category coming from [Diagnostic](pgls_diagnostics::Diagnostic), this function returns + /// the [Severity](pgls_diagnostics::Severity) associated to the rule, if the configuration changed it. + /// If the severity is off or not set, then the function returns the default severity of the rule: + /// [Severity::Error] for recommended rules and [Severity::Warning] for other rules. + /// + /// If not, the function returns [None]. + pub fn get_severity_from_code(&self, category: &Category) -> Option { + let mut split_code = category.name().split('/'); - pub(crate) fn recommended_rules_as_filters() -> &'static [RuleFilter<'static>] { - Self::RECOMMENDED_RULES_AS_FILTERS - } + let _assists = split_code.next(); + debug_assert_eq!(_assists, Some("assists")); - pub(crate) fn all_rules_as_filters() -> &'static [RuleFilter<'static>] { - Self::ALL_RULES_AS_FILTERS + let group = ::from_str(split_code.next()?).ok()?; + let rule_name = split_code.next()?; + let rule_name = Self::has_rule(group, rule_name)?; + match group { + #( + RuleGroup::#group_pascal_idents => self + .#group_idents + .as_ref() + .and_then(|group| group.get_rule_configuration(rule_name)) + .filter(|(level, _)| !matches!(level, RuleAssistPlainConfiguration::Off)) + .map(|(level, _)| level.into()), + )* } + } - /// Select preset rules - // Preset rules shouldn't populate disabled rules - // because that will make specific rules cannot be enabled later. - pub(crate) fn collect_preset_rules( - &self, - parent_is_all: bool, - parent_is_recommended: bool, - enabled_rules: &mut FxHashSet>, - ) { - // The order of the if-else branches MATTERS! - 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()); - } + /// It returns the enabled rules by default. + /// + /// 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(); + #( #group_as_enabled_rules )* + enabled_rules + } + } + + #( #struct_groups )* + + #[test] + fn test_order() { + #( + for items in #group_pascal_idents::GROUP_RULES.windows(2) { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); } + )* + } + }; - pub(crate) fn severity(rule_name: &str) -> Severity { - match rule_name { - #( #get_severity_lines ),*, - _ => unreachable!() - } + Ok(xtask::reformat(actions_struct_content.to_string())?) +} + +/// Generate a group struct for action rules +fn generate_action_group_struct( + group: &str, + rules: &BTreeMap<&'static str, RuleMetadata>, +) -> TokenStream { + let mut lines_rule = Vec::new(); + let mut schema_lines_rules = Vec::new(); + let mut rule_enabled_check_line = Vec::new(); + let mut get_rule_configuration_line = Vec::new(); + + for (index, (rule, metadata)) in rules.iter().enumerate() { + let summary = extract_summary_from_docs(metadata.docs); + let rule_position = Literal::u8_unsuffixed(index as u8); + let rule_identifier = quote::format_ident!("{}", Case::Snake.convert(rule)); + let rule_name = Ident::new(&to_capitalized(rule), Span::call_site()); + + lines_rule.push(quote! { + #rule + }); + + let rule_option_type = quote! { + pgls_analyser::options::#rule_name + }; + let rule_option = quote! { Option> }; + + schema_lines_rules.push(quote! { + #[doc = #summary] + #[serde(skip_serializing_if = "Option::is_none")] + pub #rule_identifier: #rule_option + }); + + rule_enabled_check_line.push(quote! { + if let Some(rule) = self.#rule_identifier.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule( + Self::GROUP_NAME, + Self::GROUP_RULES[#rule_position], + )); } + } + }); + + get_rule_configuration_line.push(quote! { + #rule => self.#rule_identifier.as_ref().map(|conf| (conf.level(), conf.get_options())) + }); + } + + let group_pascal_ident = Ident::new(&to_capitalized(group), Span::call_site()); - #get_configuration_function + quote! { + #[derive(Clone, Debug, Default, Deserialize, Deserializable, Eq, Merge, PartialEq, Serialize)] + #[cfg_attr(feature = "schema", derive(JsonSchema))] + #[serde(rename_all = "camelCase", default, deny_unknown_fields)] + /// A list of rules that belong to this group + pub struct #group_pascal_ident { + #( #schema_lines_rules ),* + } + + impl #group_pascal_ident { + const GROUP_NAME: &'static str = #group; + pub(crate) const GROUP_RULES: &'static [&'static str] = &[ + #( #lines_rule ),* + ]; + + pub(crate) fn get_enabled_rules(&self) -> FxHashSet> { + let mut index_set = FxHashSet::default(); + #( #rule_enabled_check_line )* + index_set + } + + /// 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 get_rule_configuration( + &self, + rule_name: &str, + ) -> Option<(RuleAssistPlainConfiguration, Option)> { + match rule_name { + #( #get_rule_configuration_line ),*, + _ => None + } } } } diff --git a/xtask/codegen/src/lib.rs b/xtask/codegen/src/lib.rs index d095cce9a..9aff7bd1d 100644 --- a/xtask/codegen/src/lib.rs +++ b/xtask/codegen/src/lib.rs @@ -8,7 +8,7 @@ mod generate_new_analyser_rule; pub use self::generate_analyser::generate_analyser; pub use self::generate_bindings::generate_bindings; -pub use self::generate_configuration::generate_rules_configuration; +pub use self::generate_configuration::{generate_rules_configuration, generate_tool_configuration}; pub use self::generate_crate::generate_crate; pub use self::generate_new_analyser_rule::generate_new_analyser_rule; use bpaf::Bpaf; From d3f0a214af0c4446483069c4f12e530d1c4f52b3 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Tue, 4 Nov 2025 10:30:58 +0100 Subject: [PATCH 2/4] fix: update imports and resolve clippy warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix unresolved import from analyser to linter in workspace tests - Remove empty line after outer attribute in diagnostics - Apply clippy suggestions for cleaner code 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- crates/pgls_diagnostics/src/serde.rs | 1 - .../src/workspace/server/analyser.rs | 2 +- xtask/codegen/src/generate_configuration.rs | 17 +++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/pgls_diagnostics/src/serde.rs b/crates/pgls_diagnostics/src/serde.rs index 7e0e7a098..bcc85a3e1 100644 --- a/crates/pgls_diagnostics/src/serde.rs +++ b/crates/pgls_diagnostics/src/serde.rs @@ -164,7 +164,6 @@ impl From> for Location { #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[cfg_attr(test, derive(Eq, PartialEq))] - struct Advices { advices: Vec, } diff --git a/crates/pgls_workspace/src/workspace/server/analyser.rs b/crates/pgls_workspace/src/workspace/server/analyser.rs index 5d6b5adf4..b7382f2bb 100644 --- a/crates/pgls_workspace/src/workspace/server/analyser.rs +++ b/crates/pgls_workspace/src/workspace/server/analyser.rs @@ -141,7 +141,7 @@ impl RegistryVisitor for LintVisitor<'_, '_> { #[cfg(test)] mod tests { use pgls_analyse::RuleFilter; - use pgls_configuration::{RuleConfiguration, Rules, analyser::Safety}; + use pgls_configuration::{RuleConfiguration, Rules, linter::Safety}; use crate::{ settings::{LinterSettings, Settings}, diff --git a/xtask/codegen/src/generate_configuration.rs b/xtask/codegen/src/generate_configuration.rs index d9c677619..f4a922129 100644 --- a/xtask/codegen/src/generate_configuration.rs +++ b/xtask/codegen/src/generate_configuration.rs @@ -6,7 +6,6 @@ use proc_macro2::{Ident, Literal, Span, TokenStream}; use pulldown_cmark::{Event, Parser, Tag, TagEnd}; use quote::quote; use std::collections::BTreeMap; -use std::path::PathBuf; use xtask::*; /// Configuration for a tool that produces rules @@ -26,11 +25,13 @@ impl ToolConfig { } /// Derived: Crate name that contains the rules + #[allow(dead_code)] fn crate_name(&self) -> String { format!("pgls_{}", self.name) } /// Derived: The main struct name (Rules, Actions, or Transformations) + #[allow(dead_code)] fn struct_name(&self) -> &str { match self.category { RuleCategory::Lint => "Rules", @@ -230,7 +231,7 @@ fn generate_lint_mod_file(tool: &ToolConfig) -> String { /// Generate the rules.rs file for a Lint tool fn generate_lint_rules_file( - tool: &ToolConfig, + _tool: &ToolConfig, groups: BTreeMap<&'static str, BTreeMap<&'static str, RuleMetadata>>, ) -> Result { let mut struct_groups = Vec::with_capacity(groups.len()); @@ -448,7 +449,7 @@ fn generate_lint_rules_file( } }; - Ok(xtask::reformat(rules_struct_content.to_string())?) + xtask::reformat(rules_struct_content.to_string()) } /// Generate a group struct for lint rules @@ -664,13 +665,13 @@ fn extract_summary_from_docs(docs: &str) -> String { } Event::Start(tag) => match tag { Tag::Strong | Tag::Paragraph => continue, - _ => panic!("Unimplemented tag {:?}", tag), + _ => panic!("Unimplemented tag {tag:?}"), }, Event::End(tag) => match tag { TagEnd::Strong | TagEnd::Paragraph => continue, - _ => panic!("Unimplemented tag {:?}", tag), + _ => panic!("Unimplemented tag {tag:?}"), }, - _ => panic!("Unimplemented event {:?}", event), + _ => panic!("Unimplemented event {event:?}"), } } @@ -756,7 +757,7 @@ fn generate_action_mod_file(tool: &ToolConfig) -> String { /// Generate the actions.rs file for an Action tool fn generate_action_actions_file( - tool: &ToolConfig, + _tool: &ToolConfig, groups: BTreeMap<&'static str, BTreeMap<&'static str, RuleMetadata>>, ) -> Result { let mut struct_groups = Vec::with_capacity(groups.len()); @@ -905,7 +906,7 @@ fn generate_action_actions_file( } }; - Ok(xtask::reformat(actions_struct_content.to_string())?) + xtask::reformat(actions_struct_content.to_string()) } /// Generate a group struct for action rules From 4d6e4ba32e9acaee15b76a3e990d4f4db10015db Mon Sep 17 00:00:00 2001 From: psteinroe Date: Tue, 4 Nov 2025 10:38:20 +0100 Subject: [PATCH 3/4] fix: order roles alphabetically and update test expectations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ORDER BY clause to roles query for deterministic results - Update role completion tests to expect all default Supabase roles - Create helper function to centralize expected roles list - Update SQLx offline cache for modified query 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ...6752dffcca3b7f4d5b112b0ebc51437e5302.json} | 4 +- .../pgls_completions/src/providers/roles.rs | 159 ++++-------------- .../pgls_schema_cache/src/queries/roles.sql | 3 +- 3 files changed, 39 insertions(+), 127 deletions(-) rename .sqlx/{query-d46da23daf6ae841c4cda6d0fad6772d19ebcab6f9033bce09a4b286024916d6.json => query-9c330708473ef3e53c145d20b86f6752dffcca3b7f4d5b112b0ebc51437e5302.json} (93%) diff --git a/.sqlx/query-d46da23daf6ae841c4cda6d0fad6772d19ebcab6f9033bce09a4b286024916d6.json b/.sqlx/query-9c330708473ef3e53c145d20b86f6752dffcca3b7f4d5b112b0ebc51437e5302.json similarity index 93% rename from .sqlx/query-d46da23daf6ae841c4cda6d0fad6772d19ebcab6f9033bce09a4b286024916d6.json rename to .sqlx/query-9c330708473ef3e53c145d20b86f6752dffcca3b7f4d5b112b0ebc51437e5302.json index 27bf6ba8e..cc665c417 100644 --- a/.sqlx/query-d46da23daf6ae841c4cda6d0fad6772d19ebcab6f9033bce09a4b286024916d6.json +++ b/.sqlx/query-9c330708473ef3e53c145d20b86f6752dffcca3b7f4d5b112b0ebc51437e5302.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "with \nnames_of_parents as (\n select m.member, r.rolname\n from pg_catalog.pg_auth_members m\n join pg_catalog.pg_roles r\n on r.oid = m.roleid\n),\nnames_of_children as (\n select m.roleid, r.rolname\n from pg_catalog.pg_auth_members m\n join pg_catalog.pg_roles r\n on r.oid = m.member\n)\nselect \n r.rolname as \"name!\", \n r.rolsuper as \"is_super_user!\", \n r.rolcreatedb as \"can_create_db!\", \n r.rolcanlogin as \"can_login!\",\n r.rolbypassrls as \"can_bypass_rls!\",\n r.rolcreaterole as \"can_create_roles!\",\n -- this works even if we don't have access to pg_authid; manually verified\n shobj_description(r.oid, 'pg_authid') as \"comment\",\n coalesce((\n select array_agg(m.rolname)\n from names_of_parents m\n where m.member = r.oid\n ), ARRAY[]::text[]) as \"member_of!\",\n coalesce((\n select array_agg(m.rolname)\n from names_of_children m\n where m.roleid = r.oid\n ), ARRAY[]::text[]) as \"has_member!\"\nfrom pg_catalog.pg_roles r;", + "query": "with \nnames_of_parents as (\n select m.member, r.rolname\n from pg_catalog.pg_auth_members m\n join pg_catalog.pg_roles r\n on r.oid = m.roleid\n),\nnames_of_children as (\n select m.roleid, r.rolname\n from pg_catalog.pg_auth_members m\n join pg_catalog.pg_roles r\n on r.oid = m.member\n)\nselect \n r.rolname as \"name!\", \n r.rolsuper as \"is_super_user!\", \n r.rolcreatedb as \"can_create_db!\", \n r.rolcanlogin as \"can_login!\",\n r.rolbypassrls as \"can_bypass_rls!\",\n r.rolcreaterole as \"can_create_roles!\",\n -- this works even if we don't have access to pg_authid; manually verified\n shobj_description(r.oid, 'pg_authid') as \"comment\",\n coalesce((\n select array_agg(m.rolname)\n from names_of_parents m\n where m.member = r.oid\n ), ARRAY[]::text[]) as \"member_of!\",\n coalesce((\n select array_agg(m.rolname)\n from names_of_children m\n where m.roleid = r.oid\n ), ARRAY[]::text[]) as \"has_member!\"\nfrom pg_catalog.pg_roles r\norder by r.rolname;", "describe": { "columns": [ { @@ -64,5 +64,5 @@ null ] }, - "hash": "d46da23daf6ae841c4cda6d0fad6772d19ebcab6f9033bce09a4b286024916d6" + "hash": "9c330708473ef3e53c145d20b86f6752dffcca3b7f4d5b112b0ebc51437e5302" } diff --git a/crates/pgls_completions/src/providers/roles.rs b/crates/pgls_completions/src/providers/roles.rs index 7082f4176..a86b57d46 100644 --- a/crates/pgls_completions/src/providers/roles.rs +++ b/crates/pgls_completions/src/providers/roles.rs @@ -46,21 +46,34 @@ mod tests { ); "#; + fn expected_roles() -> Vec { + vec![ + CompletionAssertion::LabelAndKind("anon".into(), crate::CompletionItemKind::Role), + CompletionAssertion::LabelAndKind( + "authenticated".into(), + crate::CompletionItemKind::Role, + ), + CompletionAssertion::LabelAndKind("owner".into(), crate::CompletionItemKind::Role), + CompletionAssertion::LabelAndKind( + "service_role".into(), + crate::CompletionItemKind::Role, + ), + CompletionAssertion::LabelAndKind( + "test_login".into(), + crate::CompletionItemKind::Role, + ), + CompletionAssertion::LabelAndKind( + "test_nologin".into(), + crate::CompletionItemKind::Role, + ), + ] + } + #[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] async fn works_in_drop_role(pool: PgPool) { assert_complete_results( format!("drop role {}", QueryWithCursorPosition::cursor_marker()).as_str(), - vec![ - CompletionAssertion::LabelAndKind("owner".into(), crate::CompletionItemKind::Role), - CompletionAssertion::LabelAndKind( - "test_login".into(), - crate::CompletionItemKind::Role, - ), - CompletionAssertion::LabelAndKind( - "test_nologin".into(), - crate::CompletionItemKind::Role, - ), - ], + expected_roles(), Some(SETUP), &pool, ) @@ -71,17 +84,7 @@ mod tests { async fn works_in_alter_role(pool: PgPool) { assert_complete_results( format!("alter role {}", QueryWithCursorPosition::cursor_marker()).as_str(), - vec![ - CompletionAssertion::LabelAndKind("owner".into(), crate::CompletionItemKind::Role), - CompletionAssertion::LabelAndKind( - "test_login".into(), - crate::CompletionItemKind::Role, - ), - CompletionAssertion::LabelAndKind( - "test_nologin".into(), - crate::CompletionItemKind::Role, - ), - ], + expected_roles(), Some(SETUP), &pool, ) @@ -94,17 +97,7 @@ mod tests { assert_complete_results( format!("set role {}", QueryWithCursorPosition::cursor_marker()).as_str(), - vec![ - CompletionAssertion::LabelAndKind("owner".into(), crate::CompletionItemKind::Role), - CompletionAssertion::LabelAndKind( - "test_login".into(), - crate::CompletionItemKind::Role, - ), - CompletionAssertion::LabelAndKind( - "test_nologin".into(), - crate::CompletionItemKind::Role, - ), - ], + expected_roles(), None, &pool, ) @@ -116,17 +109,7 @@ mod tests { QueryWithCursorPosition::cursor_marker() ) .as_str(), - vec![ - CompletionAssertion::LabelAndKind("owner".into(), crate::CompletionItemKind::Role), - CompletionAssertion::LabelAndKind( - "test_login".into(), - crate::CompletionItemKind::Role, - ), - CompletionAssertion::LabelAndKind( - "test_nologin".into(), - crate::CompletionItemKind::Role, - ), - ], + expected_roles(), None, &pool, ) @@ -140,24 +123,14 @@ mod tests { assert_complete_results( format!( r#"create policy "my cool policy" on public.users - as restrictive + as restrictive for all to {} using (true);"#, QueryWithCursorPosition::cursor_marker() ) .as_str(), - vec![ - CompletionAssertion::LabelAndKind("owner".into(), crate::CompletionItemKind::Role), - CompletionAssertion::LabelAndKind( - "test_login".into(), - crate::CompletionItemKind::Role, - ), - CompletionAssertion::LabelAndKind( - "test_nologin".into(), - crate::CompletionItemKind::Role, - ), - ], + expected_roles(), None, &pool, ) @@ -171,17 +144,7 @@ mod tests { QueryWithCursorPosition::cursor_marker() ) .as_str(), - vec![ - CompletionAssertion::LabelAndKind("owner".into(), crate::CompletionItemKind::Role), - CompletionAssertion::LabelAndKind( - "test_login".into(), - crate::CompletionItemKind::Role, - ), - CompletionAssertion::LabelAndKind( - "test_nologin".into(), - crate::CompletionItemKind::Role, - ), - ], + expected_roles(), None, &pool, ) @@ -200,18 +163,7 @@ mod tests { QueryWithCursorPosition::cursor_marker() ) .as_str(), - vec![ - // recognizing already mentioned roles is not supported for now - CompletionAssertion::LabelAndKind("owner".into(), crate::CompletionItemKind::Role), - CompletionAssertion::LabelAndKind( - "test_login".into(), - crate::CompletionItemKind::Role, - ), - CompletionAssertion::LabelAndKind( - "test_nologin".into(), - crate::CompletionItemKind::Role, - ), - ], + expected_roles(), None, &pool, ) @@ -225,18 +177,7 @@ mod tests { QueryWithCursorPosition::cursor_marker() ) .as_str(), - vec![ - // recognizing already mentioned roles is not supported for now - CompletionAssertion::LabelAndKind("owner".into(), crate::CompletionItemKind::Role), - CompletionAssertion::LabelAndKind( - "test_login".into(), - crate::CompletionItemKind::Role, - ), - CompletionAssertion::LabelAndKind( - "test_nologin".into(), - crate::CompletionItemKind::Role, - ), - ], + expected_roles(), None, &pool, ) @@ -248,18 +189,7 @@ mod tests { QueryWithCursorPosition::cursor_marker() ) .as_str(), - vec![ - // recognizing already mentioned roles is not supported for now - CompletionAssertion::LabelAndKind("owner".into(), crate::CompletionItemKind::Role), - CompletionAssertion::LabelAndKind( - "test_login".into(), - crate::CompletionItemKind::Role, - ), - CompletionAssertion::LabelAndKind( - "test_nologin".into(), - crate::CompletionItemKind::Role, - ), - ], + expected_roles(), None, &pool, ) @@ -298,27 +228,8 @@ mod tests { ]; for query in queries { - assert_complete_results( - query.as_str(), - vec![ - // recognizing already mentioned roles is not supported for now - CompletionAssertion::LabelAndKind( - "owner".into(), - crate::CompletionItemKind::Role, - ), - CompletionAssertion::LabelAndKind( - "test_login".into(), - crate::CompletionItemKind::Role, - ), - CompletionAssertion::LabelAndKind( - "test_nologin".into(), - crate::CompletionItemKind::Role, - ), - ], - None, - &pool, - ) - .await; + assert_complete_results(query.as_str(), expected_roles(), None, &pool) + .await; } } } diff --git a/crates/pgls_schema_cache/src/queries/roles.sql b/crates/pgls_schema_cache/src/queries/roles.sql index 28f6f4185..ce49579fb 100644 --- a/crates/pgls_schema_cache/src/queries/roles.sql +++ b/crates/pgls_schema_cache/src/queries/roles.sql @@ -30,4 +30,5 @@ select from names_of_children m where m.roleid = r.oid ), ARRAY[]::text[]) as "has_member!" -from pg_catalog.pg_roles r; \ No newline at end of file +from pg_catalog.pg_roles r +order by r.rolname; \ No newline at end of file From 14ee3b83d16243cd76b2b30149ad01a422648fa0 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Thu, 6 Nov 2025 10:24:40 +0100 Subject: [PATCH 4/4] progress --- crates/pgls_completions/src/providers/roles.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/pgls_completions/src/providers/roles.rs b/crates/pgls_completions/src/providers/roles.rs index a86b57d46..4e8c7f17a 100644 --- a/crates/pgls_completions/src/providers/roles.rs +++ b/crates/pgls_completions/src/providers/roles.rs @@ -58,10 +58,7 @@ mod tests { "service_role".into(), crate::CompletionItemKind::Role, ), - CompletionAssertion::LabelAndKind( - "test_login".into(), - crate::CompletionItemKind::Role, - ), + CompletionAssertion::LabelAndKind("test_login".into(), crate::CompletionItemKind::Role), CompletionAssertion::LabelAndKind( "test_nologin".into(), crate::CompletionItemKind::Role, @@ -228,8 +225,7 @@ mod tests { ]; for query in queries { - assert_complete_results(query.as_str(), expected_roles(), None, &pool) - .await; + assert_complete_results(query.as_str(), expected_roles(), None, &pool).await; } } }