diff --git a/Cargo.lock b/Cargo.lock index 16b1de5e..d5c626e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3108,6 +3108,7 @@ dependencies = [ "pgt_test_utils", "pgt_text_size", "pgt_typecheck", + "pgt_workspace_macros", "rustc-hash 2.1.0", "schemars", "serde", @@ -3122,6 +3123,15 @@ dependencies = [ "tree_sitter_sql", ] +[[package]] +name = "pgt_workspace_macros" +version = "0.0.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "pin-project" version = "1.1.7" @@ -3341,9 +3351,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] diff --git a/Cargo.toml b/Cargo.toml index 15c6f02f..4a3454c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ convert_case = "0.6.0" prost-reflect = "0.15.3" protox = "0.8.0" sqlx = { version = "0.8.2", features = ["runtime-tokio", "runtime-async-std", "postgres", "json"] } -syn = "1.0.109" +syn = { version = "1.0.109", features = ["full"] } termcolor = "1.4.1" test-log = "0.2.17" tokio = { version = "1.40.0", features = ["full"] } @@ -85,6 +85,7 @@ pgt_tokenizer = { path = "./crates/pgt_tokenizer", version = "0.0.0 pgt_treesitter_queries = { path = "./crates/pgt_treesitter_queries", version = "0.0.0" } pgt_typecheck = { path = "./crates/pgt_typecheck", version = "0.0.0" } pgt_workspace = { path = "./crates/pgt_workspace", version = "0.0.0" } +pgt_workspace_macros = { path = "./crates/pgt_workspace_macros", version = "0.0.0" } pgt_test_macros = { path = "./crates/pgt_test_macros" } pgt_test_utils = { path = "./crates/pgt_test_utils" } diff --git a/crates/pgt_diagnostics/src/serde.rs b/crates/pgt_diagnostics/src/serde.rs index 334bd4e9..57ed3e28 100644 --- a/crates/pgt_diagnostics/src/serde.rs +++ b/crates/pgt_diagnostics/src/serde.rs @@ -164,6 +164,7 @@ 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, } @@ -250,7 +251,7 @@ impl super::Advices for Advices { #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -#[cfg_attr(test, derive(Eq, PartialEq))] +#[cfg_attr(test, derive(PartialEq, Eq))] enum Advice { Log(LogCategory, MarkupBuf), List(Vec), diff --git a/crates/pgt_workspace/Cargo.toml b/crates/pgt_workspace/Cargo.toml index 3ef4936b..e78f4391 100644 --- a/crates/pgt_workspace/Cargo.toml +++ b/crates/pgt_workspace/Cargo.toml @@ -32,6 +32,7 @@ pgt_statement_splitter = { workspace = true } pgt_suppressions = { workspace = true } pgt_text_size.workspace = true pgt_typecheck = { workspace = true } +pgt_workspace_macros = { workspace = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/pgt_workspace/src/features/code_actions.rs b/crates/pgt_workspace/src/features/code_actions.rs index 22223dd3..cd1706d3 100644 --- a/crates/pgt_workspace/src/features/code_actions.rs +++ b/crates/pgt_workspace/src/features/code_actions.rs @@ -12,7 +12,7 @@ pub struct CodeActionsParams { pub skip: Vec, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Default)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct CodeActionsResult { pub actions: Vec, @@ -57,7 +57,7 @@ pub struct ExecuteStatementParams { pub path: PgTPath, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Default, PartialEq, Eq)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct ExecuteStatementResult { pub message: String, diff --git a/crates/pgt_workspace/src/features/diagnostics.rs b/crates/pgt_workspace/src/features/diagnostics.rs index ff60e142..a697641e 100644 --- a/crates/pgt_workspace/src/features/diagnostics.rs +++ b/crates/pgt_workspace/src/features/diagnostics.rs @@ -12,7 +12,7 @@ pub struct PullDiagnosticsParams { pub skip: Vec, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Default)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct PullDiagnosticsResult { pub diagnostics: Vec, diff --git a/crates/pgt_workspace/src/workspace/server.rs b/crates/pgt_workspace/src/workspace/server.rs index e6456afc..c6ed0827 100644 --- a/crates/pgt_workspace/src/workspace/server.rs +++ b/crates/pgt_workspace/src/workspace/server.rs @@ -21,6 +21,7 @@ use pgt_diagnostics::{ }; use pgt_fs::{ConfigName, PgTPath}; use pgt_typecheck::{IdentifierType, TypecheckParams, TypedIdentifier}; +use pgt_workspace_macros::ignored_path; use schema_cache_manager::SchemaCacheManager; use sqlx::{Executor, PgPool}; use tracing::{debug, info}; @@ -30,7 +31,7 @@ use crate::{ configuration::to_analyser_rules, features::{ code_actions::{ - self, CodeAction, CodeActionKind, CodeActionsResult, CommandAction, + CodeAction, CodeActionKind, CodeActionsParams, CodeActionsResult, CommandAction, CommandActionCategory, ExecuteStatementParams, ExecuteStatementResult, }, completions::{CompletionsResult, GetCompletionsParams, get_statement_for_completions}, @@ -262,6 +263,7 @@ impl Workspace for WorkspaceServer { } /// Add a new file to the workspace + #[ignored_path(path=¶ms.path)] #[tracing::instrument(level = "info", skip_all, fields(path = params.path.as_path().as_os_str().to_str()), err)] fn open_file(&self, params: OpenFileParams) -> Result<(), WorkspaceError> { let mut documents = self.documents.write().unwrap(); @@ -277,6 +279,7 @@ impl Workspace for WorkspaceServer { } /// Remove a file from the workspace + #[ignored_path(path=¶ms.path)] fn close_file(&self, params: super::CloseFileParams) -> Result<(), WorkspaceError> { let mut documents = self.documents.write().unwrap(); documents @@ -291,6 +294,7 @@ impl Workspace for WorkspaceServer { path = params.path.as_os_str().to_str(), version = params.version ), err)] + #[ignored_path(path=¶ms.path)] fn change_file(&self, params: super::ChangeFileParams) -> Result<(), WorkspaceError> { let mut documents = self.documents.write().unwrap(); @@ -312,6 +316,7 @@ impl Workspace for WorkspaceServer { None } + #[ignored_path(path=¶ms.path)] fn get_file_content(&self, params: GetFileContentParams) -> Result { let documents = self.documents.read().unwrap(); let document = documents @@ -324,10 +329,11 @@ impl Workspace for WorkspaceServer { Ok(self.is_ignored(params.pgt_path.as_path())) } + #[ignored_path(path=¶ms.path)] fn pull_code_actions( &self, - params: code_actions::CodeActionsParams, - ) -> Result { + params: CodeActionsParams, + ) -> Result { let documents = self.documents.read().unwrap(); let parser = documents .get(¶ms.path) @@ -366,6 +372,7 @@ impl Workspace for WorkspaceServer { Ok(CodeActionsResult { actions }) } + #[ignored_path(path=¶ms.path)] fn execute_statement( &self, params: ExecuteStatementParams, @@ -409,6 +416,7 @@ impl Workspace for WorkspaceServer { }) } + #[ignored_path(path=¶ms.path)] fn pull_diagnostics( &self, params: PullDiagnosticsParams, @@ -607,6 +615,7 @@ impl Workspace for WorkspaceServer { }) } + #[ignored_path(path=¶ms.path)] #[tracing::instrument(level = "debug", skip_all, fields( path = params.path.as_os_str().to_str(), position = params.position.to_string() diff --git a/crates/pgt_workspace/src/workspace/server.tests.rs b/crates/pgt_workspace/src/workspace/server.tests.rs index c3fbf723..0578f90f 100644 --- a/crates/pgt_workspace/src/workspace/server.tests.rs +++ b/crates/pgt_workspace/src/workspace/server.tests.rs @@ -1,6 +1,10 @@ -use biome_deserialize::Merge; +use std::sync::Arc; + +use biome_deserialize::{Merge, StringSet}; use pgt_analyse::RuleCategories; -use pgt_configuration::{PartialConfiguration, database::PartialDatabaseConfiguration}; +use pgt_configuration::{ + PartialConfiguration, database::PartialDatabaseConfiguration, files::PartialFilesConfiguration, +}; use pgt_diagnostics::Diagnostic; use pgt_fs::PgTPath; use pgt_text_size::TextRange; @@ -8,8 +12,10 @@ use sqlx::PgPool; use crate::{ Workspace, WorkspaceError, + features::code_actions::ExecuteStatementResult, workspace::{ - OpenFileParams, RegisterProjectFolderParams, UpdateSettingsParams, server::WorkspaceServer, + OpenFileParams, RegisterProjectFolderParams, StatementId, UpdateSettingsParams, + server::WorkspaceServer, }, }; @@ -152,3 +158,51 @@ async fn test_syntax_error(test_db: PgPool) { Some(TextRange::new(7.into(), 15.into())) ); } + +#[tokio::test] +async fn correctly_ignores_files() { + let mut conf = PartialConfiguration::init(); + conf.merge_with(PartialConfiguration { + files: Some(PartialFilesConfiguration { + ignore: Some(StringSet::from_iter(["test.sql".to_string()])), + ..Default::default() + }), + ..Default::default() + }); + + let workspace = get_test_workspace(Some(conf)).expect("Unable to create test workspace"); + + let path = PgTPath::new("test.sql"); + let content = r#" + seect 1; + "#; + + let diagnostics_result = workspace.pull_diagnostics(crate::workspace::PullDiagnosticsParams { + path: path.clone(), + categories: RuleCategories::all(), + max_diagnostics: 100, + only: vec![], + skip: vec![], + }); + + assert!( + diagnostics_result.is_ok_and(|res| res.diagnostics.is_empty() + && res.errors == 0 + && res.skipped_diagnostics == 0) + ); + + let close_file_result = + workspace.close_file(crate::workspace::CloseFileParams { path: path.clone() }); + + assert!(close_file_result.is_ok()); + + let execute_statement_result = + workspace.execute_statement(crate::workspace::ExecuteStatementParams { + path: path.clone(), + statement_id: StatementId::Root { + content: Arc::from(content), + }, + }); + + assert!(execute_statement_result.is_ok_and(|res| res == ExecuteStatementResult::default())); +} diff --git a/crates/pgt_workspace_macros/Cargo.toml b/crates/pgt_workspace_macros/Cargo.toml new file mode 100644 index 00000000..c192db04 --- /dev/null +++ b/crates/pgt_workspace_macros/Cargo.toml @@ -0,0 +1,19 @@ +[package] +authors.workspace = true +categories.workspace = true +description = "" +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +name = "pgt_workspace_macros" +repository.workspace = true +version = "0.0.0" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = { version = "1.0.95" } +quote = { workspace = true } +syn = { workspace = true } diff --git a/crates/pgt_workspace_macros/src/lib.rs b/crates/pgt_workspace_macros/src/lib.rs new file mode 100644 index 00000000..d46f484d --- /dev/null +++ b/crates/pgt_workspace_macros/src/lib.rs @@ -0,0 +1,123 @@ +use std::ops::Deref; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{TypePath, TypeTuple, parse_macro_input}; + +struct IgnoredPath { + path: syn::Expr, +} + +impl syn::parse::Parse for IgnoredPath { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let arg_name: syn::Ident = input.parse()?; + + if arg_name != "path" { + return Err(syn::Error::new_spanned( + arg_name, + "Expected 'path' argument.", + )); + } + + let _: syn::Token!(=) = input.parse()?; + let path: syn::Expr = input.parse()?; + + Ok(Self { path }) + } +} + +#[proc_macro_attribute] +/// You can use this on a workspace server function to return a default if the specified path +/// is ignored by the user's settings. +/// +/// This will work for any function where &self is in scope and that returns `Result`, `Result<(), E>`, or `T`, where `T: Default`. +/// `path` needs to point at a `&PgTPath`. +/// +/// ### Usage +/// +/// ```ignore +/// impl WorkspaceServer { +/// #[ignore_path(path=¶ms.path)] +/// fn foo(&self, params: FooParams) -> Result { +/// ... codeblock +/// } +/// } +/// +/// // …expands to… +/// +/// impl WorkspaceServer { +/// fn foo(&self, params: FooParams) -> Result { +/// if self.is_ignored(¶ms.path) { +/// return Ok(FooResult::default()); +/// } +/// ... codeblock +/// } +/// } +/// ``` +pub fn ignored_path(args: TokenStream, input: TokenStream) -> TokenStream { + let ignored_path = parse_macro_input!(args as IgnoredPath); + let input_fn = parse_macro_input!(input as syn::ItemFn); + + let macro_specified_path = ignored_path.path; + + let vis = &input_fn.vis; + let sig = &input_fn.sig; + let block = &input_fn.block; + let attrs = &input_fn.attrs; + + // handles cases `fn foo() -> Result` and `fn foo() -> Result<(), E>` + // T needs to implement default + if let syn::ReturnType::Type(_, ty) = &sig.output { + if let syn::Type::Path(TypePath { path, .. }) = ty.deref() { + if let Some(seg) = path.segments.last() { + if seg.ident == "Result" { + if let syn::PathArguments::AngleBracketed(type_args) = &seg.arguments { + if let Some(syn::GenericArgument::Type(t)) = type_args.args.first() { + if let syn::Type::Tuple(TypeTuple { elems, .. }) = t { + // case: Result<(), E> + if elems.is_empty() { + return TokenStream::from(quote! { + #(#attrs)* + #vis #sig { + if self.is_ignored(#macro_specified_path) { + return Ok(()); + }; + #block + } + }); + } + } + if let syn::Type::Path(TypePath { path, .. }) = t { + if let Some(seg) = path.segments.first() { + let ident = &seg.ident; + return TokenStream::from(quote! { + #(#attrs)* + #vis #sig { + if self.is_ignored(#macro_specified_path) { + return Ok(#ident::default()); + }; + #block + } + }); + } + } + }; + }; + }; + }; + }; + }; + + // case fn foo() -> T {} + // handles all other T's + // T needs to implement Default + TokenStream::from(quote! { + #(#attrs)* + #vis #sig { + if self.is_ignored(#macro_specified_path) { + return Default::default(); + } + #block + } + }) +}