rigsql_rules/layout/
lt09.rs1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
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 return vec![LintViolation::new(
62 self.code(),
63 "Select targets should be on separate lines.",
64 ctx.segment.span(),
65 )];
66 }
67
68 vec![]
69 }
70}