diff --git a/crates/pgls_diagnostics/src/context.rs b/crates/pgls_diagnostics/src/context.rs index 6cefb2d11..af85f2d00 100644 --- a/crates/pgls_diagnostics/src/context.rs +++ b/crates/pgls_diagnostics/src/context.rs @@ -458,6 +458,7 @@ mod internal { resource: match loc.resource { Some(Resource::Argv) => Some(Resource::Argv), Some(Resource::Memory) => Some(Resource::Memory), + Some(Resource::Database) => Some(Resource::Database), Some(Resource::File(file)) => { if let Some(Resource::File(path)) = &self.path { Some(Resource::File(path.as_ref())) @@ -468,6 +469,7 @@ mod internal { None => self.path.as_ref().map(Resource::as_deref), }, span: loc.span, + database_object: loc.database_object, source_code: loc.source_code, } } @@ -523,6 +525,7 @@ mod internal { Location { resource: loc.resource, span: self.span.or(loc.span), + database_object: loc.database_object, source_code: loc.source_code, } } diff --git a/crates/pgls_diagnostics/src/display.rs b/crates/pgls_diagnostics/src/display.rs index b19707f89..cacc459fa 100644 --- a/crates/pgls_diagnostics/src/display.rs +++ b/crates/pgls_diagnostics/src/display.rs @@ -139,6 +139,23 @@ impl fmt::Display for PrintHeader<'_, D> { fmt.write_str(" ")?; } + // Print the database object if present (e.g., "table public.contacts") + if let Some(db_obj) = location.database_object { + if let Some(obj_type) = db_obj.object_type { + fmt.write_markup(markup! { + {obj_type}" " + })?; + } + if let Some(schema) = db_obj.schema { + fmt.write_markup(markup! { + {schema}"." + })?; + } + fmt.write_markup(markup! { + {db_obj.name}" " + })?; + } + // Print the category of the diagnostic, with a hyperlink if // the category has an associated link if let Some(category) = diagnostic.category() { @@ -790,6 +807,7 @@ mod tests { visitor.record_frame(Location { resource: Some(Resource::File("other_path")), span: Some(TextRange::new(TextSize::from(8), TextSize::from(16))), + database_object: None, source_code: Some(SourceCode { text: "context location context", line_starts: None, diff --git a/crates/pgls_diagnostics/src/lib.rs b/crates/pgls_diagnostics/src/lib.rs index ffb9f5f58..add7e1449 100644 --- a/crates/pgls_diagnostics/src/lib.rs +++ b/crates/pgls_diagnostics/src/lib.rs @@ -36,7 +36,9 @@ pub use crate::display::{ }; pub use crate::display_github::PrintGitHubDiagnostic; pub use crate::error::{Error, Result}; -pub use crate::location::{LineIndex, LineIndexBuf, Location, Resource, SourceCode}; +pub use crate::location::{ + DatabaseObject, DatabaseObjectOwned, LineIndex, LineIndexBuf, Location, Resource, SourceCode, +}; use pgls_console::fmt::{Formatter, Termcolor}; use pgls_console::markup; use std::fmt::Write; diff --git a/crates/pgls_diagnostics/src/location.rs b/crates/pgls_diagnostics/src/location.rs index 40597e8ef..04a49af1e 100644 --- a/crates/pgls_diagnostics/src/location.rs +++ b/crates/pgls_diagnostics/src/location.rs @@ -4,6 +4,28 @@ use std::fmt::Debug; use std::ops::Range; use std::{borrow::Borrow, ops::Deref}; +/// Represents a database object (table, function, etc.) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DatabaseObject<'a> { + /// Optional schema name + pub schema: Option<&'a str>, + /// Object name (required) + pub name: &'a str, + /// Optional object type (e.g., "table", "function", "view") + pub object_type: Option<&'a str>, +} + +/// Owned version of DatabaseObject for use in diagnostic structs +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DatabaseObjectOwned { + /// Optional schema name + pub schema: Option, + /// Object name (required) + pub name: String, + /// Optional object type (e.g., "table", "function", "view") + pub object_type: Option, +} + /// Represents the location of a diagnostic in a resource. #[derive(Debug, Default, Clone, Copy)] pub struct Location<'a> { @@ -12,6 +34,8 @@ pub struct Location<'a> { /// An optional range of text within the resource associated with the /// diagnostic. pub span: Option, + /// An optional database object reference + pub database_object: Option>, /// The optional source code of the resource. pub source_code: Option>, } @@ -23,6 +47,7 @@ impl<'a> Location<'a> { resource: None, span: None, source_code: None, + database_object: None, } } } @@ -42,6 +67,8 @@ impl Eq for Location<'_> {} #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] pub enum Resource { + /// The diagnostic is related to the database and its schema. + Database, /// The diagnostic is related to the content of the command line arguments. Argv, /// The diagnostic is related to the content of a memory buffer. @@ -70,6 +97,7 @@ impl

Resource

{ P: Deref, { match self { + Resource::Database => Resource::Database, Resource::Argv => Resource::Argv, Resource::Memory => Resource::Memory, Resource::File(file) => Resource::File(file), @@ -81,6 +109,7 @@ impl Resource<&'_ str> { /// Converts a `Path<&str>` to `Path`. pub fn to_owned(self) -> Resource { match self { + Resource::Database => Resource::Database, Resource::Argv => Resource::Argv, Resource::Memory => Resource::Memory, Resource::File(file) => Resource::File(file.to_owned()), @@ -194,6 +223,7 @@ pub struct LocationBuilder<'a> { resource: Option>, span: Option, source_code: Option>, + database_object: Option>, } impl<'a> LocationBuilder<'a> { @@ -212,11 +242,17 @@ impl<'a> LocationBuilder<'a> { self } + pub fn database_object(mut self, database_object: &'a D) -> Self { + self.database_object = database_object.as_database_object(); + self + } + pub fn build(self) -> Location<'a> { Location { resource: self.resource, span: self.span, source_code: self.source_code, + database_object: self.database_object, } } } @@ -342,6 +378,39 @@ impl AsSourceCode for String { } } +/// Utility trait for types that can be converted into a database object reference +pub trait AsDatabaseObject { + fn as_database_object(&self) -> Option>; +} + +impl AsDatabaseObject for Option { + fn as_database_object(&self) -> Option> { + self.as_ref().and_then(T::as_database_object) + } +} + +impl AsDatabaseObject for &'_ T { + fn as_database_object(&self) -> Option> { + T::as_database_object(*self) + } +} + +impl<'a> AsDatabaseObject for DatabaseObject<'a> { + fn as_database_object(&self) -> Option> { + Some(*self) + } +} + +impl AsDatabaseObject for DatabaseObjectOwned { + fn as_database_object(&self) -> Option> { + Some(DatabaseObject { + schema: self.schema.as_deref(), + name: &self.name, + object_type: self.object_type.as_deref(), + }) + } +} + #[cfg(test)] mod tests { use pgls_text_size::TextSize; diff --git a/crates/pgls_diagnostics/src/serde.rs b/crates/pgls_diagnostics/src/serde.rs index 7e0e7a098..ab57d90fa 100644 --- a/crates/pgls_diagnostics/src/serde.rs +++ b/crates/pgls_diagnostics/src/serde.rs @@ -274,6 +274,7 @@ impl super::Advices for Advice { Advice::Frame(location) => visitor.record_frame(super::Location { resource: location.path.as_ref().map(super::Resource::as_deref), span: location.span, + database_object: None, source_code: location.source_code.as_deref().map(|text| SourceCode { text, line_starts: None, @@ -496,4 +497,31 @@ mod tests { // // assert_eq!(diag, expected); // } + + #[test] + fn test_database_object_location_macro() { + use crate::{DatabaseObjectOwned, Diagnostic}; + + #[derive(Debug, Diagnostic)] + #[diagnostic(severity = Error, category = "lint")] + struct TestDatabaseObjectDiagnostic { + #[location(database_object)] + db_object: DatabaseObjectOwned, + } + + let diag = TestDatabaseObjectDiagnostic { + db_object: DatabaseObjectOwned { + schema: Some("public".to_string()), + name: "contacts".to_string(), + object_type: Some("table".to_string()), + }, + }; + + let location = diag.location(); + assert!(location.database_object.is_some()); + let db_obj = location.database_object.unwrap(); + assert_eq!(db_obj.schema, Some("public")); + assert_eq!(db_obj.name, "contacts"); + assert_eq!(db_obj.object_type, Some("table")); + } } diff --git a/crates/pgls_diagnostics_macros/src/parse.rs b/crates/pgls_diagnostics_macros/src/parse.rs index 0e6e1b91e..f013a83ad 100644 --- a/crates/pgls_diagnostics_macros/src/parse.rs +++ b/crates/pgls_diagnostics_macros/src/parse.rs @@ -436,6 +436,7 @@ pub(crate) enum LocationField { Resource(Ident), Span(Ident), SourceCode(Ident), + DatabaseObject(Ident), } impl Parse for LocationAttr { @@ -450,6 +451,8 @@ impl Parse for LocationAttr { LocationField::Span(ident) } else if ident == "source_code" { LocationField::SourceCode(ident) + } else if ident == "database_object" { + LocationField::DatabaseObject(ident) } else { return Err(Error::new_spanned(ident, "unknown location field")); }; @@ -467,6 +470,7 @@ impl ToTokens for LocationField { LocationField::Resource(ident) => ident.to_tokens(tokens), LocationField::Span(ident) => ident.to_tokens(tokens), LocationField::SourceCode(ident) => ident.to_tokens(tokens), + LocationField::DatabaseObject(ident) => ident.to_tokens(tokens), } } } diff --git a/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts b/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts index 701554f89..5c0aa4f1f 100644 --- a/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts +++ b/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts @@ -139,7 +139,11 @@ export type Advice = /** * Represents the resource a diagnostic is associated with. */ -export type Resource_for_String = "argv" | "memory" | { file: string }; +export type Resource_for_String = + | "database" + | "argv" + | "memory" + | { file: string }; export type TextRange = [TextSize, TextSize]; export interface MarkupNodeBuf { content: string; diff --git a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts index 701554f89..5c0aa4f1f 100644 --- a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts +++ b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts @@ -139,7 +139,11 @@ export type Advice = /** * Represents the resource a diagnostic is associated with. */ -export type Resource_for_String = "argv" | "memory" | { file: string }; +export type Resource_for_String = + | "database" + | "argv" + | "memory" + | { file: string }; export type TextRange = [TextSize, TextSize]; export interface MarkupNodeBuf { content: string;