sqruff_lib/rules/layout/
lt12.rs

1use ahash::AHashMap;
2use sqruff_lib_core::dialects::syntax::SyntaxKind;
3use sqruff_lib_core::lint_fix::LintFix;
4use sqruff_lib_core::parser::segments::base::{ErasedSegment, SegmentBuilder};
5use sqruff_lib_core::utils::functional::segments::Segments;
6
7use crate::core::config::Value;
8use crate::core::rules::base::{Erased, ErasedRule, LintPhase, LintResult, Rule, RuleGroups};
9use crate::core::rules::context::RuleContext;
10use crate::core::rules::crawlers::{Crawler, RootOnlyCrawler};
11use crate::utils::functional::context::FunctionalContext;
12
13fn get_trailing_newlines(segment: &ErasedSegment) -> Vec<ErasedSegment> {
14    let mut result = Vec::new();
15
16    for seg in segment.recursive_crawl_all(true) {
17        if seg.is_type(SyntaxKind::Newline) {
18            result.push(seg.clone());
19        } else if !seg.is_whitespace()
20            && !seg.is_type(SyntaxKind::Dedent)
21            && !seg.is_type(SyntaxKind::EndOfFile)
22        {
23            break;
24        }
25    }
26
27    result
28}
29
30fn get_last_segment(mut segment: Segments) -> (Vec<ErasedSegment>, Segments) {
31    let mut parent_stack = Vec::new();
32
33    loop {
34        let children = segment.children(None);
35
36        if !children.is_empty() {
37            parent_stack.push(segment.first().unwrap().clone());
38            segment = children.find_last(Some(|s| !s.is_type(SyntaxKind::EndOfFile)));
39        } else {
40            return (parent_stack, segment);
41        }
42    }
43}
44
45#[derive(Debug, Default, Clone)]
46pub struct RuleLT12;
47
48impl Rule for RuleLT12 {
49    fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
50        Ok(RuleLT12.erased())
51    }
52    fn lint_phase(&self) -> LintPhase {
53        LintPhase::Post
54    }
55
56    fn name(&self) -> &'static str {
57        "layout.end_of_file"
58    }
59
60    fn description(&self) -> &'static str {
61        "Files must end with a single trailing newline."
62    }
63
64    fn long_description(&self) -> &'static str {
65        r#"
66**Anti-pattern**
67
68The content in file does not end with a single trailing newline. The $ represents end of file.
69
70```sql
71 SELECT
72     a
73 FROM foo$
74
75 -- Ending on an indented line means there is no newline
76 -- at the end of the file, the • represents space.
77
78 SELECT
79 ••••a
80 FROM
81 ••••foo
82 ••••$
83
84 -- Ending on a semi-colon means the last line is not a
85 -- newline.
86
87 SELECT
88     a
89 FROM foo
90 ;$
91
92 -- Ending with multiple newlines.
93
94 SELECT
95     a
96 FROM foo
97
98 $
99```
100
101**Best practice**
102
103Add trailing newline to the end. The $ character represents end of file.
104
105```sql
106 SELECT
107     a
108 FROM foo
109 $
110
111 -- Ensuring the last line is not indented so is just a
112 -- newline.
113
114 SELECT
115 ••••a
116 FROM
117 ••••foo
118 $
119
120 -- Even when ending on a semi-colon, ensure there is a
121 -- newline after.
122
123 SELECT
124     a
125 FROM foo
126 ;
127 $
128```
129"#
130    }
131    fn groups(&self) -> &'static [RuleGroups] {
132        &[RuleGroups::All, RuleGroups::Core, RuleGroups::Layout]
133    }
134
135    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
136        let (parent_stack, segment) = get_last_segment(FunctionalContext::new(context).segment());
137
138        if segment.is_empty() {
139            return Vec::new();
140        }
141
142        let trailing_newlines = Segments::from_vec(get_trailing_newlines(&context.segment), None);
143        if trailing_newlines.is_empty() {
144            let fix_anchor_segment = if parent_stack.len() == 1 {
145                segment.first().unwrap().clone()
146            } else {
147                parent_stack[1].clone()
148            };
149
150            vec![LintResult::new(
151                segment.first().unwrap().clone().into(),
152                vec![LintFix::create_after(
153                    fix_anchor_segment,
154                    vec![SegmentBuilder::newline(context.tables.next_id(), "\n")],
155                    None,
156                )],
157                None,
158                None,
159            )]
160        } else if trailing_newlines.len() > 1 {
161            vec![LintResult::new(
162                segment.first().unwrap().clone().into(),
163                trailing_newlines
164                    .into_iter()
165                    .skip(1)
166                    .map(|d| LintFix::delete(d.clone()))
167                    .collect(),
168                None,
169                None,
170            )]
171        } else {
172            vec![]
173        }
174    }
175
176    fn is_fix_compatible(&self) -> bool {
177        true
178    }
179
180    fn crawl_behaviour(&self) -> Crawler {
181        RootOnlyCrawler.into()
182    }
183}