rigsql_rules/layout/
lt01.rs1use rigsql_core::{Segment, SegmentType, TokenKind};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6#[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 if ctx.index_in_parent == 0 {
49 return vec![];
50 }
51
52 let prev = &ctx.siblings[ctx.index_in_parent - 1];
53 if prev.segment_type() == SegmentType::Newline {
55 return vec![];
56 }
57
58 let text = t.token.text.as_str();
59
60 let is_trailing = if ctx.index_in_parent + 1 < ctx.siblings.len() {
64 ctx.siblings[ctx.index_in_parent + 1].segment_type() == SegmentType::Newline
65 } else {
66 false
67 };
68
69 if is_trailing {
70 return vec![LintViolation::with_fix_and_msg_key(
71 self.code(),
72 "Trailing whitespace.",
73 t.token.span,
74 vec![SourceEdit::delete(t.token.span)],
75 "rules.LT01.msg.trailing",
76 vec![],
77 )];
78 }
79
80 if text.len() > 1 {
82 return vec![LintViolation::with_fix_and_msg_key(
83 self.code(),
84 format!("Expected single space, found {} spaces.", text.len()),
85 t.token.span,
86 vec![SourceEdit::replace(t.token.span, " ")],
87 "rules.LT01.msg",
88 vec![("count".to_string(), text.len().to_string())],
89 )];
90 }
91
92 vec![]
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::test_utils::lint_sql;
100
101 #[test]
102 fn test_lt01_flags_double_space() {
103 let violations = lint_sql("SELECT * FROM t", RuleLT01);
104 assert!(!violations.is_empty());
105 assert!(violations.iter().all(|v| v.rule_code == "LT01"));
106 }
107
108 #[test]
109 fn test_lt01_accepts_single_space() {
110 let violations = lint_sql("SELECT * FROM t", RuleLT01);
111 assert_eq!(violations.len(), 0);
112 }
113
114 #[test]
115 fn test_lt01_skips_indentation() {
116 let violations = lint_sql("SELECT *\n FROM t", RuleLT01);
117 assert_eq!(violations.len(), 0);
118 }
119
120 #[test]
121 fn test_lt01_trailing_whitespace_removed() {
122 let violations = lint_sql("SELECT * \n FROM t", RuleLT01);
123 assert_eq!(violations.len(), 1);
124 assert_eq!(violations[0].message, "Trailing whitespace.");
125 assert!(violations[0].fixes[0].new_text.is_empty());
126 }
127
128 #[test]
129 fn test_lt01_single_trailing_space() {
130 let violations = lint_sql("SELECT * \nFROM t", RuleLT01);
131 assert_eq!(violations.len(), 1);
132 assert_eq!(violations[0].message, "Trailing whitespace.");
133 assert!(violations[0].fixes[0].new_text.is_empty());
134 }
135}