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