Skip to content

Commit a359409

Browse files
committed
progress
1 parent 0b05fdb commit a359409

File tree

8 files changed

+335
-98
lines changed

8 files changed

+335
-98
lines changed

Cargo.lock

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

crates/pgt_plpgsql_check/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pgt_query = { workspace = true }
1818
pgt_query_ext = { workspace = true }
1919
pgt_schema_cache = { workspace = true }
2020
pgt_text_size = { workspace = true }
21+
regex = { workspace = true }
2122
serde = { workspace = true }
2223
serde_json = { workspace = true }
2324
sqlx = { workspace = true }

crates/pgt_plpgsql_check/src/diagnostics.rs

Lines changed: 110 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ pub struct PlPgSqlCheckDiagnostic {
2424
#[derive(Debug, Clone)]
2525
pub struct PlPgSqlCheckAdvices {
2626
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>,
3127
}
3228

3329
impl Advices for PlPgSqlCheckAdvices {
@@ -40,30 +36,6 @@ impl Advices for PlPgSqlCheckAdvices {
4036
)?;
4137
}
4238

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-
6739
Ok(())
6840
}
6941
}
@@ -72,19 +44,19 @@ impl Advices for PlPgSqlCheckAdvices {
7244
pub fn create_diagnostics_from_check_result(
7345
result: &PlpgSqlCheckResult,
7446
fn_body: &str,
75-
start: usize,
47+
offset: usize,
7648
) -> Vec<PlPgSqlCheckDiagnostic> {
7749
result
7850
.issues
7951
.iter()
80-
.map(|issue| create_diagnostic_from_issue(issue, fn_body, start))
52+
.map(|issue| create_diagnostic_from_issue(issue, fn_body, offset))
8153
.collect()
8254
}
8355

8456
fn create_diagnostic_from_issue(
8557
issue: &PlpgSqlCheckIssue,
8658
fn_body: &str,
87-
start: usize,
59+
offset: usize,
8860
) -> PlPgSqlCheckDiagnostic {
8961
let severity = match issue.level.as_str() {
9062
"error" => Severity::Error,
@@ -93,50 +65,118 @@ fn create_diagnostic_from_issue(
9365
_ => Severity::Information,
9466
};
9567

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-
13068
PlPgSqlCheckDiagnostic {
13169
message: issue.message.clone().into(),
13270
severity,
133-
span,
71+
span: resolve_span(issue, fn_body, offset),
13472
advices: PlPgSqlCheckAdvices {
13573
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()),
14074
},
14175
}
14276
}
77+
78+
fn resolve_span(issue: &PlpgSqlCheckIssue, fn_body: &str, offset: usize) -> Option<TextRange> {
79+
let stmt = issue.statement.as_ref()?;
80+
81+
let line_number = stmt
82+
.line_number
83+
.parse::<usize>()
84+
.expect("Expected line number to be a valid usize");
85+
86+
let text = &stmt.text;
87+
88+
// calculate the offset to the target line
89+
let line_offset: usize = fn_body
90+
.lines()
91+
.take(line_number - 1)
92+
.map(|line| line.len() + 1) // +1 for newline
93+
.sum();
94+
95+
// find the position within the target line
96+
let line = fn_body.lines().nth(line_number - 1)?;
97+
let start = line
98+
.to_lowercase()
99+
.find(&text.to_lowercase())
100+
.unwrap_or_else(|| {
101+
line.char_indices()
102+
.find_map(|(i, c)| if !c.is_whitespace() { Some(i) } else { None })
103+
.unwrap_or(0)
104+
});
105+
106+
let stmt_offset = line_offset + start;
107+
108+
if let Some(q) = &issue.query {
109+
// first find the query within the fn body *after* stmt_offset
110+
let query_start = fn_body[stmt_offset..]
111+
.to_lowercase()
112+
.find(&q.text.to_lowercase())
113+
.map(|pos| pos + stmt_offset);
114+
115+
// the position is *within* the query text
116+
let pos = q
117+
.position
118+
.parse::<usize>()
119+
.expect("Expected query position to be a valid usize")
120+
- 1; // -1 because the position is 1-based
121+
122+
let start = query_start? + pos;
123+
124+
// the range of the diagnostics is the token that `pos` is on
125+
// Find the end of the current token by looking for whitespace or SQL delimiters
126+
let remaining = &fn_body[start..];
127+
let end = remaining
128+
.char_indices()
129+
.find(|(_, c)| {
130+
c.is_whitespace() || matches!(c, ',' | ';' | ')' | '(' | '=' | '<' | '>')
131+
})
132+
.map(|(i, c)| {
133+
if matches!(c, ';') {
134+
i + 1 // include the semicolon
135+
} else {
136+
i // just the token end
137+
}
138+
})
139+
.unwrap_or(remaining.len());
140+
141+
return Some(TextRange::new(
142+
((offset + start) as u32).into(),
143+
((offset + start + end) as u32).into(),
144+
));
145+
}
146+
147+
// if no query is present, the end range covers
148+
// - if text is "IF" or "ELSIF", then until the next "THEN"
149+
// - TODO: check "LOOP", "CASE", "WHILE", "EXPECTION" and others
150+
// - else: until the next semicolon or end of line
151+
152+
if text.to_uppercase() == "IF" || text.to_uppercase() == "ELSIF" {
153+
// Find the position of the next "THEN" after the statement
154+
let remaining = &fn_body[stmt_offset..];
155+
if let Some(then_pos) = remaining.to_uppercase().find("THEN") {
156+
let end = then_pos + "THEN".len();
157+
return Some(TextRange::new(
158+
((offset + stmt_offset) as u32).into(),
159+
((offset + stmt_offset + end) as u32).into(),
160+
));
161+
}
162+
}
163+
164+
// if no specific end is found, use the next semicolon or the end of the line
165+
let remaining = &fn_body[stmt_offset..];
166+
let end = remaining
167+
.char_indices()
168+
.find(|(_, c)| matches!(c, ';' | '\n' | '\r'))
169+
.map(|(i, c)| {
170+
if c == ';' {
171+
i + 1 // include the semicolon
172+
} else {
173+
i // just the end of the line
174+
}
175+
})
176+
.unwrap_or(remaining.len());
177+
178+
Some(TextRange::new(
179+
((offset + stmt_offset) as u32).into(),
180+
((offset + stmt_offset + end) as u32).into(),
181+
))
182+
}

0 commit comments

Comments
 (0)