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 Newline or this is last sibling)
61        let is_trailing = if ctx.index_in_parent + 1 < ctx.siblings.len() {
62            ctx.siblings[ctx.index_in_parent + 1].segment_type() == SegmentType::Newline
63        } else {
64            true
65        };
66
67        if is_trailing {
68            return vec![LintViolation::with_fix_and_msg_key(
69                self.code(),
70                "Trailing whitespace.",
71                t.token.span,
72                vec![SourceEdit::delete(t.token.span)],
73                "rules.LT01.msg.trailing",
74                vec![],
75            )];
76        }
77
78        // Excessive inline spacing (multiple spaces)
79        if text.len() > 1 {
80            return vec![LintViolation::with_fix_and_msg_key(
81                self.code(),
82                format!("Expected single space, found {} spaces.", text.len()),
83                t.token.span,
84                vec![SourceEdit::replace(t.token.span, " ")],
85                "rules.LT01.msg",
86                vec![("count".to_string(), text.len().to_string())],
87            )];
88        }
89
90        vec![]
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::test_utils::lint_sql;
98
99    #[test]
100    fn test_lt01_flags_double_space() {
101        let violations = lint_sql("SELECT  *  FROM t", RuleLT01);
102        assert!(!violations.is_empty());
103        assert!(violations.iter().all(|v| v.rule_code == "LT01"));
104    }
105
106    #[test]
107    fn test_lt01_accepts_single_space() {
108        let violations = lint_sql("SELECT * FROM t", RuleLT01);
109        assert_eq!(violations.len(), 0);
110    }
111
112    #[test]
113    fn test_lt01_skips_indentation() {
114        let violations = lint_sql("SELECT *\n    FROM t", RuleLT01);
115        assert_eq!(violations.len(), 0);
116    }
117
118    #[test]
119    fn test_lt01_trailing_whitespace_removed() {
120        let violations = lint_sql("SELECT *  \n FROM t", RuleLT01);
121        assert_eq!(violations.len(), 1);
122        assert_eq!(violations[0].message, "Trailing whitespace.");
123        assert!(violations[0].fixes[0].new_text.is_empty());
124    }
125
126    #[test]
127    fn test_lt01_single_trailing_space() {
128        let violations = lint_sql("SELECT * \nFROM t", RuleLT01);
129        assert_eq!(violations.len(), 1);
130        assert_eq!(violations[0].message, "Trailing whitespace.");
131        assert!(violations[0].fixes[0].new_text.is_empty());
132    }
133}