rigsql_rules/layout/
lt12.rs1use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
2use crate::violation::{LintViolation, SourceEdit};
3
4#[derive(Debug, Default)]
6pub struct RuleLT12;
7
8impl Rule for RuleLT12 {
9 fn code(&self) -> &'static str {
10 "LT12"
11 }
12 fn name(&self) -> &'static str {
13 "layout.end_of_file"
14 }
15 fn description(&self) -> &'static str {
16 "Files must end with a single trailing newline."
17 }
18 fn explanation(&self) -> &'static str {
19 "Files should end with exactly one newline character. Missing trailing newlines \
20 can cause issues with some tools, and multiple trailing newlines are untidy."
21 }
22 fn groups(&self) -> &[RuleGroup] {
23 &[RuleGroup::Layout]
24 }
25 fn is_fixable(&self) -> bool {
26 true
27 }
28
29 fn crawl_type(&self) -> CrawlType {
30 CrawlType::RootOnly
31 }
32
33 fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
34 let source = ctx.source;
35 if source.is_empty() {
36 return vec![];
37 }
38
39 let end = source.len() as u32;
40
41 if !source.ends_with('\n') {
42 return vec![LintViolation::with_fix_and_msg_key(
43 self.code(),
44 "File does not end with a trailing newline.",
45 rigsql_core::Span::new(end, end),
46 vec![SourceEdit::insert(end, "\n")],
47 "rules.LT12.msg.missing",
48 vec![],
49 )];
50 }
51
52 let trimmed = source.trim_end_matches(&['\n', '\r'][..]);
54 let trailing_newlines = source[trimmed.len()..]
55 .bytes()
56 .filter(|&b| b == b'\n')
57 .count();
58 if trailing_newlines > 1 {
59 let span_start = trimmed.len() as u32;
60 return vec![LintViolation::with_fix_and_msg_key(
61 self.code(),
62 format!(
63 "File ends with {} trailing newlines instead of 1.",
64 trailing_newlines
65 ),
66 rigsql_core::Span::new(span_start, end),
67 vec![SourceEdit::replace(
68 rigsql_core::Span::new(span_start, end),
69 "\n",
70 )],
71 "rules.LT12.msg.multiple",
72 vec![("count".to_string(), trailing_newlines.to_string())],
73 )];
74 }
75
76 vec![]
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use crate::test_utils::lint_sql;
84
85 #[test]
86 fn test_lt12_flags_no_trailing_newline() {
87 let violations = lint_sql("SELECT 1", RuleLT12);
88 assert_eq!(violations.len(), 1);
89 }
90
91 #[test]
92 fn test_lt12_accepts_single_trailing_newline() {
93 let violations = lint_sql("SELECT 1\n", RuleLT12);
94 assert_eq!(violations.len(), 0);
95 }
96
97 #[test]
98 fn test_lt12_flags_multiple_trailing_newlines() {
99 let violations = lint_sql("SELECT 1\n\n", RuleLT12);
100 assert_eq!(violations.len(), 1);
101 }
102}