Skip to main content

rigsql_rules/layout/
lt07.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6/// LT07: 'WITH' keyword not followed by single space.
7///
8/// After the WITH keyword there should be exactly one space before the
9/// next non-trivia token (the CTE name).
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.with_spacing"
19    }
20    fn description(&self) -> &'static str {
21        "'WITH' keyword not followed by single space."
22    }
23    fn explanation(&self) -> &'static str {
24        "The WITH keyword in a Common Table Expression should be followed by exactly \
25         one space before the CTE name. Multiple spaces or newlines between WITH and \
26         the CTE name reduce readability."
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
42        // Find the WITH keyword
43        let mut with_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("WITH") {
47                    with_idx = Some(i);
48                    break;
49                }
50            }
51        }
52
53        let Some(with_idx) = with_idx else {
54            return vec![];
55        };
56
57        // Collect all trivia between WITH and the next non-trivia token
58        let mut trivia_start = None;
59        let mut trivia_end = None;
60        let mut raw_trivia = String::new();
61
62        for child in &children[with_idx + 1..] {
63            let st = child.segment_type();
64            if st.is_trivia() {
65                if trivia_start.is_none() {
66                    trivia_start = Some(child.span());
67                }
68                trivia_end = Some(child.span());
69                raw_trivia.push_str(&child.raw());
70            } else {
71                break;
72            }
73        }
74
75        // If the trivia between WITH and next token is exactly one space, it's fine
76        if raw_trivia == " " {
77            return vec![];
78        }
79
80        // If there's no trivia at all, we need to insert a space
81        if trivia_start.is_none() {
82            let with_span = children[with_idx].span();
83            return vec![LintViolation::with_fix(
84                self.code(),
85                "Expected single space after WITH keyword.",
86                with_span,
87                vec![SourceEdit::insert(with_span.end, " ")],
88            )];
89        }
90
91        let start = trivia_start.unwrap();
92        let end = trivia_end.unwrap();
93        let full_span = start.merge(end);
94
95        vec![LintViolation::with_fix(
96            self.code(),
97            "Expected single space after WITH keyword.",
98            full_span,
99            vec![SourceEdit::replace(full_span, " ")],
100        )]
101    }
102}