Skip to content

Commit 72caa41

Browse files
committed
feat: plpgsql check
1 parent b651c75 commit 72caa41

File tree

16 files changed

+627
-28
lines changed

16 files changed

+627
-28
lines changed

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ pgt_lexer = { path = "./crates/pgt_lexer", version = "0.0.0" }
7676
pgt_lexer_codegen = { path = "./crates/pgt_lexer_codegen", version = "0.0.0" }
7777
pgt_lsp = { path = "./crates/pgt_lsp", version = "0.0.0" }
7878
pgt_markup = { path = "./crates/pgt_markup", version = "0.0.0" }
79+
pgt_plpgsql_check = { path = "./crates/pgt_plpgsql_check", version = "0.0.0" }
7980
pgt_query = { path = "./crates/pgt_query", version = "0.0.0" }
8081
pgt_query_ext = { path = "./crates/pgt_query_ext", version = "0.0.0" }
8182
pgt_query_macros = { path = "./crates/pgt_query_macros", version = "0.0.0" }

Dockerfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
FROM postgres:15
2+
3+
# Install build dependencies
4+
RUN apt-get update && \
5+
apt-get install -y postgresql-server-dev-15 gcc make git && \
6+
cd /tmp && \
7+
git clone https://github.com/okbob/plpgsql_check.git && \
8+
cd plpgsql_check && \
9+
make && \
10+
make install && \
11+
apt-get remove -y postgresql-server-dev-15 gcc make git && \
12+
apt-get autoremove -y && \
13+
rm -rf /tmp/plpgsql_check /var/lib/apt/lists/*
14+
15+
# Add initialization script directly
16+
RUN echo "CREATE EXTENSION IF NOT EXISTS plpgsql_check;" > /docker-entrypoint-initdb.d/01-create-extension.sql

crates/pgt_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ define_categories! {
3232
"flags/invalid",
3333
"project",
3434
"typecheck",
35+
"plpgsql_check",
3536
"internalError/panic",
3637
"syntax",
3738
"dummy",

crates/pgt_plpgsql_check/Cargo.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[package]
2+
authors.workspace = true
3+
categories.workspace = true
4+
description = "<DESCRIPTION>"
5+
edition.workspace = true
6+
homepage.workspace = true
7+
keywords.workspace = true
8+
license.workspace = true
9+
name = "pgt_plpgsql_check"
10+
repository.workspace = true
11+
version = "0.0.0"
12+
13+
14+
[dependencies]
15+
pgt_console = { workspace = true }
16+
pgt_diagnostics = { workspace = true }
17+
pgt_query = { workspace = true }
18+
pgt_query_ext = { workspace = true }
19+
pgt_schema_cache = { workspace = true }
20+
pgt_text_size = { workspace = true }
21+
serde = { workspace = true }
22+
serde_json = { workspace = true }
23+
sqlx = { workspace = true }
24+
tree-sitter = { workspace = true }
25+
26+
[dev-dependencies]
27+
pgt_test_utils = { workspace = true }
28+
29+
[lib]
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
use std::io;
2+
3+
use pgt_console::markup;
4+
use pgt_diagnostics::{Advices, Diagnostic, LogCategory, MessageAndDescription, Severity, Visit};
5+
use pgt_text_size::TextRange;
6+
7+
use crate::{PlpgSqlCheckIssue, PlpgSqlCheckResult};
8+
9+
/// A specialized diagnostic for plpgsql_check.
10+
#[derive(Clone, Debug, Diagnostic)]
11+
#[diagnostic(category = "plpgsql_check")]
12+
pub struct PlPgSqlCheckDiagnostic {
13+
#[location(span)]
14+
pub span: Option<TextRange>,
15+
#[description]
16+
#[message]
17+
pub message: MessageAndDescription,
18+
#[advice]
19+
pub advices: PlPgSqlCheckAdvices,
20+
#[severity]
21+
pub severity: Severity,
22+
}
23+
24+
#[derive(Debug, Clone)]
25+
pub struct PlPgSqlCheckAdvices {
26+
pub code: Option<String>,
27+
pub statement: Option<String>,
28+
pub query: Option<String>,
29+
pub line_number: Option<String>,
30+
pub query_position: Option<String>,
31+
}
32+
33+
impl Advices for PlPgSqlCheckAdvices {
34+
fn record(&self, visitor: &mut dyn Visit) -> io::Result<()> {
35+
// Show the error code if available
36+
if let Some(code) = &self.code {
37+
visitor.record_log(
38+
LogCategory::Error,
39+
&markup! { "SQL State: " <Emphasis>{code}</Emphasis> },
40+
)?;
41+
}
42+
43+
// Show statement information if available
44+
if let Some(statement) = &self.statement {
45+
if let Some(line_number) = &self.line_number {
46+
visitor.record_log(
47+
LogCategory::Info,
48+
&markup! { "At line " <Emphasis>{line_number}</Emphasis> ": "{statement}"" },
49+
)?;
50+
} else {
51+
visitor.record_log(LogCategory::Info, &markup! { "Statement: "{statement}"" })?;
52+
}
53+
}
54+
55+
// Show query information if available
56+
if let Some(query) = &self.query {
57+
if let Some(pos) = &self.query_position {
58+
visitor.record_log(
59+
LogCategory::Info,
60+
&markup! { "In query at position " <Emphasis>{pos}</Emphasis> ":\n"{query}"" },
61+
)?;
62+
} else {
63+
visitor.record_log(LogCategory::Info, &markup! { "Query:\n"{query}"" })?;
64+
}
65+
}
66+
67+
Ok(())
68+
}
69+
}
70+
71+
/// Convert plpgsql_check results into diagnostics
72+
pub fn create_diagnostics_from_check_result(
73+
result: &PlpgSqlCheckResult,
74+
fn_body: &str,
75+
start: usize,
76+
) -> Vec<PlPgSqlCheckDiagnostic> {
77+
result
78+
.issues
79+
.iter()
80+
.map(|issue| create_diagnostic_from_issue(issue, fn_body, start))
81+
.collect()
82+
}
83+
84+
fn create_diagnostic_from_issue(
85+
issue: &PlpgSqlCheckIssue,
86+
fn_body: &str,
87+
start: usize,
88+
) -> PlPgSqlCheckDiagnostic {
89+
let severity = match issue.level.as_str() {
90+
"error" => Severity::Error,
91+
"warning" => Severity::Warning,
92+
"notice" => Severity::Hint,
93+
_ => Severity::Information,
94+
};
95+
96+
let span = if let Some(s) = &issue.statement {
97+
let line_number = s.line_number.parse::<usize>().unwrap_or(0);
98+
if line_number > 0 {
99+
let mut current_offset = 0;
100+
let mut result = None;
101+
for (i, line) in fn_body.lines().enumerate() {
102+
if i + 1 == line_number {
103+
if let Some(stmt_pos) = line.to_lowercase().find(&s.text.to_lowercase()) {
104+
let line_start = start + current_offset + stmt_pos;
105+
let line_end = line_start + s.text.len();
106+
result = Some(TextRange::new(
107+
(line_start as u32).into(),
108+
(line_end as u32).into(),
109+
));
110+
} else {
111+
let line_start = start + current_offset;
112+
let line_end = line_start + line.len();
113+
result = Some(TextRange::new(
114+
(line_start as u32).into(),
115+
(line_end as u32).into(),
116+
));
117+
}
118+
break;
119+
}
120+
current_offset += line.len() + 1;
121+
}
122+
result
123+
} else {
124+
None
125+
}
126+
} else {
127+
None
128+
};
129+
130+
PlPgSqlCheckDiagnostic {
131+
message: issue.message.clone().into(),
132+
severity,
133+
span,
134+
advices: PlPgSqlCheckAdvices {
135+
code: issue.sql_state.clone(),
136+
statement: issue.statement.as_ref().map(|s| s.text.clone()),
137+
query: issue.query.as_ref().map(|q| q.text.clone()),
138+
line_number: issue.statement.as_ref().map(|s| s.line_number.clone()),
139+
query_position: issue.query.as_ref().map(|q| q.position.clone()),
140+
},
141+
}
142+
}

0 commit comments

Comments
 (0)