From 77bdba02ee0f0fefd0f4a6b6707adc30a11ffd1d Mon Sep 17 00:00:00 2001 From: psteinroe Date: Sun, 25 Oct 2026 16:28:06 +0000 Subject: [PATCH 1/2] refactor(workspace): move formatting out of server.rs pull_file_formatting builds the format config and formats the document in server/formatting.rs, together with collapse_blank_lines and restore_named_parameters. The code is moved as it was. --- crates/pgls_workspace/src/workspace/server.rs | 285 +---------------- .../src/workspace/server/formatting.rs | 298 ++++++++++++++++++ 2 files changed, 308 insertions(+), 275 deletions(-) create mode 100644 crates/pgls_workspace/src/workspace/server/formatting.rs diff --git a/crates/pgls_workspace/src/workspace/server.rs b/crates/pgls_workspace/src/workspace/server.rs index 7a04b53c8..76bb44c26 100644 --- a/crates/pgls_workspace/src/workspace/server.rs +++ b/crates/pgls_workspace/src/workspace/server.rs @@ -19,7 +19,7 @@ use cache_budget::CacheBudget; use connection_manager::ConnectionManager; #[cfg(feature = "db")] use document::TypecheckDiagnosticsMapper; -use document::{DefaultMapper, Document, ExecuteStatementMapper, FormatStatementMapper}; +use document::{DefaultMapper, Document, ExecuteStatementMapper}; #[cfg(feature = "db")] use futures::{StreamExt, TryStreamExt, stream}; #[cfg(feature = "db")] @@ -69,7 +69,7 @@ use crate::{ CheckDatabaseConnectionParams, CheckDatabaseConnectionResult, PullDiagnosticsResult, PullFileDiagnosticsParams, }, - format::{PullFileFormattingParams, PullFormattingResult, StatementFormatResult}, + format::{PullFileFormattingParams, PullFormattingResult}, inlay_hints::{InlayHint, PullInlayHintsParams, PullInlayHintsResult}, on_hover::{OnHoverParams, OnHoverResult}, }, @@ -95,6 +95,7 @@ mod connection_key; #[cfg(feature = "db")] mod connection_manager; pub(crate) mod document; +mod formatting; mod migration; mod pg_query; #[cfg(feature = "db")] @@ -1595,193 +1596,14 @@ impl Workspace for WorkspaceServer { .get(¶ms.path) .ok_or(WorkspaceError::not_found())?; - let config = pgls_pretty_print::FormatConfig { - line_width: settings.formatter.line_width as usize, - indent_size: settings.formatter.indent_size as usize, - indent_style: settings.formatter.indent_style.into(), - keyword_case: settings.formatter.keyword_case.into(), - constant_case: settings.formatter.constant_case.into(), - type_case: settings.formatter.type_case.into(), - comma_style: settings.formatter.comma_style.into(), - logical_operator_placement: settings.formatter.logical_operator_placement.into(), - layout: settings.formatter.layout.into(), - values_row_layout: settings.formatter.values_row_layout.into(), - cast_style: settings.formatter.cast_style.into(), - clause_body_style: settings.formatter.clause_body_style.into(), - isolate_semicolon: settings.formatter.isolate_semicolon, - function_argument_groups: settings - .formatter - .function_argument_groups - .iter() - .map(|(name, size)| (name.to_lowercase(), usize::from(size.get()))) - .collect(), - }; - - let mut diagnostics = Vec::new(); - let mut statements = Vec::new(); - let mut formatted_output = String::new(); - let path_str = params.path.as_path().display().to_string(); - - let format_candidates: Vec<_> = doc.iter(FormatStatementMapper).collect(); - let mut formatted_sql_fn_bodies = HashMap::new(); - - // Only format SQL function bodies if skip_fn_bodies is false - if !settings.formatter.skip_fn_bodies { - for (id, _stmt_range, text, ast_result) in &format_candidates { - if !id.is_child() { - continue; - } - - let Some(parent_id) = id.parent() else { - continue; - }; - - let Ok(ast) = ast_result else { - continue; - }; - - let Ok(result) = pgls_pretty_print::format_statement(ast, text, &config) else { - continue; - }; - - let Some(formatted) = restore_named_parameters(text, &result.formatted, ast) else { - continue; - }; - - formatted_sql_fn_bodies.insert(parent_id, formatted); - } - } - - // Reconstruct the file by replacing each statement's source span with its - // formatted output while copying the gaps between statements (whitespace and - // standalone comments) verbatim. libpg_query drops comments from the AST, so - // anything that lives outside a statement's range would otherwise be lost. - let source = doc.get_document_content(); - let mut last_end = 0usize; - - // Formatting a range leaves everything outside its statements as written. - let collapses_blank_lines = params.range.is_none(); - - for (id, stmt_range, text, ast_result) in format_candidates { - if id.is_child() { - continue; - } - - let stmt_start = usize::from(stmt_range.start()); - let stmt_end = usize::from(stmt_range.end()); - - // Preserve the standalone comments that precede this statement, with at most one - // blank line between statements and none before the first. - let gap = &source[last_end..stmt_start]; - if collapses_blank_lines { - let gap = collapse_blank_lines(gap); - formatted_output.push_str(if last_end == 0 { - gap.trim_start() - } else { - &gap - }); - } else { - formatted_output.push_str(gap); - } - last_end = stmt_end; - - // Leave statements outside the requested range untouched. - if let Some(filter_range) = params.range - && stmt_range.intersect(filter_range).is_none() - { - formatted_output.push_str(&text); - continue; - } - - match ast_result { - Ok(ast) => { - let mut ast = ast; - if let Some(formatted_sql_fn_body) = formatted_sql_fn_bodies.get(&id) { - // Copies the tree only for the functions whose body is replaced. - sql_function::set_formatted_sql_fn_body( - Arc::make_mut(&mut ast), - formatted_sql_fn_body, - ); - } - - match pgls_pretty_print::format_statement(&ast, &text, &config) { - Ok(result) => { - match restore_named_parameters(&text, &result.formatted, &ast) { - Some(formatted) => { - if text != formatted { - statements.push(StatementFormatResult { - original: text.clone(), - formatted: formatted.clone(), - range: stmt_range, - }); - } - formatted_output.push_str(&formatted); - } - None => { - diagnostics.push(SDiagnostic::new( - pgls_diagnostics::Error::from( - WorkspaceError::format_error( - "Named parameters could not be restored after \ - formatting, the statement was left untouched" - .to_string(), - ), - ) - .with_file_path(&path_str) - .with_file_span(stmt_range), - )); - - formatted_output.push_str(&text); - } - } - } - Err(err) => { - diagnostics.push(SDiagnostic::new( - pgls_diagnostics::Error::from(WorkspaceError::format_error( - err.to_string(), - )) - .with_file_path(&path_str) - .with_file_span(stmt_range), - )); - - formatted_output.push_str(&text); - } - } - } - Err(syntax_err) => { - diagnostics.push(SDiagnostic::new( - pgls_diagnostics::Error::from(syntax_err).with_file_path(&path_str), - )); - - formatted_output.push_str(&text); - } - } - } - - // Preserve any trailing text or comments after the last statement, without blank lines - // at the end of the file but with its final line break. - let rest = &source[last_end..]; - if collapses_blank_lines { - let rest = collapse_blank_lines(rest); - let rest = if last_end == 0 { - rest.trim() - } else { - rest.trim_end() - }; - formatted_output.push_str(rest); - if source.ends_with('\n') && !formatted_output.is_empty() { - formatted_output.push('\n'); - } - } else { - formatted_output.push_str(rest); - } - + let result = formatting::format_document( + doc, + &settings.formatter, + params.range, + ¶ms.path.as_path().display().to_string(), + ); self.charge_document_caches(&documents, ¶ms.path); - Ok(PullFormattingResult { - original: doc.get_document_content().to_string(), - formatted: formatted_output, - statements, - diagnostics, - }) + Ok(result) } #[cfg(feature = "db")] @@ -2049,93 +1871,6 @@ fn is_dir(path: &Path) -> bool { path.is_dir() || (path.is_symlink() && fs::read_link(path).is_ok_and(|path| path.is_dir())) } -/// Keeps at most one blank line in `text`, which only holds whitespace and comments. -fn collapse_blank_lines(text: &str) -> String { - let mut result = String::with_capacity(text.len()); - // Line breaks since the last text that is not whitespace. Two make a blank line. - let mut line_breaks = 0; - for line in text.split_inclusive('\n') { - let is_blank = line.trim().is_empty(); - let ends_line = line.ends_with('\n'); - if is_blank && ends_line { - line_breaks += 1; - if line_breaks <= 2 { - result.push('\n'); - } - continue; - } - - result.push_str(line); - if !is_blank { - line_breaks = usize::from(ends_line); - } - } - result -} - -/// Put psql named parameters back where the formatter printed their placeholders. -/// -/// The parser only ever sees placeholders, so the printed statement carries `$1` or a bare -/// identifier instead of `:'agen_code'` or `:raw_data`. Substitution happens on the token stream -/// so that a `$1` inside a string literal is left alone, and the result is proven by converting it -/// back and comparing the normalized ASTs: a statement that does not survive that round trip is -/// reported as unrestorable and kept as it was written. -fn restore_named_parameters( - original: &str, - formatted: &str, - original_ast: &pgls_query::NodeEnum, -) -> Option { - use pgls_lexer::{SyntaxKind, lex}; - - let conversion = pgls_lexer::convert_to_positional_params_with_metadata(original); - - if conversion.identifier_replacements.is_empty() && conversion.value_replacements.is_empty() { - return Some(formatted.to_string()); - } - - if !conversion.restorable { - return None; - } - - let lexed = lex(formatted); - let mut restored = String::with_capacity(formatted.len()); - - for (idx, kind) in lexed.tokens().enumerate() { - if kind == SyntaxKind::EOF { - break; - } - - let text = lexed.text(idx); - - let replacement = match kind { - SyntaxKind::IDENT => conversion.identifier_replacements.get(text), - SyntaxKind::POSITIONAL_PARAM => text - .trim_start_matches('$') - .parse::() - .ok() - .and_then(|index| conversion.value_replacements.get(&index)), - _ => None, - }; - - match replacement { - Some(source_text) => restored.push_str(source_text), - None => restored.push_str(text), - } - } - - let reparsed = pgls_query::parse(&pgls_lexer::convert_to_positional_params(&restored)).ok()?; - let mut restored_ast = reparsed.into_root()?; - let mut expected_ast = original_ast.clone(); - pgls_pretty_print::normalize_ast(&mut restored_ast); - pgls_pretty_print::normalize_ast(&mut expected_ast); - - if restored_ast == expected_ast { - Some(restored) - } else { - None - } -} - #[cfg(all(test, feature = "db"))] #[path = "server.tests.rs"] mod tests; diff --git a/crates/pgls_workspace/src/workspace/server/formatting.rs b/crates/pgls_workspace/src/workspace/server/formatting.rs new file mode 100644 index 000000000..481284e17 --- /dev/null +++ b/crates/pgls_workspace/src/workspace/server/formatting.rs @@ -0,0 +1,298 @@ +//! Formatting of a document. Each statement is printed on its own, and the text between the +//! statements is kept, since the parser drops comments. + +use std::{collections::HashMap, sync::Arc}; + +use pgls_diagnostics::{DiagnosticExt, serde::Diagnostic as SDiagnostic}; +use pgls_pretty_print::FormatConfig; +use pgls_text_size::TextRange; + +use super::{ + document::{Document, FormatStatementMapper}, + sql_function, +}; +use crate::{ + WorkspaceError, + features::format::{PullFormattingResult, StatementFormatResult}, + settings::FormatterSettings, +}; + +fn format_config(settings: &FormatterSettings) -> FormatConfig { + FormatConfig { + line_width: settings.line_width as usize, + indent_size: settings.indent_size as usize, + indent_style: settings.indent_style.into(), + keyword_case: settings.keyword_case.into(), + constant_case: settings.constant_case.into(), + type_case: settings.type_case.into(), + comma_style: settings.comma_style.into(), + logical_operator_placement: settings.logical_operator_placement.into(), + layout: settings.layout.into(), + values_row_layout: settings.values_row_layout.into(), + cast_style: settings.cast_style.into(), + clause_body_style: settings.clause_body_style.into(), + isolate_semicolon: settings.isolate_semicolon, + function_argument_groups: settings + .function_argument_groups + .iter() + .map(|(name, size)| (name.to_lowercase(), usize::from(size.get()))) + .collect(), + } +} + +/// Formats the statements of `doc`, or only those that intersect `range`. `path` is the path of +/// the document in diagnostics. +pub(super) fn format_document( + doc: &Document, + settings: &FormatterSettings, + range: Option, + path: &str, +) -> PullFormattingResult { + let mut diagnostics = Vec::new(); + let mut statements = Vec::new(); + let mut formatted_output = String::new(); + + let config = format_config(settings); + let format_candidates: Vec<_> = doc.iter(FormatStatementMapper).collect(); + let mut formatted_sql_fn_bodies = HashMap::new(); + + // Only format SQL function bodies if skip_fn_bodies is false + if !settings.skip_fn_bodies { + for (id, _stmt_range, text, ast_result) in &format_candidates { + if !id.is_child() { + continue; + } + + let Some(parent_id) = id.parent() else { + continue; + }; + + let Ok(ast) = ast_result else { + continue; + }; + + let Ok(result) = pgls_pretty_print::format_statement(ast, text, &config) else { + continue; + }; + + let Some(formatted) = restore_named_parameters(text, &result.formatted, ast) else { + continue; + }; + + formatted_sql_fn_bodies.insert(parent_id, formatted); + } + } + + // Reconstruct the file by replacing each statement's source span with its + // formatted output while copying the gaps between statements (whitespace and + // standalone comments) verbatim. libpg_query drops comments from the AST, so + // anything that lives outside a statement's range would otherwise be lost. + let source = doc.get_document_content(); + let mut last_end = 0usize; + + // Formatting a range leaves everything outside its statements as written. + let collapses_blank_lines = range.is_none(); + + for (id, stmt_range, text, ast_result) in format_candidates { + if id.is_child() { + continue; + } + + let stmt_start = usize::from(stmt_range.start()); + let stmt_end = usize::from(stmt_range.end()); + + // Preserve the standalone comments that precede this statement, with at most one + // blank line between statements and none before the first. + let gap = &source[last_end..stmt_start]; + if collapses_blank_lines { + let gap = collapse_blank_lines(gap); + formatted_output.push_str(if last_end == 0 { + gap.trim_start() + } else { + &gap + }); + } else { + formatted_output.push_str(gap); + } + last_end = stmt_end; + + // Leave statements outside the requested range untouched. + if let Some(filter_range) = range + && stmt_range.intersect(filter_range).is_none() + { + formatted_output.push_str(&text); + continue; + } + + match ast_result { + Ok(ast) => { + let mut ast = ast; + if let Some(formatted_sql_fn_body) = formatted_sql_fn_bodies.get(&id) { + // Copies the tree only for the functions whose body is replaced. + sql_function::set_formatted_sql_fn_body( + Arc::make_mut(&mut ast), + formatted_sql_fn_body, + ); + } + + match pgls_pretty_print::format_statement(&ast, &text, &config) { + Ok(result) => match restore_named_parameters(&text, &result.formatted, &ast) { + Some(formatted) => { + if text != formatted { + statements.push(StatementFormatResult { + original: text.clone(), + formatted: formatted.clone(), + range: stmt_range, + }); + } + formatted_output.push_str(&formatted); + } + None => { + diagnostics.push(SDiagnostic::new( + pgls_diagnostics::Error::from(WorkspaceError::format_error( + "Named parameters could not be restored after \ + formatting, the statement was left untouched" + .to_string(), + )) + .with_file_path(path) + .with_file_span(stmt_range), + )); + + formatted_output.push_str(&text); + } + }, + Err(err) => { + diagnostics.push(SDiagnostic::new( + pgls_diagnostics::Error::from(WorkspaceError::format_error( + err.to_string(), + )) + .with_file_path(path) + .with_file_span(stmt_range), + )); + + formatted_output.push_str(&text); + } + } + } + Err(syntax_err) => { + diagnostics.push(SDiagnostic::new( + pgls_diagnostics::Error::from(syntax_err).with_file_path(path), + )); + + formatted_output.push_str(&text); + } + } + } + + // Preserve any trailing text or comments after the last statement, without blank lines + // at the end of the file but with its final line break. + let rest = &source[last_end..]; + if collapses_blank_lines { + let rest = collapse_blank_lines(rest); + let rest = if last_end == 0 { + rest.trim() + } else { + rest.trim_end() + }; + formatted_output.push_str(rest); + if source.ends_with('\n') && !formatted_output.is_empty() { + formatted_output.push('\n'); + } + } else { + formatted_output.push_str(rest); + } + + PullFormattingResult { + original: doc.get_document_content().to_string(), + formatted: formatted_output, + statements, + diagnostics, + } +} + +/// Keeps at most one blank line in `text`, which only holds whitespace and comments. +fn collapse_blank_lines(text: &str) -> String { + let mut result = String::with_capacity(text.len()); + // Line breaks since the last text that is not whitespace. Two make a blank line. + let mut line_breaks = 0; + for line in text.split_inclusive('\n') { + let is_blank = line.trim().is_empty(); + let ends_line = line.ends_with('\n'); + if is_blank && ends_line { + line_breaks += 1; + if line_breaks <= 2 { + result.push('\n'); + } + continue; + } + + result.push_str(line); + if !is_blank { + line_breaks = usize::from(ends_line); + } + } + result +} + +/// Put psql named parameters back where the formatter printed their placeholders. +/// +/// The parser only ever sees placeholders, so the printed statement carries `$1` or a bare +/// identifier instead of `:'agen_code'` or `:raw_data`. Substitution happens on the token stream +/// so that a `$1` inside a string literal is left alone, and the result is proven by converting it +/// back and comparing the normalized ASTs: a statement that does not survive that round trip is +/// reported as unrestorable and kept as it was written. +fn restore_named_parameters( + original: &str, + formatted: &str, + original_ast: &pgls_query::NodeEnum, +) -> Option { + use pgls_lexer::{SyntaxKind, lex}; + + let conversion = pgls_lexer::convert_to_positional_params_with_metadata(original); + + if conversion.identifier_replacements.is_empty() && conversion.value_replacements.is_empty() { + return Some(formatted.to_string()); + } + + if !conversion.restorable { + return None; + } + + let lexed = lex(formatted); + let mut restored = String::with_capacity(formatted.len()); + + for (idx, kind) in lexed.tokens().enumerate() { + if kind == SyntaxKind::EOF { + break; + } + + let text = lexed.text(idx); + + let replacement = match kind { + SyntaxKind::IDENT => conversion.identifier_replacements.get(text), + SyntaxKind::POSITIONAL_PARAM => text + .trim_start_matches('$') + .parse::() + .ok() + .and_then(|index| conversion.value_replacements.get(&index)), + _ => None, + }; + + match replacement { + Some(source_text) => restored.push_str(source_text), + None => restored.push_str(text), + } + } + + let reparsed = pgls_query::parse(&pgls_lexer::convert_to_positional_params(&restored)).ok()?; + let mut restored_ast = reparsed.into_root()?; + let mut expected_ast = original_ast.clone(); + pgls_pretty_print::normalize_ast(&mut restored_ast); + pgls_pretty_print::normalize_ast(&mut expected_ast); + + if restored_ast == expected_ast { + Some(restored) + } else { + None + } +} From 38112d7603810de55b67f5ec0d183c3a76082aa3 Mon Sep 17 00:00:00 2001 From: psteinroe Date: Sun, 25 Oct 2026 16:28:06 +0000 Subject: [PATCH 2/2] refactor(workspace): share the rule selection of the linter and splinter visitors LintVisitor and SplinterVisitor only differed in the rules that the configuration enables and disables, and in the check of linter.enabled and typecheck.enabled for the lint rules. Both are now a RuleSelection, and AnalyserVisitorBuilder passes the configured rules and applies the check. The splinter category is of the lint kind, so filtering the categories to lint as LintVisitor did visits the same rules. --- .../src/workspace/server/analyser.rs | 212 ++++-------------- 1 file changed, 48 insertions(+), 164 deletions(-) diff --git a/crates/pgls_workspace/src/workspace/server/analyser.rs b/crates/pgls_workspace/src/workspace/server/analyser.rs index 7f88c3a2c..c2860617c 100644 --- a/crates/pgls_workspace/src/workspace/server/analyser.rs +++ b/crates/pgls_workspace/src/workspace/server/analyser.rs @@ -5,9 +5,9 @@ use rustc_hash::FxHashSet; use crate::settings::Settings; pub(crate) struct AnalyserVisitorBuilder<'a> { - lint: Option>, + lint: Option>, #[cfg(feature = "db")] - splinter: Option>, + splinter: Option>, settings: &'a Settings, } @@ -35,7 +35,7 @@ impl<'a> AnalyserVisitorBuilder<'a> { only: &'a [RuleSelector], skip: &'a [RuleSelector], ) -> Self { - self.lint = Some(LintVisitor::new(only, skip, self.settings)); + self.lint = Some(RuleSelection::new(only, skip)); self } @@ -46,7 +46,7 @@ impl<'a> AnalyserVisitorBuilder<'a> { only: &'a [RuleSelector], skip: &'a [RuleSelector], ) -> Self { - self.splinter = Some(SplinterVisitor::new(only, skip, self.settings)); + self.splinter = Some(RuleSelection::new(only, skip)); self } @@ -56,14 +56,38 @@ impl<'a> AnalyserVisitorBuilder<'a> { let mut enabled_rules = vec![]; if let Some(mut lint) = self.lint { pgls_analyser::visit_registry(&mut lint); - let (linter_enabled_rules, linter_disabled_rules) = lint.finish(); + let (mut linter_enabled_rules, linter_disabled_rules) = lint.finish( + self.settings + .as_linter_rules() + .map(|rules| (rules.as_enabled_rules(), rules.as_disabled_rules())), + ); + + // `linter.enabled` switches the lint rules, and `typecheck.enabled` the typecheck + // rules. + let linter_enabled = self.settings.linter.enabled; + let typecheck_enabled = self.settings.typecheck.enabled; + linter_enabled_rules.retain(|filter| { + let group = match filter { + RuleFilter::Group(group) | RuleFilter::Rule(group, _) => *group, + }; + if group == pgls_analyser::TYPECHECK_GROUP { + typecheck_enabled + } else { + linter_enabled + } + }); + enabled_rules.extend(linter_enabled_rules); disabled_rules.extend(linter_disabled_rules); } #[cfg(feature = "db")] if let Some(mut splinter) = self.splinter { pgls_splinter::registry::visit_registry(&mut splinter); - let (splinter_enabled_rules, splinter_disabled_rules) = splinter.finish(); + let (splinter_enabled_rules, splinter_disabled_rules) = splinter.finish( + self.settings + .as_splinter_rules() + .map(|rules| (rules.as_enabled_rules(), rules.as_disabled_rules())), + ); enabled_rules.extend(splinter_enabled_rules); disabled_rules.extend(splinter_disabled_rules); } @@ -72,93 +96,51 @@ impl<'a> AnalyserVisitorBuilder<'a> { } } -/// Type meant to register all the lint rules +/// The rules of a registry that `only` and `skip` select, visiting the registry of the linter +/// or of splinter. #[derive(Debug)] -struct LintVisitor<'a> { - pub(crate) enabled_rules: FxHashSet>, - pub(crate) disabled_rules: FxHashSet>, +struct RuleSelection<'a> { + enabled_rules: FxHashSet>, + disabled_rules: FxHashSet>, only: &'a [RuleSelector], skip: &'a [RuleSelector], - settings: &'a Settings, } -impl<'a> LintVisitor<'a> { - pub(crate) fn new( - only: &'a [RuleSelector], - skip: &'a [RuleSelector], - settings: &'a Settings, - ) -> Self { +impl<'a> RuleSelection<'a> { + fn new(only: &'a [RuleSelector], skip: &'a [RuleSelector]) -> Self { Self { enabled_rules: Default::default(), disabled_rules: Default::default(), only, skip, - settings, } } + /// The enabled and the disabled rules. Without `only`, they include the rules that the + /// configuration enables and disables, `configured`. fn finish( mut self, + configured: Option<( + FxHashSet>, + FxHashSet>, + )>, ) -> ( FxHashSet>, FxHashSet>, ) { - let has_only_filter = !self.only.is_empty(); - - if !has_only_filter { - let enabled_rules = self - .settings - .as_linter_rules() - .map(|rules| rules.as_enabled_rules()) - .unwrap_or_default(); - + if self.only.is_empty() { + let (enabled_rules, disabled_rules) = configured.unwrap_or_default(); self.enabled_rules.extend(enabled_rules); - - let disabled_rules = self - .settings - .as_linter_rules() - .map(|rules| rules.as_disabled_rules()) - .unwrap_or_default(); self.disabled_rules.extend(disabled_rules); } - // `linter.enabled` switches the lint rules, and `typecheck.enabled` the typecheck rules. - let linter_enabled = self.settings.linter.enabled; - let typecheck_enabled = self.settings.typecheck.enabled; - self.enabled_rules.retain(|filter| { - let group = match filter { - RuleFilter::Group(group) | RuleFilter::Rule(group, _) => *group, - }; - if group == pgls_analyser::TYPECHECK_GROUP { - typecheck_enabled - } else { - linter_enabled - } - }); - (self.enabled_rules, self.disabled_rules) } - - fn push_rule(&mut self) - where - R: RuleMeta + 'static, - { - // Do not report unused suppression comment diagnostics if a single rule is run. - for filter in self.only.iter().flat_map(|selector| selector.filters()) { - if filter.match_rule::() { - self.enabled_rules.insert(filter); - } - } - for filter in self.skip.iter().flat_map(|selector| selector.filters()) { - if filter.match_rule::() { - self.disabled_rules.insert(filter); - } - } - } } -impl RegistryVisitor for LintVisitor<'_> { +impl RegistryVisitor for RuleSelection<'_> { fn record_category(&mut self) { + // The splinter rules are of the lint kind too. if C::CATEGORY == RuleCategory::Lint { C::record_groups(self) } @@ -188,69 +170,7 @@ impl RegistryVisitor for LintVisitor<'_> { where R: RuleMeta + 'static, { - self.push_rule::() - } -} - -/// Type meant to register all the splinter (database lint) rules -#[cfg(feature = "db")] -#[derive(Debug)] -struct SplinterVisitor<'a> { - pub(crate) enabled_rules: FxHashSet>, - pub(crate) disabled_rules: FxHashSet>, - only: &'a [RuleSelector], - skip: &'a [RuleSelector], - settings: &'a Settings, -} - -#[cfg(feature = "db")] -impl<'a> SplinterVisitor<'a> { - pub(crate) fn new( - only: &'a [RuleSelector], - skip: &'a [RuleSelector], - settings: &'a Settings, - ) -> Self { - Self { - enabled_rules: Default::default(), - disabled_rules: Default::default(), - only, - skip, - settings, - } - } - - fn finish( - mut self, - ) -> ( - FxHashSet>, - FxHashSet>, - ) { - let has_only_filter = !self.only.is_empty(); - - if !has_only_filter { - let enabled_rules = self - .settings - .as_splinter_rules() - .map(|rules| rules.as_enabled_rules()) - .unwrap_or_default(); - - self.enabled_rules.extend(enabled_rules); - - let disabled_rules = self - .settings - .as_splinter_rules() - .map(|rules| rules.as_disabled_rules()) - .unwrap_or_default(); - self.disabled_rules.extend(disabled_rules); - } - - (self.enabled_rules, self.disabled_rules) - } - - fn push_rule(&mut self) - where - R: RuleMeta + 'static, - { + // Do not report unused suppression comment diagnostics if a single rule is run. for filter in self.only.iter().flat_map(|selector| selector.filters()) { if filter.match_rule::() { self.enabled_rules.insert(filter); @@ -264,42 +184,6 @@ impl<'a> SplinterVisitor<'a> { } } -#[cfg(feature = "db")] -impl RegistryVisitor for SplinterVisitor<'_> { - fn record_category(&mut self) { - // Splinter uses Lint as its kind in declare_category! macro - // We always record because we're visiting the splinter registry specifically - C::record_groups(self) - } - - fn record_group(&mut self) { - if self - .only - .iter() - .flat_map(|selector| selector.filters()) - .any(|filter| filter.match_group::()) - { - G::record_rules(self) - } - - if self - .skip - .iter() - .flat_map(|selector| selector.filters()) - .any(|filter| filter.match_group::()) - { - G::record_rules(self) - } - } - - fn record_rule(&mut self) - where - R: RuleMeta + 'static, - { - self.push_rule::() - } -} - #[cfg(test)] mod tests { use pgls_analyse::RuleFilter;