Skip to main content

rigsql_rules/layout/
lt15.rs

1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6/// LT15: Too many consecutive blank lines.
7///
8/// Files should not have more than max_blank_lines consecutive blank lines.
9#[derive(Debug)]
10pub struct RuleLT15 {
11    pub max_blank_lines: usize,
12}
13
14impl Default for RuleLT15 {
15    fn default() -> Self {
16        Self { max_blank_lines: 1 }
17    }
18}
19
20impl Rule for RuleLT15 {
21    fn code(&self) -> &'static str {
22        "LT15"
23    }
24    fn name(&self) -> &'static str {
25        "layout.newlines"
26    }
27    fn description(&self) -> &'static str {
28        "Too many consecutive blank lines."
29    }
30    fn explanation(&self) -> &'static str {
31        "Files should not contain too many consecutive blank lines. Multiple \
32         blank lines waste vertical space and reduce readability."
33    }
34    fn groups(&self) -> &[RuleGroup] {
35        &[RuleGroup::Layout]
36    }
37    fn is_fixable(&self) -> bool {
38        true
39    }
40
41    fn configure(&mut self, settings: &std::collections::HashMap<String, String>) {
42        if let Some(val) = settings.get("max_blank_lines") {
43            if let Ok(n) = val.parse() {
44                self.max_blank_lines = n;
45            }
46        }
47    }
48
49    fn crawl_type(&self) -> CrawlType {
50        CrawlType::RootOnly
51    }
52
53    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
54        let mut violations = Vec::new();
55        let mut consecutive_newlines = 0usize;
56        let mut newline_spans: Vec<rigsql_core::Span> = Vec::new();
57
58        let max = self.max_blank_lines;
59        let code = self.code();
60
61        let flush = |consecutive: &mut usize,
62                     spans: &mut Vec<rigsql_core::Span>,
63                     violations: &mut Vec<LintViolation>| {
64            // N consecutive newlines = N-1 blank lines
65            if *consecutive > max + 1 {
66                let keep = max + 1; // keep this many newlines
67                                    // Delete from right after the last kept newline to the end of the
68                                    // last excess newline. This captures any whitespace-only content on
69                                    // blank lines between them, preventing leftover indent.
70                let delete_start = spans[keep - 1].end;
71                let delete_end = spans.last().unwrap().end;
72                let delete_span = rigsql_core::Span::new(delete_start, delete_end);
73                violations.push(LintViolation::with_fix(
74                    code,
75                    format!("Too many blank lines ({}, max {}).", *consecutive - 1, max),
76                    spans[0],
77                    vec![SourceEdit::delete(delete_span)],
78                ));
79            }
80            *consecutive = 0;
81            spans.clear();
82        };
83
84        ctx.root.walk(&mut |seg| {
85            if seg.segment_type() == SegmentType::Newline {
86                consecutive_newlines += 1;
87                newline_spans.push(seg.span());
88            } else if seg.segment_type() == SegmentType::Whitespace {
89                // Whitespace between newlines doesn't reset the count
90            } else if seg.children().is_empty() {
91                flush(
92                    &mut consecutive_newlines,
93                    &mut newline_spans,
94                    &mut violations,
95                );
96            }
97        });
98
99        // Check trailing newlines
100        flush(
101            &mut consecutive_newlines,
102            &mut newline_spans,
103            &mut violations,
104        );
105
106        violations
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::test_utils::lint_sql;
114
115    #[test]
116    fn test_lt15_accepts_single_blank_line() {
117        let violations = lint_sql("SELECT 1;\n\nSELECT 2;\n", RuleLT15::default());
118        assert_eq!(violations.len(), 0);
119    }
120
121    #[test]
122    fn test_lt15_accepts_no_blank_lines() {
123        let violations = lint_sql("SELECT 1;\n", RuleLT15::default());
124        assert_eq!(violations.len(), 0);
125    }
126}