Skip to main content

rigsql_rules/layout/
lt01.rs

1use rigsql_core::{Segment, SegmentType, TokenKind};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6/// LT01: Inappropriate spacing.
7///
8/// Checks for multiple spaces where a single space is expected,
9/// and missing spaces around operators.
10#[derive(Debug, Default)]
11pub struct RuleLT01;
12
13impl Rule for RuleLT01 {
14    fn code(&self) -> &'static str {
15        "LT01"
16    }
17    fn name(&self) -> &'static str {
18        "layout.spacing"
19    }
20    fn description(&self) -> &'static str {
21        "Inappropriate spacing found."
22    }
23    fn explanation(&self) -> &'static str {
24        "SQL should use single spaces between keywords and expressions. \
25         Multiple consecutive spaces (except for indentation) reduce readability. \
26         Operators should have spaces on both sides."
27    }
28    fn groups(&self) -> &[RuleGroup] {
29        &[RuleGroup::Layout]
30    }
31    fn is_fixable(&self) -> bool {
32        true
33    }
34
35    fn crawl_type(&self) -> CrawlType {
36        CrawlType::Segment(vec![SegmentType::Whitespace])
37    }
38
39    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
40        let Segment::Token(t) = ctx.segment else {
41            return vec![];
42        };
43        if t.token.kind != TokenKind::Whitespace {
44            return vec![];
45        }
46
47        // Position 0 means start of siblings — skip
48        if ctx.index_in_parent == 0 {
49            return vec![];
50        }
51
52        let prev = &ctx.siblings[ctx.index_in_parent - 1];
53        // If previous is Newline, this is indentation — skip
54        if prev.segment_type() == SegmentType::Newline {
55            return vec![];
56        }
57
58        let text = t.token.text.as_str();
59
60        // Check if this is trailing whitespace (next sibling is explicitly a Newline).
61        // When the whitespace is the last sibling, it may be at a node boundary
62        // (e.g. space between two statements in the same line) — NOT trailing.
63        let is_trailing = if ctx.index_in_parent + 1 < ctx.siblings.len() {
64            ctx.siblings[ctx.index_in_parent + 1].segment_type() == SegmentType::Newline
65        } else {
66            false
67        };
68
69        if is_trailing {
70            return vec![LintViolation::with_fix_and_msg_key(
71                self.code(),
72                "Trailing whitespace.",
73                t.token.span,
74                vec![SourceEdit::delete(t.token.span)],
75                "rules.LT01.msg.trailing",
76                vec![],
77            )];
78        }
79
80        // Excessive inline spacing (multiple spaces)
81        if text.len() > 1 {
82            return vec![LintViolation::with_fix_and_msg_key(
83                self.code(),
84                format!("Expected single space, found {} spaces.", text.len()),
85                t.token.span,
86                vec![SourceEdit::replace(t.token.span, " ")],
87                "rules.LT01.msg",
88                vec![("count".to_string(), text.len().to_string())],
89            )];
90        }
91
92        vec![]
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::test_utils::lint_sql;
100
101    #[test]
102    fn test_lt01_flags_double_space() {
103        let violations = lint_sql("SELECT  *  FROM t", RuleLT01);
104        assert!(!violations.is_empty());
105        assert!(violations.iter().all(|v| v.rule_code == "LT01"));
106    }
107
108    #[test]
109    fn test_lt01_accepts_single_space() {
110        let violations = lint_sql("SELECT * FROM t", RuleLT01);
111        assert_eq!(violations.len(), 0);
112    }
113
114    #[test]
115    fn test_lt01_skips_indentation() {
116        let violations = lint_sql("SELECT *\n    FROM t", RuleLT01);
117        assert_eq!(violations.len(), 0);
118    }
119
120    #[test]
121    fn test_lt01_trailing_whitespace_removed() {
122        let violations = lint_sql("SELECT *  \n FROM t", RuleLT01);
123        assert_eq!(violations.len(), 1);
124        assert_eq!(violations[0].message, "Trailing whitespace.");
125        assert!(violations[0].fixes[0].new_text.is_empty());
126    }
127
128    #[test]
129    fn test_lt01_single_trailing_space() {
130        let violations = lint_sql("SELECT * \nFROM t", RuleLT01);
131        assert_eq!(violations.len(), 1);
132        assert_eq!(violations[0].message, "Trailing whitespace.");
133        assert!(violations[0].fixes[0].new_text.is_empty());
134    }
135}