Skip to main content

sqrust_rules/layout/
no_multiple_statements_on_line.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct NoMultipleStatementsOnLine;
4
5impl Rule for NoMultipleStatementsOnLine {
6    fn name(&self) -> &'static str {
7        "Layout/NoMultipleStatementsOnLine"
8    }
9
10    fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
11        find_violations(&ctx.source, self.name())
12    }
13}
14
15fn find_violations(source: &str, rule_name: &'static str) -> Vec<Diagnostic> {
16    let bytes = source.as_bytes();
17    let len = bytes.len();
18    let mut diags = Vec::new();
19
20    let mut i = 0;
21    let mut in_string = false;
22    let mut in_line_comment = false;
23    let mut block_depth: usize = 0;
24
25    while i < len {
26        // Reset line comment at newline
27        if bytes[i] == b'\n' {
28            in_line_comment = false;
29            i += 1;
30            continue;
31        }
32
33        // If inside a line comment, skip everything until newline
34        if in_line_comment {
35            i += 1;
36            continue;
37        }
38
39        // Single-quoted string handling (outside block comment)
40        if !in_string && block_depth == 0 && bytes[i] == b'\'' {
41            in_string = true;
42            i += 1;
43            continue;
44        }
45        if in_string {
46            if bytes[i] == b'\'' {
47                // SQL '' escape
48                if i + 1 < len && bytes[i + 1] == b'\'' {
49                    i += 2;
50                    continue;
51                }
52                in_string = false;
53            }
54            i += 1;
55            continue;
56        }
57
58        // Block comment open: /*
59        if block_depth == 0 && i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
60            block_depth += 1;
61            i += 2;
62            continue;
63        }
64        // Inside block comment
65        if block_depth > 0 {
66            if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'/' {
67                block_depth -= 1;
68                i += 2;
69            } else {
70                i += 1;
71            }
72            continue;
73        }
74
75        // Line comment start: --
76        if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
77            in_line_comment = true;
78            i += 2;
79            continue;
80        }
81
82        // Semicolon found outside strings and comments
83        if bytes[i] == b';' {
84            let mut j = i + 1;
85
86            // Skip spaces and tabs (not newlines)
87            while j < len && (bytes[j] == b' ' || bytes[j] == b'\t') {
88                j += 1;
89            }
90
91            // EOF or newline → no violation
92            if j >= len || bytes[j] == b'\n' || bytes[j] == b'\r' {
93                i += 1;
94                continue;
95            }
96
97            // If next non-whitespace starts a line comment '--' → no violation
98            if j + 1 < len && bytes[j] == b'-' && bytes[j + 1] == b'-' {
99                i += 1;
100                continue;
101            }
102
103            // Otherwise: there is real content after the semicolon on the same line
104            let (line, col) = byte_offset_to_line_col(source, j);
105            diags.push(Diagnostic {
106                rule: rule_name,
107                message: "Multiple statements on the same line; each statement should be on its own line".to_string(),
108                line,
109                col,
110            });
111        }
112
113        i += 1;
114    }
115
116    diags
117}
118
119/// Converts a byte offset into a 1-indexed (line, col) pair.
120fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
121    let mut line = 1usize;
122    let mut line_start = 0usize;
123    for (i, ch) in source.char_indices() {
124        if i == offset {
125            break;
126        }
127        if ch == '\n' {
128            line += 1;
129            line_start = i + 1;
130        }
131    }
132    let col = offset - line_start + 1;
133    (line, col)
134}