From 1e8de9cbe1a6254d35fd0d5fc3009b08b81dda20 Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 15 Jul 2025 08:51:48 +0200 Subject: [PATCH 01/11] intermediary --- Cargo.lock | 14 +++- Cargo.toml | 1 + crates/pgt_workspace/Cargo.toml | 1 + .../pgt_workspace/src/features/diagnostics.rs | 2 +- crates/pgt_workspace/src/workspace/server.rs | 2 + crates/pgt_workspace_macros/Cargo.toml | 21 +++++ crates/pgt_workspace_macros/src/lib.rs | 82 +++++++++++++++++++ 7 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 crates/pgt_workspace_macros/Cargo.toml create mode 100644 crates/pgt_workspace_macros/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4da985a3..e9b54a10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3104,6 +3104,7 @@ dependencies = [ "pgt_suppressions", "pgt_text_size", "pgt_typecheck", + "pgt_workspace_macros", "rustc-hash 2.1.0", "schemars", "serde", @@ -3118,6 +3119,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" @@ -3337,9 +3347,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..218f2b4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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_workspace/Cargo.toml b/crates/pgt_workspace/Cargo.toml index f535e505..7850eec9 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/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 f7ace3c2..c01e6abb 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}; @@ -407,6 +408,7 @@ impl Workspace for WorkspaceServer { }) } + #[ignored_path(path = ¶ms.path)] fn pull_diagnostics( &self, params: PullDiagnosticsParams, diff --git a/crates/pgt_workspace_macros/Cargo.toml b/crates/pgt_workspace_macros/Cargo.toml new file mode 100644 index 00000000..fcf60973 --- /dev/null +++ b/crates/pgt_workspace_macros/Cargo.toml @@ -0,0 +1,21 @@ +[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] +syn = { workspace = true } +proc-macro2 = { version = "1.0.95"} +quote = {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..3e1e3be2 --- /dev/null +++ b/crates/pgt_workspace_macros/src/lib.rs @@ -0,0 +1,82 @@ +use std::ops::Deref; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{TypePath, 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 = match input.parse() { + Ok(it) => it, + Err(_) => return Err(syn::Error::new_spanned(arg_name, "This is wrong brotha")), + }; + + Ok(Self { path }) + } +} + +#[proc_macro_attribute] +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 path = ignored_path.path; + let vis = &input_fn.vis; + let sig = &input_fn.sig; + let block = &input_fn.block; + let attrs = &input_fn.attrs; + + // get T in Result + 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(t) = type_args.args.first() { + if let syn::GenericArgument::Type(t) = t { + 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(#path) { + return Ok(#ident::default()); + }; + #block + } + }); + } + } + }; + }; + }; + }; + }; + }; + }; + + return TokenStream::from(quote! { + #(#attrs)* + #vis #sig { + if self.is_ignored(#path) { + return Default::default(); + } + #block + } + }); +} From d4cf1ba902127a34d9fb0bc962eca4024dbede15 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Jul 2025 09:54:29 +0200 Subject: [PATCH 02/11] reset to main --- crates/pgt_workspace/Cargo.toml | 5 +- .../pgt_workspace/src/features/diagnostics.rs | 2 +- crates/pgt_workspace/src/workspace/server.rs | 96 +++--- .../src/workspace/server.tests.rs | 154 +++++++++ .../workspace/server/connection_manager.rs | 50 ++- .../src/workspace/server/document.rs | 310 +++++++++++++++++- .../src/workspace/server/function_utils.rs | 57 ++++ .../src/workspace/server/pg_query.rs | 198 ++++++++++- .../src/workspace/server/sql_function.rs | 60 +--- .../src/workspace/server/tree_sitter.rs | 30 +- 10 files changed, 793 insertions(+), 169 deletions(-) create mode 100644 crates/pgt_workspace/src/workspace/server.tests.rs create mode 100644 crates/pgt_workspace/src/workspace/server/function_utils.rs diff --git a/crates/pgt_workspace/Cargo.toml b/crates/pgt_workspace/Cargo.toml index 7850eec9..3ef4936b 100644 --- a/crates/pgt_workspace/Cargo.toml +++ b/crates/pgt_workspace/Cargo.toml @@ -32,7 +32,6 @@ 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"] } @@ -63,7 +62,9 @@ schema = [ ] [dev-dependencies] -tempfile = "3.15.0" +pgt_test_utils = { workspace = true } +sqlx = { workspace = true } +tempfile = "3.15.0" [lib] doctest = false diff --git a/crates/pgt_workspace/src/features/diagnostics.rs b/crates/pgt_workspace/src/features/diagnostics.rs index a697641e..ff60e142 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, Default)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] #[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 c01e6abb..e6456afc 100644 --- a/crates/pgt_workspace/src/workspace/server.rs +++ b/crates/pgt_workspace/src/workspace/server.rs @@ -10,18 +10,17 @@ use analyser::AnalyserVisitorBuilder; use async_helper::run_async; use connection_manager::ConnectionManager; use document::{ - AsyncDiagnosticsMapper, CursorPositionFilter, DefaultMapper, Document, ExecuteStatementMapper, - SyncDiagnosticsMapper, + CursorPositionFilter, DefaultMapper, Document, ExecuteStatementMapper, + TypecheckDiagnosticsMapper, }; use futures::{StreamExt, stream}; use pgt_analyse::{AnalyserOptions, AnalysisFilter}; -use pgt_analyser::{Analyser, AnalyserConfig, AnalyserContext}; +use pgt_analyser::{Analyser, AnalyserConfig, AnalyserParams}; use pgt_diagnostics::{ Diagnostic, DiagnosticExt, Error, Severity, serde::Diagnostic as SDiagnostic, }; 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}; @@ -38,6 +37,7 @@ use crate::{ diagnostics::{PullDiagnosticsParams, PullDiagnosticsResult}, }, settings::{WorkspaceSettings, WorkspaceSettingsHandle, WorkspaceSettingsHandleMut}, + workspace::AnalyserDiagnosticsMapper, }; use super::{ @@ -54,6 +54,7 @@ mod async_helper; mod connection_key; mod connection_manager; pub(crate) mod document; +mod function_utils; mod migration; mod pg_query; mod schema_cache_manager; @@ -408,7 +409,6 @@ impl Workspace for WorkspaceServer { }) } - #[ignored_path(path = ¶ms.path)] fn pull_diagnostics( &self, params: PullDiagnosticsParams, @@ -445,7 +445,7 @@ impl Workspace for WorkspaceServer { if let Some(pool) = self.get_current_connection() { let path_clone = params.path.clone(); let schema_cache = self.schema_cache.load(pool.clone())?; - let input = doc.iter(AsyncDiagnosticsMapper).collect::>(); + let input = doc.iter(TypecheckDiagnosticsMapper).collect::>(); // sorry for the ugly code :( let async_results = run_async(async move { stream::iter(input) @@ -528,47 +528,49 @@ impl Workspace for WorkspaceServer { filter, }); + let path = params.path.as_path().display().to_string(); + + let schema_cache = self + .get_current_connection() + .and_then(|pool| self.schema_cache.load(pool.clone()).ok()); + + let mut analysable_stmts = vec![]; + for (stmt_root, diagnostic) in doc.iter(AnalyserDiagnosticsMapper) { + if let Some(node) = stmt_root { + analysable_stmts.push(node); + } + if let Some(diag) = diagnostic { + diagnostics.push(SDiagnostic::new( + diag.with_file_path(path.clone()) + .with_severity(Severity::Error), + )); + } + } + diagnostics.extend( - doc.iter(SyncDiagnosticsMapper) - .flat_map(|(_id, range, ast, diag)| { - let mut errors: Vec = vec![]; - - if let Some(diag) = diag { - errors.push(diag.into()); - } - - if let Some(ast) = ast { - errors.extend( - analyser - .run(AnalyserContext { root: &ast }) - .into_iter() - .map(Error::from) - .collect::>(), - ); - } - - errors - .into_iter() - .map(|d| { - let severity = d - .category() - .filter(|category| category.name().starts_with("lint/")) - .map_or_else( - || d.severity(), - |category| { - settings - .get_severity_from_rule_code(category) - .unwrap_or(Severity::Warning) - }, - ); - - SDiagnostic::new( - d.with_file_path(params.path.as_path().display().to_string()) - .with_file_span(range) - .with_severity(severity), - ) + analyser + .run(AnalyserParams { + stmts: analysable_stmts, + schema_cache: schema_cache.as_deref(), + }) + .into_iter() + .map(Error::from) + .map(|d| { + let severity = d + .category() + .map(|category| { + settings + .get_severity_from_rule_code(category) + .unwrap_or(Severity::Warning) }) - .collect::>() + .unwrap(); + + let span = d.location().span; + SDiagnostic::new( + d.with_file_path(path.clone()) + .with_file_span(span) + .with_severity(severity), + ) }), ); @@ -653,3 +655,7 @@ impl Workspace for WorkspaceServer { fn is_dir(path: &Path) -> bool { path.is_dir() || (path.is_symlink() && fs::read_link(path).is_ok_and(|path| path.is_dir())) } + +#[cfg(test)] +#[path = "server.tests.rs"] +mod tests; diff --git a/crates/pgt_workspace/src/workspace/server.tests.rs b/crates/pgt_workspace/src/workspace/server.tests.rs new file mode 100644 index 00000000..c3fbf723 --- /dev/null +++ b/crates/pgt_workspace/src/workspace/server.tests.rs @@ -0,0 +1,154 @@ +use biome_deserialize::Merge; +use pgt_analyse::RuleCategories; +use pgt_configuration::{PartialConfiguration, database::PartialDatabaseConfiguration}; +use pgt_diagnostics::Diagnostic; +use pgt_fs::PgTPath; +use pgt_text_size::TextRange; +use sqlx::PgPool; + +use crate::{ + Workspace, WorkspaceError, + workspace::{ + OpenFileParams, RegisterProjectFolderParams, UpdateSettingsParams, server::WorkspaceServer, + }, +}; + +fn get_test_workspace( + partial_config: Option, +) -> Result { + let workspace = WorkspaceServer::new(); + + workspace.register_project_folder(RegisterProjectFolderParams { + path: None, + set_as_current_workspace: true, + })?; + + workspace.update_settings(UpdateSettingsParams { + configuration: partial_config.unwrap_or(PartialConfiguration::init()), + gitignore_matches: vec![], + vcs_base_path: None, + workspace_directory: None, + })?; + + Ok(workspace) +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_diagnostics(test_db: PgPool) { + let mut conf = PartialConfiguration::init(); + conf.merge_with(PartialConfiguration { + db: Some(PartialDatabaseConfiguration { + database: Some( + test_db + .connect_options() + .get_database() + .unwrap() + .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#" + create table users ( + id serial primary key, + name text not null + ); + + drop table non_existing_table; + + select 1; + "#; + + workspace + .open_file(OpenFileParams { + path: path.clone(), + content: content.into(), + version: 1, + }) + .expect("Unable to open test file"); + + let diagnostics = workspace + .pull_diagnostics(crate::workspace::PullDiagnosticsParams { + path: path.clone(), + categories: RuleCategories::all(), + max_diagnostics: 100, + only: vec![], + skip: vec![], + }) + .expect("Unable to pull diagnostics") + .diagnostics; + + assert_eq!(diagnostics.len(), 1, "Expected one diagnostic"); + + let diagnostic = &diagnostics[0]; + + assert_eq!( + diagnostic.category().map(|c| c.name()), + Some("lint/safety/banDropTable") + ); + + assert_eq!( + diagnostic.location().span, + Some(TextRange::new(106.into(), 136.into())) + ); +} + +#[sqlx::test(migrator = "pgt_test_utils::MIGRATIONS")] +async fn test_syntax_error(test_db: PgPool) { + let mut conf = PartialConfiguration::init(); + conf.merge_with(PartialConfiguration { + db: Some(PartialDatabaseConfiguration { + database: Some( + test_db + .connect_options() + .get_database() + .unwrap() + .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; + "#; + + workspace + .open_file(OpenFileParams { + path: path.clone(), + content: content.into(), + version: 1, + }) + .expect("Unable to open test file"); + + let diagnostics = workspace + .pull_diagnostics(crate::workspace::PullDiagnosticsParams { + path: path.clone(), + categories: RuleCategories::all(), + max_diagnostics: 100, + only: vec![], + skip: vec![], + }) + .expect("Unable to pull diagnostics") + .diagnostics; + + assert_eq!(diagnostics.len(), 1, "Expected one diagnostic"); + + let diagnostic = &diagnostics[0]; + + assert_eq!(diagnostic.category().map(|c| c.name()), Some("syntax")); + + assert_eq!( + diagnostic.location().span, + Some(TextRange::new(7.into(), 15.into())) + ); +} diff --git a/crates/pgt_workspace/src/workspace/server/connection_manager.rs b/crates/pgt_workspace/src/workspace/server/connection_manager.rs index 8955b378..145b6fa0 100644 --- a/crates/pgt_workspace/src/workspace/server/connection_manager.rs +++ b/crates/pgt_workspace/src/workspace/server/connection_manager.rs @@ -34,23 +34,19 @@ impl ConnectionManager { pub(crate) fn get_pool(&self, settings: &DatabaseSettings) -> Option { let key = ConnectionKey::from(settings); - // Cleanup idle connections first - self.cleanup_idle_pools(&key); - if !settings.enable_connection { tracing::info!("Database connection disabled."); return None; } - // Try read lock first for cache hit - if let Ok(pools) = self.pools.read() { - if let Some(cached_pool) = pools.get(&key) { - // Can't update last_accessed with read lock, but that's okay for occasional misses - return Some(cached_pool.pool.clone()); + { + if let Ok(pools) = self.pools.read() { + if let Some(cached_pool) = pools.get(&key) { + return Some(cached_pool.pool.clone()); + } } } - // Cache miss or need to update timestamp - use write lock let mut pools = self.pools.write().unwrap(); // Double-check after acquiring write lock @@ -59,6 +55,21 @@ impl ConnectionManager { return Some(cached_pool.pool.clone()); } + // Clean up idle connections before creating new ones to avoid unbounded growth + let now = Instant::now(); + pools.retain(|k, cached_pool| { + let idle_duration = now.duration_since(cached_pool.last_accessed); + if idle_duration > cached_pool.idle_timeout && k != &key { + tracing::debug!( + "Removing idle database connection (idle for {:?})", + idle_duration + ); + false + } else { + true + } + }); + // Create a new pool let config = PgConnectOptions::new() .host(&settings.host) @@ -85,25 +96,4 @@ impl ConnectionManager { Some(pool) } - - /// Remove pools that haven't been accessed for longer than the idle timeout - fn cleanup_idle_pools(&self, ignore_key: &ConnectionKey) { - let now = Instant::now(); - - let mut pools = self.pools.write().unwrap(); - - // Use retain to keep only non-idle connections - pools.retain(|key, cached_pool| { - let idle_duration = now.duration_since(cached_pool.last_accessed); - if idle_duration > cached_pool.idle_timeout && key != ignore_key { - tracing::debug!( - "Removing idle database connection (idle for {:?})", - idle_duration - ); - false - } else { - true - } - }); - } } diff --git a/crates/pgt_workspace/src/workspace/server/document.rs b/crates/pgt_workspace/src/workspace/server/document.rs index f8ab639d..c9f880ec 100644 --- a/crates/pgt_workspace/src/workspace/server/document.rs +++ b/crates/pgt_workspace/src/workspace/server/document.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use pgt_analyser::AnalysableStatement; use pgt_diagnostics::{Diagnostic, DiagnosticExt, serde::Diagnostic as SDiagnostic}; use pgt_query_ext::diagnostics::SyntaxDiagnostic; use pgt_suppressions::Suppressions; @@ -208,8 +209,8 @@ impl<'a> StatementMapper<'a> for ExecuteStatementMapper { } } -pub struct AsyncDiagnosticsMapper; -impl<'a> StatementMapper<'a> for AsyncDiagnosticsMapper { +pub struct TypecheckDiagnosticsMapper; +impl<'a> StatementMapper<'a> for TypecheckDiagnosticsMapper { type Output = ( StatementId, TextRange, @@ -240,24 +241,31 @@ impl<'a> StatementMapper<'a> for AsyncDiagnosticsMapper { } } -pub struct SyncDiagnosticsMapper; -impl<'a> StatementMapper<'a> for SyncDiagnosticsMapper { - type Output = ( - StatementId, - TextRange, - Option, - Option, - ); +pub struct AnalyserDiagnosticsMapper; +impl<'a> StatementMapper<'a> for AnalyserDiagnosticsMapper { + type Output = (Option, Option); fn map(&self, parser: &'a Document, id: StatementId, range: TextRange) -> Self::Output { - let ast_result = parser.ast_db.get_or_cache_ast(&id); - - let (ast_option, diagnostics) = match &*ast_result { - Ok(node) => (Some(node.clone()), None), - Err(diag) => (None, Some(diag.clone())), + let maybe_node = parser.ast_db.get_or_cache_ast(&id); + + let (ast_option, diagnostics) = match &*maybe_node { + Ok(node) => { + let plpgsql_result = parser.ast_db.get_or_cache_plpgsql_parse(&id); + if let Some(Err(diag)) = plpgsql_result { + // offset the pgpsql diagnostic from the parent statement start + let span = diag.location().span.map(|sp| sp + range.start()); + (Some(node.clone()), Some(diag.span(span.unwrap_or(range)))) + } else { + (Some(node.clone()), None) + } + } + Err(diag) => (None, Some(diag.clone().span(range))), }; - (id.clone(), range, ast_option, diagnostics) + ( + ast_option.map(|root| AnalysableStatement { range, root }), + diagnostics, + ) } } @@ -379,4 +387,274 @@ mod tests { assert_eq!(stmts.len(), 2); assert_eq!(stmts[1].2, "select $1 + $2;"); } + + #[test] + fn test_sync_diagnostics_mapper_plpgsql_syntax_error() { + let input = " +CREATE FUNCTION test_func() + RETURNS void + LANGUAGE plpgsql + AS $$ +BEGIN + -- syntax error: missing semicolon and typo + DECLAR x integer + x := 10; +END; +$$;"; + + let d = Document::new(input.to_string(), 1); + let results = d.iter(AnalyserDiagnosticsMapper).collect::>(); + + assert_eq!(results.len(), 1); + let (ast, diagnostic) = &results[0]; + + // Should have parsed the CREATE FUNCTION statement + assert!(ast.is_some()); + + // Should have a PL/pgSQL syntax error + assert!(diagnostic.is_some()); + assert_eq!( + format!("{:?}", diagnostic.as_ref().unwrap().message), + "Invalid statement: syntax error at or near \"DECLAR\"" + ); + } + + #[test] + fn test_sync_diagnostics_mapper_plpgsql_valid() { + let input = " +CREATE FUNCTION valid_func() + RETURNS integer + LANGUAGE plpgsql + AS $$ +DECLARE + x integer := 5; +BEGIN + RETURN x * 2; +END; +$$;"; + + let d = Document::new(input.to_string(), 1); + let results = d.iter(AnalyserDiagnosticsMapper).collect::>(); + + assert_eq!(results.len(), 1); + let (ast, diagnostic) = &results[0]; + + // Should have parsed the CREATE FUNCTION statement + assert!(ast.is_some()); + + // Should NOT have any PL/pgSQL syntax errors + assert!(diagnostic.is_none()); + } + + #[test] + fn test_sync_diagnostics_mapper_plpgsql_caching() { + let input = " +CREATE FUNCTION cached_func() + RETURNS void + LANGUAGE plpgsql + AS $$ +BEGIN + RAISE NOTICE 'Testing cache'; +END; +$$;"; + + let d = Document::new(input.to_string(), 1); + + let results1 = d.iter(AnalyserDiagnosticsMapper).collect::>(); + assert_eq!(results1.len(), 1); + assert!(results1[0].0.is_some()); + assert!(results1[0].1.is_none()); + + let results2 = d.iter(AnalyserDiagnosticsMapper).collect::>(); + assert_eq!(results2.len(), 1); + assert!(results2[0].0.is_some()); + assert!(results2[0].1.is_none()); + } + + #[test] + fn test_default_mapper() { + let input = "SELECT 1; INSERT INTO users VALUES (1);"; + let d = Document::new(input.to_string(), 1); + + let results = d.iter(DefaultMapper).collect::>(); + assert_eq!(results.len(), 2); + + assert_eq!(results[0].2, "SELECT 1;"); + assert_eq!(results[1].2, "INSERT INTO users VALUES (1);"); + + assert_eq!(results[0].1.start(), 0.into()); + assert_eq!(results[0].1.end(), 9.into()); + assert_eq!(results[1].1.start(), 10.into()); + assert_eq!(results[1].1.end(), 39.into()); + } + + #[test] + fn test_execute_statement_mapper() { + let input = "SELECT 1; INVALID SYNTAX HERE;"; + let d = Document::new(input.to_string(), 1); + + let results = d.iter(ExecuteStatementMapper).collect::>(); + assert_eq!(results.len(), 2); + + // First statement should parse successfully + assert_eq!(results[0].2, "SELECT 1;"); + assert!(results[0].3.is_some()); + + // Second statement should fail to parse + assert_eq!(results[1].2, "INVALID SYNTAX HERE;"); + assert!(results[1].3.is_none()); + } + + #[test] + fn test_async_diagnostics_mapper() { + let input = " +CREATE FUNCTION test_fn() RETURNS integer AS $$ +BEGIN + RETURN 42; +END; +$$ LANGUAGE plpgsql;"; + + let d = Document::new(input.to_string(), 1); + let results = d.iter(TypecheckDiagnosticsMapper).collect::>(); + + assert_eq!(results.len(), 1); + let (_id, _range, ast, cst, sql_fn_sig) = &results[0]; + + // Should have both AST and CST + assert!(ast.is_some()); + assert_eq!(cst.root_node().kind(), "program"); + + // Should not have SQL function signature for top-level statement + assert!(sql_fn_sig.is_none()); + } + + #[test] + fn test_async_diagnostics_mapper_with_sql_function_body() { + let input = + "CREATE FUNCTION add(a int, b int) RETURNS int AS 'SELECT $1 + $2;' LANGUAGE sql;"; + let d = Document::new(input.to_string(), 1); + + let results = d.iter(TypecheckDiagnosticsMapper).collect::>(); + assert_eq!(results.len(), 2); + + // Check the function body + let (_id, _range, ast, _cst, sql_fn_sig) = &results[1]; + assert_eq!(_id.content(), "SELECT $1 + $2;"); + assert!(ast.is_some()); + assert!(sql_fn_sig.is_some()); + + let sig = sql_fn_sig.as_ref().unwrap(); + assert_eq!(sig.name, "add"); + assert_eq!(sig.args.len(), 2); + assert_eq!(sig.args[0].name, Some("a".to_string())); + assert_eq!(sig.args[1].name, Some("b".to_string())); + } + + #[test] + fn test_get_completions_mapper() { + let input = "SELECT * FROM users;"; + let d = Document::new(input.to_string(), 1); + + let results = d.iter(GetCompletionsMapper).collect::>(); + assert_eq!(results.len(), 1); + + let (_id, _range, content, tree) = &results[0]; + assert_eq!(content, "SELECT * FROM users;"); + assert_eq!(tree.root_node().kind(), "program"); + } + + #[test] + fn test_get_completions_filter() { + let input = "SELECT * FROM users; INSERT INTO"; + let d = Document::new(input.to_string(), 1); + + // Test cursor at end of first statement (terminated with semicolon) + let filter1 = GetCompletionsFilter { + cursor_position: 20.into(), + }; + let results1 = d + .iter_with_filter(DefaultMapper, filter1) + .collect::>(); + assert_eq!(results1.len(), 0); // No completions after semicolon + + // Test cursor at end of second statement (not terminated) + let filter2 = GetCompletionsFilter { + cursor_position: 32.into(), + }; + let results2 = d + .iter_with_filter(DefaultMapper, filter2) + .collect::>(); + assert_eq!(results2.len(), 1); + assert_eq!(results2[0].2, "INSERT INTO"); + } + + #[test] + fn test_cursor_position_filter() { + let input = "SELECT 1; INSERT INTO users VALUES (1);"; + let d = Document::new(input.to_string(), 1); + + // Cursor in first statement + let filter1 = CursorPositionFilter::new(5.into()); + let results1 = d + .iter_with_filter(DefaultMapper, filter1) + .collect::>(); + assert_eq!(results1.len(), 1); + assert_eq!(results1[0].2, "SELECT 1;"); + + // Cursor in second statement + let filter2 = CursorPositionFilter::new(25.into()); + let results2 = d + .iter_with_filter(DefaultMapper, filter2) + .collect::>(); + assert_eq!(results2.len(), 1); + assert_eq!(results2[0].2, "INSERT INTO users VALUES (1);"); + } + + #[test] + fn test_id_filter() { + let input = "SELECT 1; SELECT 2;"; + let d = Document::new(input.to_string(), 1); + + // Get all statements first to get their IDs + let all_results = d.iter(DefaultMapper).collect::>(); + assert_eq!(all_results.len(), 2); + + // Filter by first statement ID + let filter = IdFilter::new(all_results[0].0.clone()); + let results = d + .iter_with_filter(DefaultMapper, filter) + .collect::>(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].2, "SELECT 1;"); + } + + #[test] + fn test_no_filter() { + let input = "SELECT 1; SELECT 2; SELECT 3;"; + let d = Document::new(input.to_string(), 1); + + let results = d + .iter_with_filter(DefaultMapper, NoFilter) + .collect::>(); + assert_eq!(results.len(), 3); + } + + #[test] + fn test_find_method() { + let input = "SELECT 1; SELECT 2;"; + let d = Document::new(input.to_string(), 1); + + // Get all statements to get their IDs + let all_results = d.iter(DefaultMapper).collect::>(); + + // Find specific statement + let result = d.find(all_results[1].0.clone(), DefaultMapper); + assert!(result.is_some()); + assert_eq!(result.unwrap().2, "SELECT 2;"); + + // Try to find non-existent statement + let fake_id = StatementId::new("SELECT 3;"); + let result = d.find(fake_id, DefaultMapper); + assert!(result.is_none()); + } } diff --git a/crates/pgt_workspace/src/workspace/server/function_utils.rs b/crates/pgt_workspace/src/workspace/server/function_utils.rs new file mode 100644 index 00000000..cf02ceb1 --- /dev/null +++ b/crates/pgt_workspace/src/workspace/server/function_utils.rs @@ -0,0 +1,57 @@ +/// Helper function to find a specific option value from function options +pub fn find_option_value( + create_fn: &pgt_query_ext::protobuf::CreateFunctionStmt, + option_name: &str, +) -> Option { + create_fn + .options + .iter() + .filter_map(|opt_wrapper| opt_wrapper.node.as_ref()) + .find_map(|opt| { + if let pgt_query_ext::NodeEnum::DefElem(def_elem) = opt { + if def_elem.defname == option_name { + def_elem + .arg + .iter() + .filter_map(|arg_wrapper| arg_wrapper.node.as_ref()) + .find_map(|arg| { + if let pgt_query_ext::NodeEnum::String(s) = arg { + Some(s.sval.clone()) + } else if let pgt_query_ext::NodeEnum::List(l) = arg { + l.items.iter().find_map(|item_wrapper| { + if let Some(pgt_query_ext::NodeEnum::String(s)) = + item_wrapper.node.as_ref() + { + Some(s.sval.clone()) + } else { + None + } + }) + } else { + None + } + }) + } else { + None + } + } else { + None + } + }) +} + +pub fn parse_name(nodes: &[pgt_query_ext::protobuf::Node]) -> Option<(Option, String)> { + let names = nodes + .iter() + .map(|n| match &n.node { + Some(pgt_query_ext::NodeEnum::String(s)) => Some(s.sval.clone()), + _ => None, + }) + .collect::>(); + + match names.as_slice() { + [Some(schema), Some(name)] => Some((Some(schema.clone()), name.clone())), + [Some(name)] => Some((None, name.clone())), + _ => None, + } +} diff --git a/crates/pgt_workspace/src/workspace/server/pg_query.rs b/crates/pgt_workspace/src/workspace/server/pg_query.rs index 6f1fa2c1..ba471dfa 100644 --- a/crates/pgt_workspace/src/workspace/server/pg_query.rs +++ b/crates/pgt_workspace/src/workspace/server/pg_query.rs @@ -3,19 +3,25 @@ use std::sync::{Arc, Mutex}; use lru::LruCache; use pgt_query_ext::diagnostics::*; +use pgt_text_size::TextRange; +use super::function_utils::find_option_value; use super::statement_identifier::StatementId; const DEFAULT_CACHE_SIZE: usize = 1000; pub struct PgQueryStore { - db: Mutex>>>, + ast_db: Mutex>>>, + plpgsql_db: Mutex>>, } impl PgQueryStore { pub fn new() -> PgQueryStore { PgQueryStore { - db: Mutex::new(LruCache::new( + ast_db: Mutex::new(LruCache::new( + NonZeroUsize::new(DEFAULT_CACHE_SIZE).unwrap(), + )), + plpgsql_db: Mutex::new(LruCache::new( NonZeroUsize::new(DEFAULT_CACHE_SIZE).unwrap(), )), } @@ -25,7 +31,7 @@ impl PgQueryStore { &self, statement: &StatementId, ) -> Arc> { - let mut cache = self.db.lock().unwrap(); + let mut cache = self.ast_db.lock().unwrap(); if let Some(existing) = cache.get(statement) { return existing.clone(); @@ -35,4 +41,190 @@ impl PgQueryStore { cache.put(statement.clone(), r.clone()); r } + + pub fn get_or_cache_plpgsql_parse( + &self, + statement: &StatementId, + ) -> Option> { + let ast = self.get_or_cache_ast(statement); + + let create_fn = match ast.as_ref() { + Ok(pgt_query_ext::NodeEnum::CreateFunctionStmt(node)) => node, + _ => return None, + }; + + let language = find_option_value(create_fn, "language")?; + + if language != "plpgsql" { + return None; + } + + let mut cache = self.plpgsql_db.lock().unwrap(); + + if let Some(existing) = cache.get(statement) { + return Some(existing.clone()); + } + + let sql_body = find_option_value(create_fn, "as")?; + + let start = statement.content().find(&sql_body)?; + let end = start + sql_body.len(); + + let range = TextRange::new(start.try_into().unwrap(), end.try_into().unwrap()); + + let r = pgt_query_ext::parse_plpgsql(statement.content()) + .map_err(|err| SyntaxDiagnostic::new(err.to_string(), Some(range))); + cache.put(statement.clone(), r.clone()); + + Some(r) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_plpgsql_syntax_error() { + let input = " +create function test_organisation_id () + returns setof text + language plpgsql + security invoker + as $$ + -- syntax error here + delare + v_organisation_id uuid; +begin + return next is(private.organisation_id(), v_organisation_id, 'should return organisation_id of token'); +end +$$; +"; + + let store = PgQueryStore::new(); + + let res = store.get_or_cache_plpgsql_parse(&StatementId::new(input)); + + assert!(matches!(res, Some(Err(_)))); + } + + #[test] + fn test_plpgsql_valid() { + let input = " +CREATE FUNCTION test_function() + RETURNS integer + LANGUAGE plpgsql + AS $$ +DECLARE + counter integer := 0; +BEGIN + counter := counter + 1; + RETURN counter; +END; +$$; +"; + + let store = PgQueryStore::new(); + + let res = store.get_or_cache_plpgsql_parse(&StatementId::new(input)); + + assert!(matches!(res, Some(Ok(_)))); + } + + #[test] + fn test_non_plpgsql_function() { + let input = " +CREATE FUNCTION add_numbers(a integer, b integer) + RETURNS integer + LANGUAGE sql + AS $$ + SELECT a + b; + $$; +"; + + let store = PgQueryStore::new(); + + let res = store.get_or_cache_plpgsql_parse(&StatementId::new(input)); + + assert!(res.is_none()); + } + + #[test] + fn test_non_function_statement() { + let input = "SELECT * FROM users WHERE id = 1;"; + + let store = PgQueryStore::new(); + + let res = store.get_or_cache_plpgsql_parse(&StatementId::new(input)); + + assert!(res.is_none()); + } + + #[test] + fn test_cache_behavior() { + let input = " +CREATE FUNCTION cached_function() + RETURNS void + LANGUAGE plpgsql + AS $$ +BEGIN + RAISE NOTICE 'Hello from cache test'; +END; +$$; +"; + + let store = PgQueryStore::new(); + let statement_id = StatementId::new(input); + + // First call should parse + let res1 = store.get_or_cache_plpgsql_parse(&statement_id); + assert!(matches!(res1, Some(Ok(_)))); + + // Second call should return cached result + let res2 = store.get_or_cache_plpgsql_parse(&statement_id); + assert!(matches!(res2, Some(Ok(_)))); + } + + #[test] + fn test_plpgsql_with_complex_body() { + let input = " +CREATE FUNCTION complex_function(p_id integer) + RETURNS TABLE(id integer, name text, status boolean) + LANGUAGE plpgsql + AS $$ +DECLARE + v_count integer; + v_status boolean := true; +BEGIN + SELECT COUNT(*) INTO v_count FROM users WHERE user_id = p_id; + + IF v_count > 0 THEN + RETURN QUERY + SELECT u.id, u.name, v_status + FROM users u + WHERE u.user_id = p_id; + ELSE + RAISE EXCEPTION 'User not found'; + END IF; +END; +$$; +"; + + let store = PgQueryStore::new(); + + let res = store.get_or_cache_plpgsql_parse(&StatementId::new(input)); + + assert!(matches!(res, Some(Ok(_)))); + } + + #[test] + fn test_invalid_ast() { + let input = "CREATE FUNCTION invalid syntax here"; + + let store = PgQueryStore::new(); + + let res = store.get_or_cache_plpgsql_parse(&StatementId::new(input)); + + assert!(res.is_none()); + } } diff --git a/crates/pgt_workspace/src/workspace/server/sql_function.rs b/crates/pgt_workspace/src/workspace/server/sql_function.rs index bc2c6c3b..6161dda7 100644 --- a/crates/pgt_workspace/src/workspace/server/sql_function.rs +++ b/crates/pgt_workspace/src/workspace/server/sql_function.rs @@ -1,5 +1,7 @@ use pgt_text_size::TextRange; +use super::function_utils::{find_option_value, parse_name}; + #[derive(Debug, Clone)] pub struct ArgType { pub schema: Option, @@ -106,64 +108,6 @@ pub fn get_sql_fn_body(ast: &pgt_query_ext::NodeEnum, content: &str) -> Option Option { - create_fn - .options - .iter() - .filter_map(|opt_wrapper| opt_wrapper.node.as_ref()) - .find_map(|opt| { - if let pgt_query_ext::NodeEnum::DefElem(def_elem) = opt { - if def_elem.defname == option_name { - def_elem - .arg - .iter() - .filter_map(|arg_wrapper| arg_wrapper.node.as_ref()) - .find_map(|arg| { - if let pgt_query_ext::NodeEnum::String(s) = arg { - Some(s.sval.clone()) - } else if let pgt_query_ext::NodeEnum::List(l) = arg { - l.items.iter().find_map(|item_wrapper| { - if let Some(pgt_query_ext::NodeEnum::String(s)) = - item_wrapper.node.as_ref() - { - Some(s.sval.clone()) - } else { - None - } - }) - } else { - None - } - }) - } else { - None - } - } else { - None - } - }) -} - -fn parse_name(nodes: &[pgt_query_ext::protobuf::Node]) -> Option<(Option, String)> { - let names = nodes - .iter() - .map(|n| match &n.node { - Some(pgt_query_ext::NodeEnum::String(s)) => Some(s.sval.clone()), - _ => None, - }) - .collect::>(); - - match names.as_slice() { - [Some(schema), Some(name)] => Some((Some(schema.clone()), name.clone())), - [Some(name)] => Some((None, name.clone())), - _ => None, - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/pgt_workspace/src/workspace/server/tree_sitter.rs b/crates/pgt_workspace/src/workspace/server/tree_sitter.rs index b8f62b63..71411d27 100644 --- a/crates/pgt_workspace/src/workspace/server/tree_sitter.rs +++ b/crates/pgt_workspace/src/workspace/server/tree_sitter.rs @@ -28,27 +28,29 @@ impl TreeSitterStore { } pub fn get_or_cache_tree(&self, statement: &StatementId) -> Arc { - let mut cache = self.db.lock().expect("Failed to lock cache"); - - if let Some(existing) = cache.get(statement) { - return existing.clone(); + // First check cache + { + let mut cache = self.db.lock().unwrap(); + if let Some(existing) = cache.get(statement) { + return existing.clone(); + } } - // Cache miss - drop cache lock, parse, then re-acquire to insert - drop(cache); - - let mut parser = self.parser.lock().expect("Failed to lock parser"); + // Cache miss - parse outside of cache lock to avoid deadlock + let mut parser = self.parser.lock().unwrap(); let tree = Arc::new(parser.parse(statement.content(), None).unwrap()); drop(parser); - let mut cache = self.db.lock().expect("Failed to lock cache"); - - // Double-check after re-acquiring lock - if let Some(existing) = cache.get(statement) { - return existing.clone(); + // Insert into cache + { + let mut cache = self.db.lock().unwrap(); + // Double-check in case another thread inserted while we were parsing + if let Some(existing) = cache.get(statement) { + return existing.clone(); + } + cache.put(statement.clone(), tree.clone()); } - cache.put(statement.clone(), tree.clone()); tree } } From 6881824ada30bb1ac67304aaf045bed064303d89 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Jul 2025 10:03:54 +0200 Subject: [PATCH 03/11] so far --- Cargo.lock | 1 + Cargo.toml | 4 +-- crates/pgt_workspace/Cargo.toml | 1 + .../src/features/code_actions.rs | 4 +-- .../pgt_workspace/src/features/diagnostics.rs | 2 +- crates/pgt_workspace/src/workspace/server.rs | 14 ++++++-- crates/pgt_workspace_macros/Cargo.toml | 8 ++--- crates/pgt_workspace_macros/src/lib.rs | 35 +++++++++++++------ 8 files changed, 45 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e9b54a10..460cd6f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3102,6 +3102,7 @@ dependencies = [ "pgt_schema_cache", "pgt_statement_splitter", "pgt_suppressions", + "pgt_test_utils", "pgt_text_size", "pgt_typecheck", "pgt_workspace_macros", diff --git a/Cargo.toml b/Cargo.toml index 218f2b4d..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,7 +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_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_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..8c3bc153 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)] #[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..42f2ed6b 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 @@ -312,6 +315,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 +328,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 +371,7 @@ impl Workspace for WorkspaceServer { Ok(CodeActionsResult { actions }) } + #[ignored_path(path=¶ms.path)] fn execute_statement( &self, params: ExecuteStatementParams, @@ -409,6 +415,7 @@ impl Workspace for WorkspaceServer { }) } + #[ignored_path(path=¶ms.path)] fn pull_diagnostics( &self, params: PullDiagnosticsParams, @@ -607,6 +614,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_macros/Cargo.toml b/crates/pgt_workspace_macros/Cargo.toml index fcf60973..c192db04 100644 --- a/crates/pgt_workspace_macros/Cargo.toml +++ b/crates/pgt_workspace_macros/Cargo.toml @@ -14,8 +14,6 @@ version = "0.0.0" proc-macro = true [dependencies] -syn = { workspace = true } -proc-macro2 = { version = "1.0.95"} -quote = {workspace = true} - - +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 index 3e1e3be2..0f872b91 100644 --- a/crates/pgt_workspace_macros/src/lib.rs +++ b/crates/pgt_workspace_macros/src/lib.rs @@ -34,7 +34,8 @@ 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 path = ignored_path.path; + let macro_specified_path = ignored_path.path; + let vis = &input_fn.vis; let sig = &input_fn.sig; let block = &input_fn.block; @@ -51,15 +52,27 @@ pub fn ignored_path(args: TokenStream, input: TokenStream) -> TokenStream { 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(#path) { - return Ok(#ident::default()); - }; - #block - } - }); + if ident == "()" { + return TokenStream::from(quote! { + #(#attrs)* + #vis #sig { + if self.is_ignored(#macro_specified_path) { + return Ok(()); + }; + #block + } + }); + } else { + return TokenStream::from(quote! { + #(#attrs)* + #vis #sig { + if self.is_ignored(#macro_specified_path) { + return Ok(#ident::default()); + }; + #block + } + }); + } } } }; @@ -73,7 +86,7 @@ pub fn ignored_path(args: TokenStream, input: TokenStream) -> TokenStream { return TokenStream::from(quote! { #(#attrs)* #vis #sig { - if self.is_ignored(#path) { + if self.is_ignored(#macro_specified_path) { return Default::default(); } #block From 61b6266822ab39c50d4e5db640a44dd171fd83a6 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Jul 2025 10:06:55 +0200 Subject: [PATCH 04/11] maybe --- crates/pgt_workspace/src/workspace.rs | 1 + crates/pgt_workspace/src/workspace/tryout.rs | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 crates/pgt_workspace/src/workspace/tryout.rs diff --git a/crates/pgt_workspace/src/workspace.rs b/crates/pgt_workspace/src/workspace.rs index 9206b39d..4e10e2e7 100644 --- a/crates/pgt_workspace/src/workspace.rs +++ b/crates/pgt_workspace/src/workspace.rs @@ -22,6 +22,7 @@ use crate::{ mod client; mod server; +mod tryout; pub use server::StatementId; pub(crate) use server::document::*; diff --git a/crates/pgt_workspace/src/workspace/tryout.rs b/crates/pgt_workspace/src/workspace/tryout.rs new file mode 100644 index 00000000..cf63ec3c --- /dev/null +++ b/crates/pgt_workspace/src/workspace/tryout.rs @@ -0,0 +1,10 @@ +use pgt_workspace_macros::ignored_path; + +use crate::{features::code_actions::CodeActionsParams, workspace::server::WorkspaceServer}; + +impl WorkspaceServer { + #[ignored_path(path=¶ms.path)] + pub fn something(&self, params: CodeActionsParams) -> Result<(), String> { + Ok(()) + } +} From 2d8a6460339ba12317bf0b069a23ee5e2d8c6b77 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Jul 2025 10:47:43 +0200 Subject: [PATCH 05/11] mamma mia --- crates/pgt_diagnostics/src/serde.rs | 3 +- .../src/features/code_actions.rs | 2 +- crates/pgt_workspace/src/workspace.rs | 1 - crates/pgt_workspace/src/workspace/server.rs | 1 + .../src/workspace/server.tests.rs | 60 ++++++++++++++++++- crates/pgt_workspace/src/workspace/tryout.rs | 10 ---- crates/pgt_workspace_macros/src/lib.rs | 52 +++++++++------- 7 files changed, 90 insertions(+), 39 deletions(-) delete mode 100644 crates/pgt_workspace/src/workspace/tryout.rs 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/src/features/code_actions.rs b/crates/pgt_workspace/src/features/code_actions.rs index 8c3bc153..cd1706d3 100644 --- a/crates/pgt_workspace/src/features/code_actions.rs +++ b/crates/pgt_workspace/src/features/code_actions.rs @@ -57,7 +57,7 @@ pub struct ExecuteStatementParams { pub path: PgTPath, } -#[derive(Debug, serde::Serialize, serde::Deserialize, Default)] +#[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/workspace.rs b/crates/pgt_workspace/src/workspace.rs index 4e10e2e7..9206b39d 100644 --- a/crates/pgt_workspace/src/workspace.rs +++ b/crates/pgt_workspace/src/workspace.rs @@ -22,7 +22,6 @@ use crate::{ mod client; mod server; -mod tryout; pub use server::StatementId; pub(crate) use server::document::*; diff --git a/crates/pgt_workspace/src/workspace/server.rs b/crates/pgt_workspace/src/workspace/server.rs index 42f2ed6b..c6ed0827 100644 --- a/crates/pgt_workspace/src/workspace/server.rs +++ b/crates/pgt_workspace/src/workspace/server.rs @@ -294,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(); diff --git a/crates/pgt_workspace/src/workspace/server.tests.rs b/crates/pgt_workspace/src/workspace/server.tests.rs index c3fbf723..1e4e6d7c 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.len() == 0 + && 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_and(|res| res == ())); + + 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/src/workspace/tryout.rs b/crates/pgt_workspace/src/workspace/tryout.rs deleted file mode 100644 index cf63ec3c..00000000 --- a/crates/pgt_workspace/src/workspace/tryout.rs +++ /dev/null @@ -1,10 +0,0 @@ -use pgt_workspace_macros::ignored_path; - -use crate::{features::code_actions::CodeActionsParams, workspace::server::WorkspaceServer}; - -impl WorkspaceServer { - #[ignored_path(path=¶ms.path)] - pub fn something(&self, params: CodeActionsParams) -> Result<(), String> { - Ok(()) - } -} diff --git a/crates/pgt_workspace_macros/src/lib.rs b/crates/pgt_workspace_macros/src/lib.rs index 0f872b91..3ce0f792 100644 --- a/crates/pgt_workspace_macros/src/lib.rs +++ b/crates/pgt_workspace_macros/src/lib.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use proc_macro::TokenStream; use quote::quote; -use syn::{TypePath, parse_macro_input}; +use syn::{TypePath, TypeTuple, parse_macro_input}; struct IgnoredPath { path: syn::Expr, @@ -41,7 +41,8 @@ pub fn ignored_path(args: TokenStream, input: TokenStream) -> TokenStream { let block = &input_fn.block; let attrs = &input_fn.attrs; - // get T in Result + // 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() { @@ -49,30 +50,32 @@ pub fn ignored_path(args: TokenStream, input: TokenStream) -> TokenStream { if let syn::PathArguments::AngleBracketed(type_args) = &seg.arguments { if let Some(t) = type_args.args.first() { if let syn::GenericArgument::Type(t) = t { + if let syn::Type::Tuple(TypeTuple { elems, .. }) = t { + // case: Result<(), E> + if elems.len() == 0 { + 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; - if ident == "()" { - return TokenStream::from(quote! { - #(#attrs)* - #vis #sig { - if self.is_ignored(#macro_specified_path) { - return Ok(()); - }; - #block - } - }); - } else { - return TokenStream::from(quote! { - #(#attrs)* - #vis #sig { - if self.is_ignored(#macro_specified_path) { - return Ok(#ident::default()); - }; - #block - } - }); - } + return TokenStream::from(quote! { + #(#attrs)* + #vis #sig { + if self.is_ignored(#macro_specified_path) { + return Ok(#ident::default()); + }; + #block + } + }); } } }; @@ -83,6 +86,9 @@ pub fn ignored_path(args: TokenStream, input: TokenStream) -> TokenStream { }; }; + // case fn foo() -> T {} + // handles all other T's + // T needs to implement Default return TokenStream::from(quote! { #(#attrs)* #vis #sig { From 758041578070c9c35af51b33921b1e1ffb7a1398 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Jul 2025 10:57:05 +0200 Subject: [PATCH 06/11] commie --- crates/pgt_workspace_macros/src/lib.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/pgt_workspace_macros/src/lib.rs b/crates/pgt_workspace_macros/src/lib.rs index 3ce0f792..071d334a 100644 --- a/crates/pgt_workspace_macros/src/lib.rs +++ b/crates/pgt_workspace_macros/src/lib.rs @@ -30,6 +30,32 @@ impl syn::parse::Parse for IgnoredPath { } #[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. +/// +/// +/// Usage: +/// ``` +/// 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 +/// } +/// } +/// ``` +/// +/// This will work for any function where &self is in scope and that returns `Result`, `Result<(), E>`, or `T`, where `T: Default`. 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); From c26ded2a31e6f035010b8f9df4f04682baf077de Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Jul 2025 11:11:56 +0200 Subject: [PATCH 07/11] ok --- crates/pgt_workspace/src/workspace/server.tests.rs | 2 +- crates/pgt_workspace_macros/src/lib.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/pgt_workspace/src/workspace/server.tests.rs b/crates/pgt_workspace/src/workspace/server.tests.rs index 1e4e6d7c..5d1f18a2 100644 --- a/crates/pgt_workspace/src/workspace/server.tests.rs +++ b/crates/pgt_workspace/src/workspace/server.tests.rs @@ -186,7 +186,7 @@ async fn correctly_ignores_files() { }); assert!( - diagnostics_result.is_ok_and(|res| res.diagnostics.len() == 0 + diagnostics_result.is_ok_and(|res| res.diagnostics.is_empty() && res.errors == 0 && res.skipped_diagnostics == 0) ); diff --git a/crates/pgt_workspace_macros/src/lib.rs b/crates/pgt_workspace_macros/src/lib.rs index 071d334a..65c4ba96 100644 --- a/crates/pgt_workspace_macros/src/lib.rs +++ b/crates/pgt_workspace_macros/src/lib.rs @@ -78,7 +78,7 @@ pub fn ignored_path(args: TokenStream, input: TokenStream) -> TokenStream { if let syn::GenericArgument::Type(t) = t { if let syn::Type::Tuple(TypeTuple { elems, .. }) = t { // case: Result<(), E> - if elems.len() == 0 { + if elems.is_empty() { return TokenStream::from(quote! { #(#attrs)* #vis #sig { @@ -115,7 +115,7 @@ pub fn ignored_path(args: TokenStream, input: TokenStream) -> TokenStream { // case fn foo() -> T {} // handles all other T's // T needs to implement Default - return TokenStream::from(quote! { + TokenStream::from(quote! { #(#attrs)* #vis #sig { if self.is_ignored(#macro_specified_path) { @@ -123,5 +123,5 @@ pub fn ignored_path(args: TokenStream, input: TokenStream) -> TokenStream { } #block } - }); + }) } From 1d096575e5996e33bb214f2b28f59bc5a0f71e35 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Jul 2025 11:13:03 +0200 Subject: [PATCH 08/11] collapse check --- crates/pgt_workspace_macros/src/lib.rs | 56 +++++++++++++------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/crates/pgt_workspace_macros/src/lib.rs b/crates/pgt_workspace_macros/src/lib.rs index 65c4ba96..86243097 100644 --- a/crates/pgt_workspace_macros/src/lib.rs +++ b/crates/pgt_workspace_macros/src/lib.rs @@ -74,37 +74,35 @@ pub fn ignored_path(args: TokenStream, input: TokenStream) -> TokenStream { if let Some(seg) = path.segments.last() { if seg.ident == "Result" { if let syn::PathArguments::AngleBracketed(type_args) = &seg.arguments { - if let Some(t) = type_args.args.first() { - if let syn::GenericArgument::Type(t) = t { - 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 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 - } - }); - } + } + 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 + } + }); } - }; + } }; }; }; From 2a5a2085bd09f477581990da292c104bca0aa4da Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Jul 2025 11:14:21 +0200 Subject: [PATCH 09/11] k --- crates/pgt_workspace/src/workspace/server.tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/pgt_workspace/src/workspace/server.tests.rs b/crates/pgt_workspace/src/workspace/server.tests.rs index 5d1f18a2..0578f90f 100644 --- a/crates/pgt_workspace/src/workspace/server.tests.rs +++ b/crates/pgt_workspace/src/workspace/server.tests.rs @@ -194,7 +194,7 @@ async fn correctly_ignores_files() { let close_file_result = workspace.close_file(crate::workspace::CloseFileParams { path: path.clone() }); - assert!(close_file_result.is_ok_and(|res| res == ())); + assert!(close_file_result.is_ok()); let execute_statement_result = workspace.execute_statement(crate::workspace::ExecuteStatementParams { From 1ef55162257f3eb0ca07327f3fac4ca191325e68 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Jul 2025 11:19:41 +0200 Subject: [PATCH 10/11] no exec --- crates/pgt_workspace_macros/src/lib.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/pgt_workspace_macros/src/lib.rs b/crates/pgt_workspace_macros/src/lib.rs index 86243097..591b405e 100644 --- a/crates/pgt_workspace_macros/src/lib.rs +++ b/crates/pgt_workspace_macros/src/lib.rs @@ -33,9 +33,12 @@ impl syn::parse::Parse for IgnoredPath { /// 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: -/// ``` +/// ### Usage +/// +/// ```ignore /// impl WorkspaceServer { /// #[ignore_path(path=¶ms.path)] /// fn foo(&self, params: FooParams) -> Result { @@ -54,8 +57,6 @@ impl syn::parse::Parse for IgnoredPath { /// } /// } /// ``` -/// -/// This will work for any function where &self is in scope and that returns `Result`, `Result<(), E>`, or `T`, where `T: Default`. 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); From a3fe910e2de377172834cd6fea07169b63f031bd Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Jul 2025 11:21:36 +0200 Subject: [PATCH 11/11] not needed --- crates/pgt_workspace_macros/src/lib.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/pgt_workspace_macros/src/lib.rs b/crates/pgt_workspace_macros/src/lib.rs index 591b405e..d46f484d 100644 --- a/crates/pgt_workspace_macros/src/lib.rs +++ b/crates/pgt_workspace_macros/src/lib.rs @@ -20,10 +20,7 @@ impl syn::parse::Parse for IgnoredPath { } let _: syn::Token!(=) = input.parse()?; - let path: syn::Expr = match input.parse() { - Ok(it) => it, - Err(_) => return Err(syn::Error::new_spanned(arg_name, "This is wrong brotha")), - }; + let path: syn::Expr = input.parse()?; Ok(Self { path }) } @@ -34,7 +31,7 @@ impl syn::parse::Parse for IgnoredPath { /// 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``. +/// `path` needs to point at a `&PgTPath`. /// /// ### Usage ///