rigsql_rules/layout/
lt04.rs1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6#[derive(Debug)]
10pub struct RuleLT04 {
11 pub style: CommaStyle,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum CommaStyle {
16 Trailing,
17 Leading,
18}
19
20impl Default for RuleLT04 {
21 fn default() -> Self {
22 Self {
23 style: CommaStyle::Trailing,
24 }
25 }
26}
27
28impl Rule for RuleLT04 {
29 fn code(&self) -> &'static str {
30 "LT04"
31 }
32 fn name(&self) -> &'static str {
33 "layout.commas"
34 }
35 fn description(&self) -> &'static str {
36 "Commas should be at the end of the line, not the start."
37 }
38 fn explanation(&self) -> &'static str {
39 "Commas in SELECT lists, GROUP BY, and other clauses should consistently appear \
40 at the end of the line (trailing) or the start of the next line (leading). \
41 Mixing styles reduces readability."
42 }
43 fn groups(&self) -> &[RuleGroup] {
44 &[RuleGroup::Layout]
45 }
46 fn is_fixable(&self) -> bool {
47 true
48 }
49
50 fn configure(&mut self, settings: &std::collections::HashMap<String, String>) {
51 if let Some(val) = settings.get("comma_style") {
52 self.style = match val.as_str() {
53 "leading" => CommaStyle::Leading,
54 _ => CommaStyle::Trailing,
55 };
56 }
57 }
58
59 fn crawl_type(&self) -> CrawlType {
60 CrawlType::Segment(vec![SegmentType::Comma])
61 }
62
63 fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
64 let span = ctx.segment.span();
65
66 match self.style {
67 CommaStyle::Trailing => {
68 if is_leading_comma(ctx) {
69 let fixes = build_leading_to_trailing_fix(ctx);
70 return vec![LintViolation::with_fix(
71 self.code(),
72 "Comma should be at the end of the line, not the start.",
73 span,
74 fixes,
75 )];
76 }
77 }
78 CommaStyle::Leading => {
79 if is_trailing_comma(ctx) {
80 let fixes = build_trailing_to_leading_fix(ctx);
81 return vec![LintViolation::with_fix(
82 self.code(),
83 "Comma should be at the start of the line, not the end.",
84 span,
85 fixes,
86 )];
87 }
88 }
89 }
90
91 vec![]
92 }
93}
94
95fn is_leading_comma(ctx: &RuleContext) -> bool {
97 if ctx.index_in_parent == 0 {
98 return false;
99 }
100 let mut i = ctx.index_in_parent - 1;
102 loop {
103 let seg = &ctx.siblings[i];
104 match seg.segment_type() {
105 SegmentType::Whitespace => {
106 if i == 0 {
107 return false;
108 }
109 i -= 1;
110 }
111 SegmentType::Newline => return true,
112 _ => return false,
113 }
114 }
115}
116
117fn is_trailing_comma(ctx: &RuleContext) -> bool {
119 let mut i = ctx.index_in_parent + 1;
120 while i < ctx.siblings.len() {
121 let seg = &ctx.siblings[i];
122 match seg.segment_type() {
123 SegmentType::Whitespace => {
124 i += 1;
125 }
126 SegmentType::Newline => return true,
127 _ => return false,
128 }
129 }
130 false
131}
132
133fn build_leading_to_trailing_fix(ctx: &RuleContext) -> Vec<SourceEdit> {
140 let comma_span = ctx.segment.span();
141
142 let mut delete_end = comma_span.end;
144 let mut i = ctx.index_in_parent + 1;
145 while i < ctx.siblings.len() {
146 let seg = &ctx.siblings[i];
147 if seg.segment_type() == SegmentType::Whitespace {
148 delete_end = seg.span().end;
149 i += 1;
150 } else {
151 break;
152 }
153 }
154
155 let mut delete_start = comma_span.start;
157 if ctx.index_in_parent > 0 {
158 let mut j = ctx.index_in_parent - 1;
159 loop {
160 let seg = &ctx.siblings[j];
161 if seg.segment_type() == SegmentType::Whitespace {
162 delete_start = seg.span().start;
163 if j == 0 {
164 break;
165 }
166 j -= 1;
167 } else {
168 break;
169 }
170 }
171 }
172
173 let mut insert_pos = comma_span.start;
175 if ctx.index_in_parent > 0 {
176 let mut j = ctx.index_in_parent - 1;
177 loop {
178 let seg = &ctx.siblings[j];
179 match seg.segment_type() {
180 SegmentType::Whitespace | SegmentType::Newline => {
181 if j == 0 {
182 break;
183 }
184 j -= 1;
185 }
186 _ => {
187 insert_pos = seg.span().end;
188 break;
189 }
190 }
191 }
192 }
193
194 let indent_size = (delete_end - comma_span.end) as usize;
197 let original_indent_size = (comma_span.start - delete_start) as usize;
198 let total_indent = original_indent_size + indent_size;
199 let indent = " ".repeat(total_indent);
200
201 vec![
202 SourceEdit::insert(insert_pos, ","),
204 SourceEdit::replace(rigsql_core::Span::new(delete_start, delete_end), indent),
206 ]
207}
208
209fn build_trailing_to_leading_fix(ctx: &RuleContext) -> Vec<SourceEdit> {
211 let comma_span = ctx.segment.span();
212
213 let mut newline_end = comma_span.end;
215 let mut i = ctx.index_in_parent + 1;
216 while i < ctx.siblings.len() {
217 let seg = &ctx.siblings[i];
218 match seg.segment_type() {
219 SegmentType::Whitespace => {
220 i += 1;
221 }
222 SegmentType::Newline => {
223 newline_end = seg.span().end;
224 break;
225 }
226 _ => break,
227 }
228 }
229
230 let insert_pos = if i + 1 < ctx.siblings.len() {
232 ctx.siblings[i + 1].span().start
233 } else {
234 newline_end
235 };
236
237 vec![
238 SourceEdit::delete(comma_span),
240 SourceEdit::insert(insert_pos, ", "),
242 ]
243}