rigsql_rules/layout/
lt02.rs1use rigsql_core::{Segment, SegmentType, TokenKind};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6#[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; }
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 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 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 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}