Skip to main content

rigsql_rules/layout/
lt12.rs

1use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
2use crate::violation::{LintViolation, SourceEdit};
3
4/// LT12: Files must end with a single trailing newline.
5#[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        // Check for multiple trailing newlines (handle both \n and \r\n)
53        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}