Skip to main content

rigsql_rules/layout/
lt04.rs

1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// LT04: Leading/trailing commas.
7///
8/// By default, expects trailing commas (comma at end of line).
9#[derive(Debug)]
10pub struct RuleLT04 {
11    pub style: CommaStyle,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum CommaStyle {
16    Trailing,
17    Leading,
18}
19
20impl Default for RuleLT04 {
21    fn default() -> Self {
22        Self {
23            style: CommaStyle::Trailing,
24        }
25    }
26}
27
28impl Rule for RuleLT04 {
29    fn code(&self) -> &'static str {
30        "LT04"
31    }
32    fn name(&self) -> &'static str {
33        "layout.commas"
34    }
35    fn description(&self) -> &'static str {
36        "Commas should be at the end of the line, not the start."
37    }
38    fn explanation(&self) -> &'static str {
39        "Commas in SELECT lists, GROUP BY, and other clauses should consistently appear \
40         at the end of the line (trailing) or the start of the next line (leading). \
41         Mixing styles reduces readability."
42    }
43    fn groups(&self) -> &[RuleGroup] {
44        &[RuleGroup::Layout]
45    }
46    fn is_fixable(&self) -> bool {
47        true
48    }
49
50    fn configure(&mut self, settings: &std::collections::HashMap<String, String>) {
51        if let Some(val) = settings.get("comma_style") {
52            self.style = match val.as_str() {
53                "leading" => CommaStyle::Leading,
54                _ => CommaStyle::Trailing,
55            };
56        }
57    }
58
59    fn crawl_type(&self) -> CrawlType {
60        CrawlType::Segment(vec![SegmentType::Comma])
61    }
62
63    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
64        let span = ctx.segment.span();
65
66        match self.style {
67            CommaStyle::Trailing => {
68                // Comma should NOT be preceded by a newline (that means it's leading)
69                // Check: is there a newline immediately before the comma (possibly with whitespace)?
70                if is_leading_comma(ctx) {
71                    return vec![LintViolation::new(
72                        self.code(),
73                        "Comma should be at the end of the line, not the start.",
74                        span,
75                    )];
76                }
77            }
78            CommaStyle::Leading => {
79                // Comma should be preceded by a newline (leading style)
80                if is_trailing_comma(ctx) {
81                    return vec![LintViolation::new(
82                        self.code(),
83                        "Comma should be at the start of the line, not the end.",
84                        span,
85                    )];
86                }
87            }
88        }
89
90        vec![]
91    }
92}
93
94/// Check if comma is in leading position (newline then optional whitespace then comma).
95fn is_leading_comma(ctx: &RuleContext) -> bool {
96    if ctx.index_in_parent == 0 {
97        return false;
98    }
99    // Walk backwards past whitespace to see if there's a newline
100    let mut i = ctx.index_in_parent - 1;
101    loop {
102        let seg = &ctx.siblings[i];
103        match seg.segment_type() {
104            SegmentType::Whitespace => {
105                if i == 0 {
106                    return false;
107                }
108                i -= 1;
109            }
110            SegmentType::Newline => return true,
111            _ => return false,
112        }
113    }
114}
115
116/// Check if comma is in trailing position (comma then optional whitespace then newline).
117fn is_trailing_comma(ctx: &RuleContext) -> bool {
118    let mut i = ctx.index_in_parent + 1;
119    while i < ctx.siblings.len() {
120        let seg = &ctx.siblings[i];
121        match seg.segment_type() {
122            SegmentType::Whitespace => {
123                i += 1;
124            }
125            SegmentType::Newline => return true,
126            _ => return false,
127        }
128    }
129    false
130}