sqrust_rules/layout/leading_comma.rs
1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct LeadingComma;
4
5impl Rule for LeadingComma {
6 fn name(&self) -> &'static str {
7 "Layout/LeadingComma"
8 }
9
10 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
11 let mut diags = Vec::new();
12
13 // Text-based scan: works even when ctx.parse_errors is non-empty.
14 // Split on '\n' (not .lines()) so we preserve accurate line indices.
15 let lines: Vec<&str> = ctx.source.split('\n').collect();
16 // Track whether we are inside a single-quoted string literal across lines.
17 // Uses a simple odd/even quote-count heuristic: each unescaped `'` toggles
18 // the in_string state. Good enough for the rare "comma-at-line-start inside
19 // a multi-line string" false-positive prevention.
20 let mut in_string = false;
21
22 for (idx, line) in lines.iter().enumerate() {
23 let trimmed = line.trim_start();
24
25 if !in_string && trimmed.starts_with(',') {
26 // col is 1-indexed position of ',' in the original line
27 let leading_spaces = line.len() - trimmed.len();
28 let col = leading_spaces + 1;
29 diags.push(Diagnostic {
30 rule: self.name(),
31 message: "Comma at start of line; place commas at the end of the previous line"
32 .to_string(),
33 line: idx + 1,
34 col,
35 });
36 }
37
38 // Update in_string: each single-quote character toggles the state.
39 // Odd number of quotes on the line means we cross a string boundary.
40 let quote_count = line.chars().filter(|&c| c == '\'').count();
41 if quote_count % 2 != 0 {
42 in_string = !in_string;
43 }
44 }
45
46 diags
47 }
48}