diff --git a/Cargo.lock b/Cargo.lock index ba760651d..2e60b8df0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2475,14 +2475,18 @@ name = "squawk-linter" version = "2.49.0" dependencies = [ "annotate-snippets", + "anyhow", "enum-iterator", "insta", "line-index", + "log", "rowan", "rustc-hash 2.1.1", "serde", "serde_plain", "squawk-syntax", + "tempfile", + "toml", ] [[package]] @@ -2505,6 +2509,7 @@ dependencies = [ "anyhow", "crossbeam-channel", "etcetera", + "glob", "insta", "line-index", "log", diff --git a/crates/squawk/src/config.rs b/crates/squawk/src/config.rs index e7312bef3..d6c36432c 100644 --- a/crates/squawk/src/config.rs +++ b/crates/squawk/src/config.rs @@ -1,60 +1,13 @@ -use anyhow::{Context, Result}; use log::info; -use serde::Deserialize; +use squawk_linter::config::{ConfigFile, UploadToGitHubConfig}; use squawk_linter::{Rule, Version}; use std::{ - env, io::{self, IsTerminal}, - path::{Path, PathBuf}, process, }; use crate::{Command, DebugOption, Opts, Reporter, UploadToGithubArgs}; -const FILE_NAME: &str = ".squawk.toml"; - -#[derive(Debug, Default, Deserialize)] -pub struct UploadToGitHubConfig { - #[serde(default)] - pub fail_on_violations: Option, -} - -#[derive(Debug, Default, Deserialize)] -pub struct ConfigFile { - #[serde(default)] - pub excluded_paths: Vec, - #[serde(default)] - pub excluded_rules: Vec, - #[serde(default)] - pub included_rules: Vec, - #[serde(default)] - pub pg_version: Option, - #[serde(default)] - pub assume_in_transaction: Option, - #[serde(default)] - pub upload_to_github: UploadToGitHubConfig, -} - -impl ConfigFile { - pub fn parse(custom_path: Option) -> Result> { - let path = if let Some(path) = custom_path { - Some(path) - } else { - find_by_traversing_back()? - }; - - if let Some(p) = path { - info!("using config file path: {}", p.display()); - - let file_content = std::fs::read_to_string(p)?; - return Ok(Some(toml::from_str(&file_content)?)); - } - - info!("no config file found"); - Ok(None) - } -} - pub struct Config { pub excluded_paths: Vec, pub excluded_rules: Vec, @@ -165,116 +118,3 @@ impl Config { } } } - -fn recurse_directory(directory: &Path, file_name: &str) -> Result, std::io::Error> { - for entry in directory.read_dir()? { - let entry = entry?; - if entry.file_name() == file_name { - return Ok(Some(entry.path())); - } - } - if let Some(parent) = directory.parent() { - recurse_directory(parent, file_name) - } else { - Ok(None) - } -} - -fn find_by_traversing_back() -> Result> { - recurse_directory(&env::current_dir()?, FILE_NAME) - .context("Error when finding configuration file") -} - -#[cfg(test)] -mod test_config { - use std::fs; - use tempfile::NamedTempFile; - - use insta::assert_debug_snapshot; - - use super::*; - - #[test] - fn load_cfg_full() { - let squawk_toml = NamedTempFile::new().expect("generate tempFile"); - let file = r#" -pg_version = "19.1" -excluded_paths = ["example.sql"] -excluded_rules = ["require-concurrent-index-creation"] -assume_in_transaction = true - - "#; - fs::write(&squawk_toml, file).expect("Unable to write file"); - assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); - } - #[test] - fn load_pg_version() { - let squawk_toml = NamedTempFile::new().expect("generate tempFile"); - let file = r#" -pg_version = "19.1" - - "#; - fs::write(&squawk_toml, file).expect("Unable to write file"); - assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); - } - #[test] - fn load_excluded_rules() { - let squawk_toml = NamedTempFile::new().expect("generate tempFile"); - let file = r#" -excluded_rules = ["require-concurrent-index-creation"] - - "#; - fs::write(&squawk_toml, file).expect("Unable to write file"); - assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); - } - #[test] - fn load_excluded_paths() { - let squawk_toml = NamedTempFile::new().expect("generate tempFile"); - let file = r#" -excluded_paths = ["example.sql"] - - "#; - fs::write(&squawk_toml, file).expect("Unable to write file"); - assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); - } - #[test] - fn load_assume_in_transaction() { - let squawk_toml = NamedTempFile::new().expect("generate tempFile"); - let file = r" -assume_in_transaction = false - - "; - fs::write(&squawk_toml, file).expect("Unable to write file"); - assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); - } - #[test] - fn load_fail_on_violations() { - let squawk_toml = NamedTempFile::new().expect("generate tempFile"); - let file = r" -[upload_to_github] -fail_on_violations = true - "; - fs::write(&squawk_toml, file).expect("Unable to write file"); - assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); - } - #[test] - fn load_included_rules() { - let squawk_toml = NamedTempFile::new().expect("generate tempFile"); - let file = r#" -included_rules = ["require-table-schema"] - - "#; - fs::write(&squawk_toml, file).expect("Unable to write file"); - assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); - } - #[test] - fn load_excluded_rules_with_alias() { - let squawk_toml = NamedTempFile::new().expect("generate tempFile"); - let file = r#" -excluded_rules = ["prefer-timestamp-tz", "prefer-timestamptz"] - - "#; - fs::write(&squawk_toml, file).expect("Unable to write file"); - assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); - } -} diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index de1d88736..6768399f5 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -422,14 +422,11 @@ fn build_type<'a>(ty: ast::Type) -> Doc<'a> { fn leading_comments_token<'a>(node: &SyntaxToken) -> Doc<'a> { let mut doc = Doc::nil(); for next in node.siblings_with_tokens(Direction::Prev).skip(1) { - println!("prev"); match next { - rowan::NodeOrToken::Node(node) => { - println!("before node {:?}", node); + rowan::NodeOrToken::Node(_) => { break; } rowan::NodeOrToken::Token(token) => { - println!("before token {:?}", token); if token.kind() == SyntaxKind::COMMENT { doc = doc .append(Doc::text(token.text().to_string())) @@ -448,14 +445,11 @@ fn leading_comments_token<'a>(node: &SyntaxToken) -> Doc<'a> { fn leading_comments<'a>(node: &SyntaxNode) -> Doc<'a> { let mut doc = Doc::nil(); for next in node.siblings_with_tokens(Direction::Prev).skip(1) { - println!("prev"); match next { - rowan::NodeOrToken::Node(node) => { - println!("before node {:?}", node); + rowan::NodeOrToken::Node(_) => { break; } rowan::NodeOrToken::Token(token) => { - println!("before token {:?}", token); if token.kind() == SyntaxKind::COMMENT { let is_block = token.text().starts_with("--"); doc = doc @@ -479,14 +473,11 @@ fn leading_comments<'a>(node: &SyntaxNode) -> Doc<'a> { fn trailing_comments<'a>(node: &SyntaxNode) -> Doc<'a> { let mut doc = Doc::nil(); for next in node.siblings_with_tokens(Direction::Next).skip(1) { - println!("after"); match next { - rowan::NodeOrToken::Node(node) => { - println!("after node {:?}", node); + rowan::NodeOrToken::Node(_) => { break; } rowan::NodeOrToken::Token(token) => { - println!("after token {:?}", token); if token.kind() == SyntaxKind::COMMENT { doc = doc .append(Doc::space()) @@ -532,10 +523,6 @@ fn build_target<'a>(target: ast::Target) -> Option> { pub fn fmt(text: &str) -> String { let parse = ast::SourceFile::parse(text); let file = parse.tree(); - println!("{}", text); - println!("---"); - println!("{:#?}", file.syntax()); - println!("---"); debug_assert_eq!( parse.errors(), vec![], diff --git a/crates/squawk_linter/Cargo.toml b/crates/squawk_linter/Cargo.toml index fa6de9398..c067df893 100644 --- a/crates/squawk_linter/Cargo.toml +++ b/crates/squawk_linter/Cargo.toml @@ -23,8 +23,12 @@ line-index.workspace = true serde_plain.workspace = true annotate-snippets.workspace = true rustc-hash.workspace = true +toml.workspace = true +anyhow.workspace = true +log.workspace = true [dev-dependencies] +tempfile.workspace = true insta.workspace = true [lints] diff --git a/crates/squawk_linter/src/config.rs b/crates/squawk_linter/src/config.rs new file mode 100644 index 000000000..c911ac560 --- /dev/null +++ b/crates/squawk_linter/src/config.rs @@ -0,0 +1,175 @@ +use anyhow::{Context, Result}; +use log::info; +use serde::Deserialize; +use std::{ + env, + path::{Path, PathBuf}, +}; + +use crate::{Rule, Version}; + +const FILE_NAME: &str = ".squawk.toml"; + +#[derive(Debug, Default, Deserialize)] +pub struct UploadToGitHubConfig { + #[serde(default)] + pub fail_on_violations: Option, +} + +#[derive(Debug, Default, Deserialize)] +pub struct ConfigFile { + #[serde(default)] + pub excluded_paths: Vec, + #[serde(default)] + pub excluded_rules: Vec, + #[serde(default)] + pub included_rules: Vec, + #[serde(default)] + pub pg_version: Option, + #[serde(default)] + pub assume_in_transaction: Option, + #[serde(default)] + pub upload_to_github: UploadToGitHubConfig, +} + +impl ConfigFile { + pub fn parse(custom_path: Option) -> Result> { + let path = if let Some(path) = custom_path { + Some(path) + } else { + find_by_traversing_back()? + }; + Self::load(path) + } + + /// Search for `.squawk.toml` starting from the given root directory, + /// traversing up to parent directories. + pub fn find_and_parse(root: &Path) -> Result> { + let path = + recurse_directory(root, FILE_NAME).context("Error when finding configuration file")?; + Self::load(path) + } + + fn load(path: Option) -> Result> { + if let Some(p) = path { + info!("using config file path: {}", p.display()); + let file_content = std::fs::read_to_string(p)?; + return Ok(Some(toml::from_str(&file_content)?)); + } + info!("no config file found"); + Ok(None) + } +} + +fn recurse_directory(directory: &Path, file_name: &str) -> Result, std::io::Error> { + for entry in directory.read_dir()? { + let entry = entry?; + if entry.file_name() == file_name { + return Ok(Some(entry.path())); + } + } + if let Some(parent) = directory.parent() { + recurse_directory(parent, file_name) + } else { + Ok(None) + } +} + +fn find_by_traversing_back() -> Result> { + recurse_directory(&env::current_dir()?, FILE_NAME) + .context("Error when finding configuration file") +} + +#[cfg(test)] +mod test_config { + use std::fs; + use tempfile::NamedTempFile; + + use insta::assert_debug_snapshot; + + use super::*; + + #[test] + fn load_cfg_full() { + let squawk_toml = NamedTempFile::new().expect("generate tempFile"); + let file = r#" +pg_version = "19.1" +excluded_paths = ["example.sql"] +excluded_rules = ["require-concurrent-index-creation"] +assume_in_transaction = true + + "#; + fs::write(&squawk_toml, file).expect("Unable to write file"); + assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); + } + #[test] + fn load_pg_version() { + let squawk_toml = NamedTempFile::new().expect("generate tempFile"); + let file = r#" +pg_version = "19.1" + + "#; + fs::write(&squawk_toml, file).expect("Unable to write file"); + assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); + } + #[test] + fn load_excluded_rules() { + let squawk_toml = NamedTempFile::new().expect("generate tempFile"); + let file = r#" +excluded_rules = ["require-concurrent-index-creation"] + + "#; + fs::write(&squawk_toml, file).expect("Unable to write file"); + assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); + } + #[test] + fn load_excluded_paths() { + let squawk_toml = NamedTempFile::new().expect("generate tempFile"); + let file = r#" +excluded_paths = ["example.sql"] + + "#; + fs::write(&squawk_toml, file).expect("Unable to write file"); + assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); + } + #[test] + fn load_assume_in_transaction() { + let squawk_toml = NamedTempFile::new().expect("generate tempFile"); + let file = r" +assume_in_transaction = false + + "; + fs::write(&squawk_toml, file).expect("Unable to write file"); + assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); + } + #[test] + fn load_fail_on_violations() { + let squawk_toml = NamedTempFile::new().expect("generate tempFile"); + let file = r" +[upload_to_github] +fail_on_violations = true + "; + fs::write(&squawk_toml, file).expect("Unable to write file"); + assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); + } + #[test] + fn load_included_rules() { + let squawk_toml = NamedTempFile::new().expect("generate tempFile"); + let file = r#" +included_rules = ["require-table-schema"] + + "#; + fs::write(&squawk_toml, file).expect("Unable to write file"); + assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); + } + #[test] + fn load_excluded_rules_with_alias() { + let squawk_toml = NamedTempFile::new().expect("generate tempFile"); + let file = r#" +excluded_rules = ["prefer-timestamp-tz", "prefer-timestamptz"] + + "#; + fs::write(&squawk_toml, file).expect("Unable to write file"); + assert_debug_snapshot!(ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))); + } +} diff --git a/crates/squawk_linter/src/lib.rs b/crates/squawk_linter/src/lib.rs index 2fda9d047..a6173637f 100644 --- a/crates/squawk_linter/src/lib.rs +++ b/crates/squawk_linter/src/lib.rs @@ -16,6 +16,7 @@ use squawk_syntax::{Parse, SourceFile}; pub use version::Version; pub mod analyze; +pub mod config; pub mod ignore; mod ignore_index; mod version; diff --git a/crates/squawk/src/snapshots/squawk__config__test_config__load_assume_in_transaction.snap b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_assume_in_transaction.snap similarity index 87% rename from crates/squawk/src/snapshots/squawk__config__test_config__load_assume_in_transaction.snap rename to crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_assume_in_transaction.snap index b8a89dea3..7c87db224 100644 --- a/crates/squawk/src/snapshots/squawk__config__test_config__load_assume_in_transaction.snap +++ b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_assume_in_transaction.snap @@ -1,5 +1,6 @@ --- -source: crates/squawk/src/config.rs +source: crates/squawk_linter/src/config.rs +assertion_line: 148 expression: "ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))" --- Ok( diff --git a/crates/squawk/src/snapshots/squawk__config__test_config__load_cfg_full.snap b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_cfg_full.snap similarity index 92% rename from crates/squawk/src/snapshots/squawk__config__test_config__load_cfg_full.snap rename to crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_cfg_full.snap index cc6e930be..c1fa9140f 100644 --- a/crates/squawk/src/snapshots/squawk__config__test_config__load_cfg_full.snap +++ b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_cfg_full.snap @@ -1,5 +1,6 @@ --- -source: crates/squawk/src/config.rs +source: crates/squawk_linter/src/config.rs +assertion_line: 108 expression: "ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))" --- Ok( diff --git a/crates/squawk/src/snapshots/squawk__config__test_config__load_excluded_paths.snap b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_excluded_paths.snap similarity index 87% rename from crates/squawk/src/snapshots/squawk__config__test_config__load_excluded_paths.snap rename to crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_excluded_paths.snap index 183c81821..de56fda21 100644 --- a/crates/squawk/src/snapshots/squawk__config__test_config__load_excluded_paths.snap +++ b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_excluded_paths.snap @@ -1,5 +1,6 @@ --- -source: crates/squawk/src/config.rs +source: crates/squawk_linter/src/config.rs +assertion_line: 138 expression: "ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))" --- Ok( diff --git a/crates/squawk/src/snapshots/squawk__config__test_config__load_excluded_rules.snap b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_excluded_rules.snap similarity index 88% rename from crates/squawk/src/snapshots/squawk__config__test_config__load_excluded_rules.snap rename to crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_excluded_rules.snap index 9aabf2381..a3b19fb2b 100644 --- a/crates/squawk/src/snapshots/squawk__config__test_config__load_excluded_rules.snap +++ b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_excluded_rules.snap @@ -1,5 +1,6 @@ --- -source: crates/squawk/src/config.rs +source: crates/squawk_linter/src/config.rs +assertion_line: 128 expression: "ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))" --- Ok( diff --git a/crates/squawk/src/snapshots/squawk__config__test_config__load_excluded_rules_with_alias.snap b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_excluded_rules_with_alias.snap similarity index 88% rename from crates/squawk/src/snapshots/squawk__config__test_config__load_excluded_rules_with_alias.snap rename to crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_excluded_rules_with_alias.snap index 01910f516..7f8dcf779 100644 --- a/crates/squawk/src/snapshots/squawk__config__test_config__load_excluded_rules_with_alias.snap +++ b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_excluded_rules_with_alias.snap @@ -1,5 +1,6 @@ --- -source: crates/squawk/src/config.rs +source: crates/squawk_linter/src/config.rs +assertion_line: 168 expression: "ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))" --- Ok( diff --git a/crates/squawk/src/snapshots/squawk__config__test_config__load_fail_on_violations.snap b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_fail_on_violations.snap similarity index 87% rename from crates/squawk/src/snapshots/squawk__config__test_config__load_fail_on_violations.snap rename to crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_fail_on_violations.snap index 994c15fb0..1ffb898ce 100644 --- a/crates/squawk/src/snapshots/squawk__config__test_config__load_fail_on_violations.snap +++ b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_fail_on_violations.snap @@ -1,5 +1,6 @@ --- -source: crates/squawk/src/config.rs +source: crates/squawk_linter/src/config.rs +assertion_line: 158 expression: "ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))" --- Ok( diff --git a/crates/squawk/src/snapshots/squawk__config__test_config__load_included_rules.snap b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_included_rules.snap similarity index 91% rename from crates/squawk/src/snapshots/squawk__config__test_config__load_included_rules.snap rename to crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_included_rules.snap index 1977a09b8..4a3674e0f 100644 --- a/crates/squawk/src/snapshots/squawk__config__test_config__load_included_rules.snap +++ b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_included_rules.snap @@ -1,5 +1,5 @@ --- -source: crates/squawk/src/config.rs +source: crates/squawk_linter/src/config.rs expression: "ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))" --- Ok( diff --git a/crates/squawk/src/snapshots/squawk__config__test_config__load_pg_version.snap b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_pg_version.snap similarity index 90% rename from crates/squawk/src/snapshots/squawk__config__test_config__load_pg_version.snap rename to crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_pg_version.snap index c380224bf..b71d8ce90 100644 --- a/crates/squawk/src/snapshots/squawk__config__test_config__load_pg_version.snap +++ b/crates/squawk_linter/src/snapshots/squawk_linter__config__test_config__load_pg_version.snap @@ -1,5 +1,6 @@ --- -source: crates/squawk/src/config.rs +source: crates/squawk_linter/src/config.rs +assertion_line: 118 expression: "ConfigFile::parse(Some(squawk_toml.path().to_path_buf()))" --- Ok( diff --git a/crates/squawk_server/Cargo.toml b/crates/squawk_server/Cargo.toml index ff6e2cbe4..30a827e8b 100644 --- a/crates/squawk_server/Cargo.toml +++ b/crates/squawk_server/Cargo.toml @@ -33,6 +33,7 @@ rustc-hash.workspace = true squawk-thread.workspace = true crossbeam-channel.workspace = true tracing.workspace = true +glob.workspace = true [dev-dependencies] insta.workspace = true diff --git a/crates/squawk_server/src/config.rs b/crates/squawk_server/src/config.rs new file mode 100644 index 000000000..58bee90bb --- /dev/null +++ b/crates/squawk_server/src/config.rs @@ -0,0 +1,84 @@ +use lsp_types::InitializeParams; +use squawk_linter::config::ConfigFile; +use squawk_linter::{Linter, Rule, Version}; +use std::path::PathBuf; +use std::sync::Arc; + +/// Linter-relevant config extracted from `.squawk.toml`, stored once +/// in `GlobalState` and shared via Arc into Snapshots. +#[derive(Default)] +pub(crate) struct LintConfig { + pub excluded_rules: Vec, + pub included_rules: Vec, + pub pg_version: Option, + pub assume_in_transaction: bool, + pub excluded_paths: Vec, + pub workspace_root: Option, +} + +impl LintConfig { + /// Resolve the lint config from LSP initialization parameters by locating + /// the workspace root and parsing any `.squawk.toml` found above it. + pub fn from_init_params(init_params: &InitializeParams) -> Arc { + let workspace_root = workspace_root_from_init_params(init_params); + let config = workspace_root.as_ref().and_then(|root| { + ConfigFile::find_and_parse(root) + .map_err(|e| log::warn!("error loading config: {e}")) + .ok() + .flatten() + }); + Self::build(config.unwrap_or_default(), workspace_root) + } + + fn build(config: ConfigFile, workspace_root: Option) -> Arc { + let excluded_paths = config + .excluded_paths + .iter() + .filter_map(|p| glob::Pattern::new(p).ok()) + .collect(); + Arc::new(Self { + excluded_rules: config.excluded_rules, + included_rules: config.included_rules, + pg_version: config.pg_version, + assume_in_transaction: config.assume_in_transaction.unwrap_or(false), + excluded_paths, + workspace_root, + }) + } + + pub fn new_linter(&self) -> Linter { + let mut linter = Linter::with_rules(&self.included_rules, &self.excluded_rules); + if let Some(pg_version) = self.pg_version { + linter.settings.pg_version = pg_version; + } + linter.settings.assume_in_transaction = self.assume_in_transaction; + linter + } +} + +/// Resolve the workspace root from LSP `InitializeParams`, preferring the +/// modern `workspace_folders` field and falling back to the deprecated +/// `root_uri` and `root_path` for older clients. +fn workspace_root_from_init_params(init_params: &InitializeParams) -> Option { + if let Some(folder) = init_params + .workspace_folders + .as_ref() + .and_then(|folders| folders.first()) + { + if let Ok(path) = folder.uri.to_file_path() { + return Some(path); + } + } + + #[allow(deprecated)] + { + if let Some(path) = init_params + .root_uri + .as_ref() + .and_then(|uri| uri.to_file_path().ok()) + { + return Some(path); + } + init_params.root_path.as_ref().map(PathBuf::from) + } +} diff --git a/crates/squawk_server/src/global_state.rs b/crates/squawk_server/src/global_state.rs index 84a29e292..73b59ce79 100644 --- a/crates/squawk_server/src/global_state.rs +++ b/crates/squawk_server/src/global_state.rs @@ -14,6 +14,8 @@ use squawk_ide::builtins::{builtins_file, builtins_url}; use squawk_ide::db::{Database, File}; use squawk_thread::TaskPool; +use crate::config::LintConfig; + use lsp_types::request::{ CodeActionRequest, Completion, DocumentDiagnosticRequest, DocumentSymbolRequest, FoldingRangeRequest, GotoDefinition, HoverRequest, InlayHintRequest, References, @@ -40,6 +42,7 @@ pub(super) struct GlobalState { db: Database, files: Arc>, uris: Arc>, + config: Arc, req_queue: ReqQueue, sender: Sender, pub(crate) task_pool: Handle, Receiver>, @@ -47,7 +50,7 @@ pub(super) struct GlobalState { } impl GlobalState { - pub(super) fn new(sender: Sender) -> Self { + pub(super) fn new(sender: Sender, config: Arc) -> Self { let threads = std::thread::available_parallelism().unwrap_or(NonZeroUsize::MIN); let task_pool = { let (sender, receiver) = unbounded(); @@ -64,6 +67,7 @@ impl GlobalState { db, files: Arc::new(FxHashMap::default()), uris: Arc::new(uris), + config, req_queue: ReqQueue::default(), task_pool, sender, @@ -77,6 +81,7 @@ impl GlobalState { db: self.db.clone(), files: self.files.clone(), uris: self.uris.clone(), + config: self.config.clone(), } } @@ -256,6 +261,7 @@ pub(crate) struct Snapshot { pub(crate) db: Database, pub(crate) files: Arc>, pub(crate) uris: Arc>, + pub(crate) config: Arc, } impl Snapshot { diff --git a/crates/squawk_server/src/handlers/diagnostic.rs b/crates/squawk_server/src/handlers/diagnostic.rs index 70e981c6f..102a28a38 100644 --- a/crates/squawk_server/src/handlers/diagnostic.rs +++ b/crates/squawk_server/src/handlers/diagnostic.rs @@ -6,16 +6,40 @@ use lsp_types::{ use crate::global_state::Snapshot; +fn is_path_excluded( + uri: &lsp_types::Url, + excluded_paths: &[glob::Pattern], + workspace_root: Option<&std::path::Path>, +) -> bool { + let Ok(file_path) = uri.to_file_path() else { + return false; + }; + let file_path_str = file_path.to_string_lossy(); + let relative_path = workspace_root.and_then(|root| file_path.strip_prefix(root).ok()); + excluded_paths.iter().any(|pattern| { + pattern.matches(&file_path_str) + || relative_path.is_some_and(|rel| pattern.matches(&rel.to_string_lossy())) + }) +} + pub(crate) fn handle_document_diagnostic( snapshot: &Snapshot, params: DocumentDiagnosticParams, ) -> Result { let uri = params.text_document.uri; - let diagnostics = snapshot - .file(&uri) - .map(|file| crate::lint::lint(snapshot.db(), file)) - .unwrap_or_default(); + let diagnostics = if is_path_excluded( + &uri, + &snapshot.config.excluded_paths, + snapshot.config.workspace_root.as_deref(), + ) { + vec![] + } else { + snapshot + .file(&uri) + .map(|file| crate::lint::lint(snapshot.db(), file, &snapshot.config)) + .unwrap_or_default() + }; Ok(DocumentDiagnosticReportResult::Report( DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport { diff --git a/crates/squawk_server/src/ignore.rs b/crates/squawk_server/src/ignore.rs index 1d0dc9ba1..3824b6b15 100644 --- a/crates/squawk_server/src/ignore.rs +++ b/crates/squawk_server/src/ignore.rs @@ -203,6 +203,7 @@ create table c ( fn lint_sql(sql: &str) -> Vec { let db = Database::default(); let file = File::new(&db, sql.to_owned().into()); - lint(&db, file) + let config = crate::config::LintConfig::default(); + lint(&db, file, &config) } } diff --git a/crates/squawk_server/src/lib.rs b/crates/squawk_server/src/lib.rs index e57a3da48..20158bd2d 100644 --- a/crates/squawk_server/src/lib.rs +++ b/crates/squawk_server/src/lib.rs @@ -1,3 +1,4 @@ +mod config; mod diagnostic; mod dispatch; mod global_state; diff --git a/crates/squawk_server/src/lint.rs b/crates/squawk_server/src/lint.rs index 39d8340b5..f96e14565 100644 --- a/crates/squawk_server/src/lint.rs +++ b/crates/squawk_server/src/lint.rs @@ -2,9 +2,10 @@ use ::line_index::LineIndex; use lsp_types::{CodeDescription, Diagnostic, DiagnosticSeverity, Position, Range, TextEdit, Url}; use salsa::Database as Db; use squawk_ide::db::{File, line_index as file_line_index, parse}; -use squawk_linter::{Edit, Linter}; +use squawk_linter::Edit; use crate::{ + config::LintConfig, diagnostic::{AssociatedDiagnosticData, DIAGNOSTIC_NAME}, ignore::{ignore_file_edit, ignore_line_edit}, }; @@ -19,12 +20,11 @@ fn to_text_edit(edit: Edit, line_index: &LineIndex) -> Option { Some(TextEdit::new(range, edit.text.unwrap_or_default())) } -#[salsa::tracked] -pub(crate) fn lint(db: &dyn Db, file: File) -> Vec { +pub(crate) fn lint(db: &dyn Db, file: File, config: &LintConfig) -> Vec { let parse = parse(db, file); let content = file.content(db); let parse_errors = parse.errors(); - let mut linter = Linter::with_default_rules(); + let mut linter = config.new_linter(); let violations = linter.lint(&parse, content); let line_index = file_line_index(db, file); diff --git a/crates/squawk_server/src/lsp_utils.rs b/crates/squawk_server/src/lsp_utils.rs index 912f90604..5fc598549 100644 --- a/crates/squawk_server/src/lsp_utils.rs +++ b/crates/squawk_server/src/lsp_utils.rs @@ -244,6 +244,7 @@ pub(crate) fn to_semantic_tokens( text_range = TextRange::new(text_range.start(), text_range.end() - TextSize::of('\n')); } + // TODO: Convert these UTF-8 line columns to UTF-16 before encoding semantic tokens. let lsp_range = range(&line_index, text_range); let len = lsp_range.end.character - lsp_range.start.character; encoder.push_token_at(lsp_range.start, len, token.token_type, token.modifiers); diff --git a/crates/squawk_server/src/server.rs b/crates/squawk_server/src/server.rs index 1f846aeed..1b4ffea31 100644 --- a/crates/squawk_server/src/server.rs +++ b/crates/squawk_server/src/server.rs @@ -11,6 +11,7 @@ use lsp_types::{ }; use crate::{ + config::LintConfig, global_state::GlobalState, semantic_tokens::{SUPPORTED_MODIFIERS, SUPPORTED_TYPES}, }; @@ -92,8 +93,15 @@ fn main_loop(connection: Connection, params: serde_json::Value) -> Result<()> { let init_params: InitializeParams = serde_json::from_value(params).unwrap_or_default(); info!("Client process ID: {:?}", init_params.process_id); - let client_name = init_params.client_info.map(|x| x.name); + let client_name = init_params.client_info.as_ref().map(|x| x.name.clone()); info!("Client name: {client_name:?}"); - GlobalState::new(connection.sender).run(connection.receiver) + let config = LintConfig::from_init_params(&init_params); + info!("excluded rules: {:?}", config.excluded_rules); + info!("included rules: {:?}", config.included_rules); + info!("pg version: {:?}", config.pg_version); + info!("assume in transaction: {}", config.assume_in_transaction); + info!("excluded paths: {:?}", config.excluded_paths); + + GlobalState::new(connection.sender, config).run(connection.receiver) }