rigsql_rules/layout/
lt09.rs1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6#[derive(Debug, Default)]
8pub struct RuleLT09;
9
10impl Rule for RuleLT09 {
11 fn code(&self) -> &'static str {
12 "LT09"
13 }
14 fn name(&self) -> &'static str {
15 "layout.select_targets"
16 }
17 fn description(&self) -> &'static str {
18 "Select targets should be on a new line unless there is only one."
19 }
20 fn explanation(&self) -> &'static str {
21 "When a SELECT has multiple columns, each column should be on its own line. \
22 This makes diffs cleaner and improves readability. A single column can stay \
23 on the same line as SELECT."
24 }
25 fn groups(&self) -> &[RuleGroup] {
26 &[RuleGroup::Layout]
27 }
28 fn is_fixable(&self) -> bool {
29 true
30 }
31
32 fn crawl_type(&self) -> CrawlType {
33 CrawlType::Segment(vec![SegmentType::SelectClause])
34 }
35
36 fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
37 let children = ctx.segment.children();
38
39 let targets: Vec<_> = children
42 .iter()
43 .filter(|c| {
44 let st = c.segment_type();
45 !st.is_trivia() && st != SegmentType::Keyword && st != SegmentType::Comma
46 })
47 .collect();
48
49 if targets.len() <= 1 {
51 return vec![];
52 }
53
54 let has_newline_between_targets = children
57 .iter()
58 .any(|c| c.segment_type() == SegmentType::Newline);
59
60 if !has_newline_between_targets {
61 let mut fixes = Vec::new();
63 let indent = " ";
64 for (i, child) in children.iter().enumerate() {
65 if child.segment_type() == SegmentType::Keyword && i + 1 < children.len() {
67 let next = &children[i + 1];
68 if next.segment_type() == SegmentType::Whitespace {
69 fixes.push(SourceEdit::replace(next.span(), format!("\n{}", indent)));
70 }
71 }
72 if child.segment_type() == SegmentType::Comma && i + 1 < children.len() {
74 let next = &children[i + 1];
75 if next.segment_type() == SegmentType::Whitespace {
76 fixes.push(SourceEdit::replace(next.span(), format!("\n{}", indent)));
77 } else {
78 fixes.push(SourceEdit::insert(
79 child.span().end,
80 format!("\n{}", indent),
81 ));
82 }
83 }
84 }
85
86 return vec![LintViolation::with_fix_and_msg_key(
87 self.code(),
88 "Select targets should be on separate lines.",
89 ctx.segment.span(),
90 fixes,
91 "rules.LT09.msg",
92 vec![],
93 )];
94 }
95
96 vec![]
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::test_utils::lint_sql;
104
105 #[test]
106 fn test_lt09_flags_multiple_targets_single_line() {
107 let violations = lint_sql("SELECT a, b, c FROM t", RuleLT09);
108 assert_eq!(violations.len(), 1);
109 assert_eq!(violations[0].rule_code, "LT09");
110 assert!(!violations[0].fixes.is_empty());
112 }
113
114 #[test]
115 fn test_lt09_accepts_single_target() {
116 let violations = lint_sql("SELECT a FROM t", RuleLT09);
117 assert_eq!(violations.len(), 0);
118 }
119}