rigsql_rules/layout/
lt08.rs1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6#[derive(Debug, Default)]
11pub struct RuleLT08;
12
13impl Rule for RuleLT08 {
14 fn code(&self) -> &'static str {
15 "LT08"
16 }
17 fn name(&self) -> &'static str {
18 "layout.cte_newline"
19 }
20 fn description(&self) -> &'static str {
21 "Blank line expected but not found before CTE definition."
22 }
23 fn explanation(&self) -> &'static str {
24 "When a WITH clause contains multiple CTEs, each CTE after the first should \
25 be separated by a blank line to improve readability. A single newline between \
26 CTEs makes it harder to distinguish where one ends and the next begins."
27 }
28 fn groups(&self) -> &[RuleGroup] {
29 &[RuleGroup::Layout]
30 }
31 fn is_fixable(&self) -> bool {
32 true
33 }
34
35 fn crawl_type(&self) -> CrawlType {
36 CrawlType::Segment(vec![SegmentType::WithClause])
37 }
38
39 fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
40 let children = ctx.segment.children();
41 let mut violations = Vec::new();
42 let mut cte_count = 0;
43
44 for (i, child) in children.iter().enumerate() {
45 if child.segment_type() != SegmentType::CteDefinition {
46 continue;
47 }
48 cte_count += 1;
49 if cte_count <= 1 {
50 continue;
51 }
52
53 let (newline_count, insert_offset) = scan_trivia_before(children, i);
55
56 if newline_count < 2 {
57 violations.push(LintViolation::with_fix(
58 self.code(),
59 "Expected blank line before CTE definition.",
60 child.span(),
61 vec![SourceEdit::insert(insert_offset, "\n")],
62 ));
63 }
64 }
65
66 violations
67 }
68}
69
70fn scan_trivia_before(children: &[rigsql_core::Segment], cte_idx: usize) -> (usize, u32) {
73 let mut newline_count = 0;
74 let mut last_newline_end = children[cte_idx].span().start;
75
76 for child in children[..cte_idx].iter().rev() {
77 let st = child.segment_type();
78 if st == SegmentType::Newline {
79 newline_count += 1;
80 if newline_count == 1 {
81 last_newline_end = child.span().end;
82 }
83 } else if st.is_trivia() {
84 continue;
85 } else {
86 break;
87 }
88 }
89
90 (newline_count, last_newline_end)
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::test_utils::lint_sql;
97
98 #[test]
99 fn test_lt08_accepts_single_cte() {
100 let violations = lint_sql("WITH cte AS (SELECT 1) SELECT * FROM cte", RuleLT08);
101 assert_eq!(violations.len(), 0);
102 }
103}