Skip to main content

rigsql_rules/layout/
lt03.rs

1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6/// LT03: Operators should be followed by a single space.
7///
8/// Checks that comparison and arithmetic operators have spaces on both sides.
9#[derive(Debug, Default)]
10pub struct RuleLT03;
11
12impl Rule for RuleLT03 {
13    fn code(&self) -> &'static str {
14        "LT03"
15    }
16    fn name(&self) -> &'static str {
17        "layout.operators"
18    }
19    fn description(&self) -> &'static str {
20        "Operators should be surrounded by single spaces."
21    }
22    fn explanation(&self) -> &'static str {
23        "Binary operators (=, <, >, +, -, etc.) should have a single space on each side \
24         for readability. 'a=b' and 'a  = b' are harder to read than 'a = b'."
25    }
26    fn groups(&self) -> &[RuleGroup] {
27        &[RuleGroup::Layout]
28    }
29    fn is_fixable(&self) -> bool {
30        true
31    }
32
33    fn crawl_type(&self) -> CrawlType {
34        CrawlType::Segment(vec![
35            SegmentType::ComparisonOperator,
36            SegmentType::ArithmeticOperator,
37        ])
38    }
39
40    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
41        let span = ctx.segment.span();
42        let mut violations = Vec::new();
43
44        // Check space before operator
45        // Newline (or whitespace followed by newline = indentation) counts as valid separation
46        if ctx.index_in_parent > 0 {
47            let prev = &ctx.siblings[ctx.index_in_parent - 1];
48            let prev_type = prev.segment_type();
49            if prev_type != SegmentType::Whitespace && prev_type != SegmentType::Newline {
50                violations.push(LintViolation::with_fix_and_msg_key(
51                    self.code(),
52                    "Missing space before operator.",
53                    span,
54                    vec![SourceEdit::insert(span.start, " ")],
55                    "rules.LT03.msg.before",
56                    vec![],
57                ));
58            }
59        }
60
61        // Check space after operator
62        // Newline after operator is acceptable (line break after operator style)
63        if ctx.index_in_parent + 1 < ctx.siblings.len() {
64            let next = &ctx.siblings[ctx.index_in_parent + 1];
65            let next_type = next.segment_type();
66            if next_type != SegmentType::Whitespace && next_type != SegmentType::Newline {
67                violations.push(LintViolation::with_fix_and_msg_key(
68                    self.code(),
69                    "Missing space after operator.",
70                    span,
71                    vec![SourceEdit::insert(span.end, " ")],
72                    "rules.LT03.msg.after",
73                    vec![],
74                ));
75            }
76        }
77
78        violations
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::test_utils::lint_sql;
86
87    #[test]
88    fn test_lt03_flags_missing_space() {
89        let violations = lint_sql("SELECT * FROM t WHERE x=1", RuleLT03);
90        assert!(!violations.is_empty());
91        assert!(violations.iter().all(|v| v.rule_code == "LT03"));
92    }
93
94    #[test]
95    fn test_lt03_accepts_proper_spacing() {
96        let violations = lint_sql("SELECT * FROM t WHERE x = 1", RuleLT03);
97        assert_eq!(violations.len(), 0);
98    }
99
100    #[test]
101    fn test_lt03_accepts_newline_after_operator() {
102        // Operator at end of line — newline is valid separation (no conflict with LT01)
103        let violations = lint_sql("SELECT 'Data' =\n    'n'", RuleLT03);
104        assert_eq!(violations.len(), 0);
105    }
106
107    #[test]
108    fn test_lt03_accepts_newline_before_operator() {
109        // Operator at start of line — newline is valid separation
110        let violations = lint_sql("SELECT *\nFROM t WHERE x\n= 1", RuleLT03);
111        let after_violations: Vec<_> = violations
112            .iter()
113            .filter(|v| v.message.contains("before"))
114            .collect();
115        assert_eq!(after_violations.len(), 0);
116    }
117}