Skip to main content

rigsql_rules/layout/
lt07.rs

1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::utils::has_trailing_newline;
5use crate::violation::LintViolation;
6
7/// LT07: WITH clause closing bracket should be on a new line.
8///
9/// The closing parenthesis of a CTE definition should be on its own line.
10#[derive(Debug, Default)]
11pub struct RuleLT07;
12
13impl Rule for RuleLT07 {
14    fn code(&self) -> &'static str {
15        "LT07"
16    }
17    fn name(&self) -> &'static str {
18        "layout.cte_bracket"
19    }
20    fn description(&self) -> &'static str {
21        "WITH clause closing bracket should be on a new line."
22    }
23    fn explanation(&self) -> &'static str {
24        "The closing parenthesis of a CTE definition should be placed on its \
25         own line, not on the same line as the last expression in the CTE body."
26    }
27    fn groups(&self) -> &[RuleGroup] {
28        &[RuleGroup::Layout]
29    }
30    fn is_fixable(&self) -> bool {
31        false
32    }
33
34    fn crawl_type(&self) -> CrawlType {
35        CrawlType::Segment(vec![SegmentType::CteDefinition])
36    }
37
38    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
39        let children = ctx.segment.children();
40
41        // The RParen may be a direct child or nested inside a Subquery node.
42        let (search_children, rparen_idx) = if let Some(idx) = children
43            .iter()
44            .rposition(|c| c.segment_type() == SegmentType::RParen)
45        {
46            (children, idx)
47        } else {
48            // Look inside a Subquery child
49            let subquery = children
50                .iter()
51                .find(|c| c.segment_type() == SegmentType::Subquery);
52            let Some(sq) = subquery else {
53                return vec![];
54            };
55            let sq_children = sq.children();
56            let Some(idx) = sq_children
57                .iter()
58                .rposition(|c| c.segment_type() == SegmentType::RParen)
59            else {
60                return vec![];
61            };
62            (sq_children, idx)
63        };
64
65        // Look backwards from RParen for a Newline (only whitespace allowed between).
66        // Newlines may be absorbed as trailing trivia of the previous segment.
67        let mut found_newline = false;
68        for child in search_children[..rparen_idx].iter().rev() {
69            let st = child.segment_type();
70            if st == SegmentType::Newline {
71                found_newline = true;
72                break;
73            }
74            if st == SegmentType::Whitespace {
75                continue;
76            }
77            // Check if this segment ends with a trailing Newline
78            found_newline = has_trailing_newline(child);
79            break;
80        }
81
82        if !found_newline {
83            return vec![LintViolation::new(
84                self.code(),
85                "Closing bracket of CTE should be on a new line.",
86                search_children[rparen_idx].span(),
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_lt07_accepts_newline_before_bracket() {
101        let violations = lint_sql("WITH cte AS (\n  SELECT 1\n) SELECT * FROM cte", RuleLT07);
102        assert_eq!(violations.len(), 0);
103    }
104
105    #[test]
106    fn test_lt07_flags_inline_bracket() {
107        let violations = lint_sql("WITH cte AS (SELECT 1) SELECT * FROM cte", RuleLT07);
108        assert_eq!(violations.len(), 1);
109    }
110}