rigsql_rules/layout/
lt02.rs1use rigsql_core::{Segment, SegmentType, TokenKind};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
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 RuleLT02 {
21 fn round_to_indent(&self, value: usize) -> usize {
23 if value == 0 {
24 self.indent_size
25 } else {
26 value.div_ceil(self.indent_size) * self.indent_size
27 }
28 }
29}
30
31impl Rule for RuleLT02 {
32 fn code(&self) -> &'static str {
33 "LT02"
34 }
35 fn name(&self) -> &'static str {
36 "layout.indent"
37 }
38 fn description(&self) -> &'static str {
39 "Incorrect indentation."
40 }
41 fn explanation(&self) -> &'static str {
42 "SQL should use consistent indentation. Each indentation level should use \
43 the same number of spaces (default 4). Tabs should not be mixed with spaces."
44 }
45 fn groups(&self) -> &[RuleGroup] {
46 &[RuleGroup::Layout]
47 }
48 fn is_fixable(&self) -> bool {
49 true
50 }
51
52 fn configure(&mut self, settings: &std::collections::HashMap<String, String>) {
53 if let Some(val) = settings.get("indent_unit") {
54 if val == "tab" {
55 self.indent_size = 1; }
57 }
58 if let Some(val) = settings.get("tab_space_size") {
59 if let Ok(n) = val.parse() {
60 self.indent_size = n;
61 }
62 }
63 }
64
65 fn crawl_type(&self) -> CrawlType {
66 CrawlType::Segment(vec![SegmentType::Whitespace])
67 }
68
69 fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
70 let Segment::Token(t) = ctx.segment else {
71 return vec![];
72 };
73 if t.token.kind != TokenKind::Whitespace {
74 return vec![];
75 }
76
77 let text = t.token.text.as_str();
78
79 if ctx.index_in_parent == 0 {
81 return vec![];
82 }
83 let prev = &ctx.siblings[ctx.index_in_parent - 1];
84 if prev.segment_type() != SegmentType::Newline {
85 return vec![];
86 }
87
88 if text.contains('\t') && text.contains(' ') {
90 let visual_width: usize = text
91 .chars()
92 .map(|c| if c == '\t' { self.indent_size } else { 1 })
93 .sum();
94 let rounded = self.round_to_indent(visual_width);
95 let fixed = " ".repeat(rounded);
96 return vec![LintViolation::with_fix_and_msg_key(
97 self.code(),
98 "Mixed tabs and spaces in indentation.",
99 t.token.span,
100 vec![SourceEdit::replace(t.token.span, fixed)],
101 "rules.LT02.msg.mixed",
102 vec![],
103 )];
104 }
105
106 if !text.contains('\t') && text.len() % self.indent_size != 0 {
109 let rounded = self.round_to_indent(text.len());
110 let fixed = " ".repeat(rounded);
111 return vec![LintViolation::with_fix_and_msg_key(
112 self.code(),
113 format!(
114 "Indentation is not a multiple of {} spaces (found {} spaces).",
115 self.indent_size,
116 text.len()
117 ),
118 t.token.span,
119 vec![SourceEdit::replace(t.token.span, fixed)],
120 "rules.LT02.msg.not_multiple",
121 vec![
122 ("size".to_string(), self.indent_size.to_string()),
123 ("found".to_string(), text.len().to_string()),
124 ],
125 )];
126 }
127
128 vec![]
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::test_utils::lint_sql;
136
137 #[test]
138 fn test_lt02_flags_odd_indent() {
139 let violations = lint_sql("SELECT *\n FROM t", RuleLT02::default());
140 assert_eq!(violations.len(), 1);
141 assert_eq!(violations[0].rule_code, "LT02");
142 assert_eq!(violations[0].fixes.len(), 1);
144 assert_eq!(violations[0].fixes[0].new_text, " ");
145 }
146
147 #[test]
148 fn test_lt02_accepts_4space_indent() {
149 let violations = lint_sql("SELECT *\n FROM t", RuleLT02::default());
150 assert_eq!(violations.len(), 0);
151 }
152
153 #[test]
154 fn test_lt02_flags_mixed_tabs_spaces() {
155 let violations = lint_sql("SELECT *\n\t FROM t", RuleLT02::default());
156 assert_eq!(violations.len(), 1);
157 assert_eq!(violations[0].rule_code, "LT02");
158 assert_eq!(violations[0].fixes.len(), 1);
160 assert_eq!(violations[0].fixes[0].new_text, " ");
161 }
162
163 #[test]
164 fn test_lt02_fix_5_spaces_rounds_to_8() {
165 let violations = lint_sql("SELECT *\n FROM t", RuleLT02::default());
166 assert_eq!(violations.len(), 1);
167 assert_eq!(violations[0].fixes[0].new_text, " ");
169 }
170}