rigsql_rules/layout/
lt10.rs1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6#[derive(Debug, Default)]
11pub struct RuleLT10;
12
13impl Rule for RuleLT10 {
14 fn code(&self) -> &'static str {
15 "LT10"
16 }
17 fn name(&self) -> &'static str {
18 "layout.select_modifier"
19 }
20 fn description(&self) -> &'static str {
21 "SELECT modifiers (DISTINCT, ALL) must be on same line as SELECT."
22 }
23 fn explanation(&self) -> &'static str {
24 "SELECT modifiers such as DISTINCT or ALL should appear on the same line as \
25 the SELECT keyword. Placing them on a separate line is confusing and reduces \
26 readability."
27 }
28 fn groups(&self) -> &[RuleGroup] {
29 &[RuleGroup::Layout]
30 }
31 fn is_fixable(&self) -> bool {
32 false
33 }
34
35 fn crawl_type(&self) -> CrawlType {
36 CrawlType::Segment(vec![SegmentType::SelectClause])
37 }
38
39 fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
40 let children = ctx.segment.children();
41
42 let mut select_idx = None;
44 for (i, child) in children.iter().enumerate() {
45 if let Segment::Token(t) = child {
46 if t.token.text.as_str().eq_ignore_ascii_case("SELECT") {
47 select_idx = Some(i);
48 break;
49 }
50 }
51 }
52
53 let Some(select_idx) = select_idx else {
54 return vec![];
55 };
56
57 let mut has_newline = false;
59 for child in &children[select_idx + 1..] {
60 let st = child.segment_type();
61 if st == SegmentType::Newline {
62 has_newline = true;
63 } else if st.is_trivia() {
64 continue;
65 } else if let Segment::Token(t) = child {
66 let text = t.token.text.as_str();
67 if (text.eq_ignore_ascii_case("DISTINCT") || text.eq_ignore_ascii_case("ALL"))
68 && has_newline
69 {
70 return vec![LintViolation::new(
71 self.code(),
72 format!(
73 "'{}' must be on the same line as SELECT.",
74 text.to_uppercase()
75 ),
76 t.token.span,
77 )];
78 }
79 break;
81 } else {
82 break;
83 }
84 }
85
86 vec![]
87 }
88}