Skip to main content

rigsql_rules/layout/
lt10.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// LT10: SELECT modifiers (DISTINCT, ALL) must be on same line as SELECT.
7///
8/// When using SELECT DISTINCT or SELECT ALL, the modifier should appear on
9/// the same line as the SELECT keyword, with no intervening newline.
10#[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        // Find the SELECT keyword
43        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        // Look at tokens after SELECT for DISTINCT or ALL
58        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                // Whether it was a modifier or not, stop looking
80                break;
81            } else {
82                break;
83            }
84        }
85
86        vec![]
87    }
88}