Skip to main content

rigsql_rules/layout/
lt02.rs

1use rigsql_core::{Segment, SegmentType, TokenKind};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// LT02: Incorrect indentation.
7///
8/// Expects consistent indentation (default 4 spaces per level).
9#[derive(Debug)]
10pub struct RuleLT02 {
11    pub indent_size: usize,
12}
13
14impl Default for RuleLT02 {
15    fn default() -> Self {
16        Self { indent_size: 4 }
17    }
18}
19
20impl Rule for RuleLT02 {
21    fn code(&self) -> &'static str {
22        "LT02"
23    }
24    fn name(&self) -> &'static str {
25        "layout.indent"
26    }
27    fn description(&self) -> &'static str {
28        "Incorrect indentation."
29    }
30    fn explanation(&self) -> &'static str {
31        "SQL should use consistent indentation. Each indentation level should use \
32         the same number of spaces (default 4). Tabs should not be mixed with spaces."
33    }
34    fn groups(&self) -> &[RuleGroup] {
35        &[RuleGroup::Layout]
36    }
37    fn is_fixable(&self) -> bool {
38        true
39    }
40
41    fn configure(&mut self, settings: &std::collections::HashMap<String, String>) {
42        if let Some(val) = settings.get("indent_unit") {
43            if val == "tab" {
44                self.indent_size = 1; // tab mode
45            }
46        }
47        if let Some(val) = settings.get("tab_space_size") {
48            if let Ok(n) = val.parse() {
49                self.indent_size = n;
50            }
51        }
52    }
53
54    fn crawl_type(&self) -> CrawlType {
55        CrawlType::Segment(vec![SegmentType::Whitespace])
56    }
57
58    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
59        let Segment::Token(t) = ctx.segment else {
60            return vec![];
61        };
62        if t.token.kind != TokenKind::Whitespace {
63            return vec![];
64        }
65
66        let text = t.token.text.as_str();
67
68        // Only check indentation (whitespace after a newline)
69        if ctx.index_in_parent == 0 {
70            return vec![];
71        }
72        let prev = &ctx.siblings[ctx.index_in_parent - 1];
73        if prev.segment_type() != SegmentType::Newline {
74            return vec![];
75        }
76
77        // Flag tabs mixed with spaces
78        if text.contains('\t') && text.contains(' ') {
79            return vec![LintViolation::with_msg_key(
80                self.code(),
81                "Mixed tabs and spaces in indentation.",
82                t.token.span,
83                "rules.LT02.msg.mixed",
84                vec![],
85            )];
86        }
87
88        // Flag non-multiple of indent_size (space-only indentation)
89        if !text.contains('\t') && text.len() % self.indent_size != 0 {
90            return vec![LintViolation::with_msg_key(
91                self.code(),
92                format!(
93                    "Indentation is not a multiple of {} spaces (found {} spaces).",
94                    self.indent_size,
95                    text.len()
96                ),
97                t.token.span,
98                "rules.LT02.msg.not_multiple",
99                vec![
100                    ("size".to_string(), self.indent_size.to_string()),
101                    ("found".to_string(), text.len().to_string()),
102                ],
103            )];
104        }
105
106        vec![]
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::test_utils::lint_sql;
114
115    #[test]
116    fn test_lt02_flags_odd_indent() {
117        let violations = lint_sql("SELECT *\n   FROM t", RuleLT02::default());
118        assert!(!violations.is_empty());
119        assert!(violations.iter().all(|v| v.rule_code == "LT02"));
120    }
121
122    #[test]
123    fn test_lt02_accepts_4space_indent() {
124        let violations = lint_sql("SELECT *\n    FROM t", RuleLT02::default());
125        assert_eq!(violations.len(), 0);
126    }
127
128    #[test]
129    fn test_lt02_flags_mixed_tabs_spaces() {
130        let violations = lint_sql("SELECT *\n\t FROM t", RuleLT02::default());
131        assert!(!violations.is_empty());
132        assert!(violations.iter().all(|v| v.rule_code == "LT02"));
133    }
134}