Skip to content

Commit 5533414

Browse files
authored
chore(diagnostics): add database object as diagnostics location (#593)
prep for dblint
1 parent 05df46e commit 5533414

File tree

8 files changed

+135
-3
lines changed

8 files changed

+135
-3
lines changed

crates/pgls_diagnostics/src/context.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ mod internal {
458458
resource: match loc.resource {
459459
Some(Resource::Argv) => Some(Resource::Argv),
460460
Some(Resource::Memory) => Some(Resource::Memory),
461+
Some(Resource::Database) => Some(Resource::Database),
461462
Some(Resource::File(file)) => {
462463
if let Some(Resource::File(path)) = &self.path {
463464
Some(Resource::File(path.as_ref()))
@@ -468,6 +469,7 @@ mod internal {
468469
None => self.path.as_ref().map(Resource::as_deref),
469470
},
470471
span: loc.span,
472+
database_object: loc.database_object,
471473
source_code: loc.source_code,
472474
}
473475
}
@@ -523,6 +525,7 @@ mod internal {
523525
Location {
524526
resource: loc.resource,
525527
span: self.span.or(loc.span),
528+
database_object: loc.database_object,
526529
source_code: loc.source_code,
527530
}
528531
}

crates/pgls_diagnostics/src/display.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,23 @@ impl<D: Diagnostic + ?Sized> fmt::Display for PrintHeader<'_, D> {
139139
fmt.write_str(" ")?;
140140
}
141141

142+
// Print the database object if present (e.g., "table public.contacts")
143+
if let Some(db_obj) = location.database_object {
144+
if let Some(obj_type) = db_obj.object_type {
145+
fmt.write_markup(markup! {
146+
<Dim>{obj_type}" "</Dim>
147+
})?;
148+
}
149+
if let Some(schema) = db_obj.schema {
150+
fmt.write_markup(markup! {
151+
<Emphasis>{schema}"."</Emphasis>
152+
})?;
153+
}
154+
fmt.write_markup(markup! {
155+
<Emphasis>{db_obj.name}</Emphasis>" "
156+
})?;
157+
}
158+
142159
// Print the category of the diagnostic, with a hyperlink if
143160
// the category has an associated link
144161
if let Some(category) = diagnostic.category() {
@@ -790,6 +807,7 @@ mod tests {
790807
visitor.record_frame(Location {
791808
resource: Some(Resource::File("other_path")),
792809
span: Some(TextRange::new(TextSize::from(8), TextSize::from(16))),
810+
database_object: None,
793811
source_code: Some(SourceCode {
794812
text: "context location context",
795813
line_starts: None,

crates/pgls_diagnostics/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ pub use crate::display::{
3636
};
3737
pub use crate::display_github::PrintGitHubDiagnostic;
3838
pub use crate::error::{Error, Result};
39-
pub use crate::location::{LineIndex, LineIndexBuf, Location, Resource, SourceCode};
39+
pub use crate::location::{
40+
DatabaseObject, DatabaseObjectOwned, LineIndex, LineIndexBuf, Location, Resource, SourceCode,
41+
};
4042
use pgls_console::fmt::{Formatter, Termcolor};
4143
use pgls_console::markup;
4244
use std::fmt::Write;

crates/pgls_diagnostics/src/location.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,28 @@ use std::fmt::Debug;
44
use std::ops::Range;
55
use std::{borrow::Borrow, ops::Deref};
66

7+
/// Represents a database object (table, function, etc.)
8+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9+
pub struct DatabaseObject<'a> {
10+
/// Optional schema name
11+
pub schema: Option<&'a str>,
12+
/// Object name (required)
13+
pub name: &'a str,
14+
/// Optional object type (e.g., "table", "function", "view")
15+
pub object_type: Option<&'a str>,
16+
}
17+
18+
/// Owned version of DatabaseObject for use in diagnostic structs
19+
#[derive(Debug, Clone, PartialEq, Eq)]
20+
pub struct DatabaseObjectOwned {
21+
/// Optional schema name
22+
pub schema: Option<String>,
23+
/// Object name (required)
24+
pub name: String,
25+
/// Optional object type (e.g., "table", "function", "view")
26+
pub object_type: Option<String>,
27+
}
28+
729
/// Represents the location of a diagnostic in a resource.
830
#[derive(Debug, Default, Clone, Copy)]
931
pub struct Location<'a> {
@@ -12,6 +34,8 @@ pub struct Location<'a> {
1234
/// An optional range of text within the resource associated with the
1335
/// diagnostic.
1436
pub span: Option<TextRange>,
37+
/// An optional database object reference
38+
pub database_object: Option<DatabaseObject<'a>>,
1539
/// The optional source code of the resource.
1640
pub source_code: Option<BorrowedSourceCode<'a>>,
1741
}
@@ -23,6 +47,7 @@ impl<'a> Location<'a> {
2347
resource: None,
2448
span: None,
2549
source_code: None,
50+
database_object: None,
2651
}
2752
}
2853
}
@@ -42,6 +67,8 @@ impl Eq for Location<'_> {}
4267
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
4368
#[serde(rename_all = "camelCase")]
4469
pub enum Resource<Path> {
70+
/// The diagnostic is related to the database and its schema.
71+
Database,
4572
/// The diagnostic is related to the content of the command line arguments.
4673
Argv,
4774
/// The diagnostic is related to the content of a memory buffer.
@@ -70,6 +97,7 @@ impl<P> Resource<P> {
7097
P: Deref,
7198
{
7299
match self {
100+
Resource::Database => Resource::Database,
73101
Resource::Argv => Resource::Argv,
74102
Resource::Memory => Resource::Memory,
75103
Resource::File(file) => Resource::File(file),
@@ -81,6 +109,7 @@ impl Resource<&'_ str> {
81109
/// Converts a `Path<&str>` to `Path<String>`.
82110
pub fn to_owned(self) -> Resource<String> {
83111
match self {
112+
Resource::Database => Resource::Database,
84113
Resource::Argv => Resource::Argv,
85114
Resource::Memory => Resource::Memory,
86115
Resource::File(file) => Resource::File(file.to_owned()),
@@ -194,6 +223,7 @@ pub struct LocationBuilder<'a> {
194223
resource: Option<Resource<&'a str>>,
195224
span: Option<TextRange>,
196225
source_code: Option<BorrowedSourceCode<'a>>,
226+
database_object: Option<DatabaseObject<'a>>,
197227
}
198228

199229
impl<'a> LocationBuilder<'a> {
@@ -212,11 +242,17 @@ impl<'a> LocationBuilder<'a> {
212242
self
213243
}
214244

245+
pub fn database_object<D: AsDatabaseObject>(mut self, database_object: &'a D) -> Self {
246+
self.database_object = database_object.as_database_object();
247+
self
248+
}
249+
215250
pub fn build(self) -> Location<'a> {
216251
Location {
217252
resource: self.resource,
218253
span: self.span,
219254
source_code: self.source_code,
255+
database_object: self.database_object,
220256
}
221257
}
222258
}
@@ -342,6 +378,39 @@ impl AsSourceCode for String {
342378
}
343379
}
344380

381+
/// Utility trait for types that can be converted into a database object reference
382+
pub trait AsDatabaseObject {
383+
fn as_database_object(&self) -> Option<DatabaseObject<'_>>;
384+
}
385+
386+
impl<T: AsDatabaseObject> AsDatabaseObject for Option<T> {
387+
fn as_database_object(&self) -> Option<DatabaseObject<'_>> {
388+
self.as_ref().and_then(T::as_database_object)
389+
}
390+
}
391+
392+
impl<T: AsDatabaseObject + ?Sized> AsDatabaseObject for &'_ T {
393+
fn as_database_object(&self) -> Option<DatabaseObject<'_>> {
394+
T::as_database_object(*self)
395+
}
396+
}
397+
398+
impl<'a> AsDatabaseObject for DatabaseObject<'a> {
399+
fn as_database_object(&self) -> Option<DatabaseObject<'_>> {
400+
Some(*self)
401+
}
402+
}
403+
404+
impl AsDatabaseObject for DatabaseObjectOwned {
405+
fn as_database_object(&self) -> Option<DatabaseObject<'_>> {
406+
Some(DatabaseObject {
407+
schema: self.schema.as_deref(),
408+
name: &self.name,
409+
object_type: self.object_type.as_deref(),
410+
})
411+
}
412+
}
413+
345414
#[cfg(test)]
346415
mod tests {
347416
use pgls_text_size::TextSize;

crates/pgls_diagnostics/src/serde.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ impl super::Advices for Advice {
274274
Advice::Frame(location) => visitor.record_frame(super::Location {
275275
resource: location.path.as_ref().map(super::Resource::as_deref),
276276
span: location.span,
277+
database_object: None,
277278
source_code: location.source_code.as_deref().map(|text| SourceCode {
278279
text,
279280
line_starts: None,
@@ -496,4 +497,31 @@ mod tests {
496497
//
497498
// assert_eq!(diag, expected);
498499
// }
500+
501+
#[test]
502+
fn test_database_object_location_macro() {
503+
use crate::{DatabaseObjectOwned, Diagnostic};
504+
505+
#[derive(Debug, Diagnostic)]
506+
#[diagnostic(severity = Error, category = "lint")]
507+
struct TestDatabaseObjectDiagnostic {
508+
#[location(database_object)]
509+
db_object: DatabaseObjectOwned,
510+
}
511+
512+
let diag = TestDatabaseObjectDiagnostic {
513+
db_object: DatabaseObjectOwned {
514+
schema: Some("public".to_string()),
515+
name: "contacts".to_string(),
516+
object_type: Some("table".to_string()),
517+
},
518+
};
519+
520+
let location = diag.location();
521+
assert!(location.database_object.is_some());
522+
let db_obj = location.database_object.unwrap();
523+
assert_eq!(db_obj.schema, Some("public"));
524+
assert_eq!(db_obj.name, "contacts");
525+
assert_eq!(db_obj.object_type, Some("table"));
526+
}
499527
}

crates/pgls_diagnostics_macros/src/parse.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ pub(crate) enum LocationField {
436436
Resource(Ident),
437437
Span(Ident),
438438
SourceCode(Ident),
439+
DatabaseObject(Ident),
439440
}
440441

441442
impl Parse for LocationAttr {
@@ -450,6 +451,8 @@ impl Parse for LocationAttr {
450451
LocationField::Span(ident)
451452
} else if ident == "source_code" {
452453
LocationField::SourceCode(ident)
454+
} else if ident == "database_object" {
455+
LocationField::DatabaseObject(ident)
453456
} else {
454457
return Err(Error::new_spanned(ident, "unknown location field"));
455458
};
@@ -467,6 +470,7 @@ impl ToTokens for LocationField {
467470
LocationField::Resource(ident) => ident.to_tokens(tokens),
468471
LocationField::Span(ident) => ident.to_tokens(tokens),
469472
LocationField::SourceCode(ident) => ident.to_tokens(tokens),
473+
LocationField::DatabaseObject(ident) => ident.to_tokens(tokens),
470474
}
471475
}
472476
}

packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,11 @@ export type Advice =
139139
/**
140140
* Represents the resource a diagnostic is associated with.
141141
*/
142-
export type Resource_for_String = "argv" | "memory" | { file: string };
142+
export type Resource_for_String =
143+
| "database"
144+
| "argv"
145+
| "memory"
146+
| { file: string };
143147
export type TextRange = [TextSize, TextSize];
144148
export interface MarkupNodeBuf {
145149
content: string;

packages/@postgrestools/backend-jsonrpc/src/workspace.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,11 @@ export type Advice =
139139
/**
140140
* Represents the resource a diagnostic is associated with.
141141
*/
142-
export type Resource_for_String = "argv" | "memory" | { file: string };
142+
export type Resource_for_String =
143+
| "database"
144+
| "argv"
145+
| "memory"
146+
| { file: string };
143147
export type TextRange = [TextSize, TextSize];
144148
export interface MarkupNodeBuf {
145149
content: string;

0 commit comments

Comments
 (0)