Skip to main content

sqrust_rules/layout/
statement_semicolons.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct StatementSemicolons;
4
5impl Rule for StatementSemicolons {
6    fn name(&self) -> &'static str {
7        "Layout/StatementSemicolons"
8    }
9
10    fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
11        // If there are parse errors we cannot reliably determine statement
12        // boundaries, so skip.
13        if !ctx.parse_errors.is_empty() {
14            return Vec::new();
15        }
16
17        let source = &ctx.source;
18
19        // Empty or whitespace-only sources have no statements to check.
20        let trimmed = source.trim();
21        if trimmed.is_empty() {
22            return Vec::new();
23        }
24
25        // Check if the last non-whitespace character is a semicolon.
26        let last_char = trimmed.chars().next_back();
27        if last_char == Some(';') {
28            return Vec::new();
29        }
30
31        // No trailing semicolon — find the last non-empty line.
32        let (last_line_no, _) = last_non_empty_line(source);
33
34        vec![Diagnostic {
35            rule: self.name(),
36            message: "SQL statement is missing a trailing semicolon".to_string(),
37            line: last_line_no,
38            col: 1,
39        }]
40    }
41}
42
43/// Returns the 1-indexed line number and content of the last non-empty
44/// (non-whitespace-only) line in `source`.
45/// Falls back to (1, "") if the source has no non-empty lines.
46fn last_non_empty_line(source: &str) -> (usize, &str) {
47    let mut last = (1usize, "");
48    for (i, line) in source.lines().enumerate() {
49        if !line.trim().is_empty() {
50            last = (i + 1, line);
51        }
52    }
53    last
54}