rigsql_rules/layout/
lt15.rs1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6#[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 if *consecutive > max + 1 {
66 let keep = max + 1; 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 } else if seg.children().is_empty() {
96 flush(
97 &mut consecutive_newlines,
98 &mut newline_spans,
99 &mut violations,
100 );
101 }
102 });
103
104 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}