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_and_msg_key(
74                    code,
75                    format!("Too many blank lines ({}, max {}).", *consecutive - 1, max),
76                    spans[0],
77                    vec![SourceEdit::delete(delete_span)],
78                    "rules.LT15.msg",
79                    vec![
80                        ("count".to_string(), (*consecutive - 1).to_string()),
81                        ("max".to_string(), max.to_string()),
82                    ],
83                ));
84            }
85            *consecutive = 0;
86            spans.clear();
87        };
88
89        ctx.root.walk(&mut |seg| {
90            if seg.segment_type() == SegmentType::Newline {
91                consecutive_newlines += 1;
92                newline_spans.push(seg.span());
93            } else if seg.segment_type() == SegmentType::Whitespace {
94                // Whitespace between newlines doesn't reset the count
95            } else if seg.children().is_empty() {
96                flush(
97                    &mut consecutive_newlines,
98                    &mut newline_spans,
99                    &mut violations,
100                );
101            }
102        });
103
104        // Check trailing newlines
105        flush(
106            &mut consecutive_newlines,
107            &mut newline_spans,
108            &mut violations,
109        );
110
111        violations
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::test_utils::lint_sql;
119
120    #[test]
121    fn test_lt15_accepts_single_blank_line() {
122        let violations = lint_sql("SELECT 1;\n\nSELECT 2;\n", RuleLT15::default());
123        assert_eq!(violations.len(), 0);
124    }
125
126    #[test]
127    fn test_lt15_accepts_no_blank_lines() {
128        let violations = lint_sql("SELECT 1;\n", RuleLT15::default());
129        assert_eq!(violations.len(), 0);
130    }
131}