rigsql_rules/layout/
lt14.rs1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::utils::has_trailing_newline;
5use crate::violation::{LintViolation, SourceEdit};
6
7#[derive(Debug, Default)]
11pub struct RuleLT14;
12
13const CLAUSE_TYPES: &[SegmentType] = &[
14 SegmentType::FromClause,
15 SegmentType::WhereClause,
16 SegmentType::GroupByClause,
17 SegmentType::HavingClause,
18 SegmentType::OrderByClause,
19 SegmentType::LimitClause,
20];
21
22impl Rule for RuleLT14 {
23 fn code(&self) -> &'static str {
24 "LT14"
25 }
26 fn name(&self) -> &'static str {
27 "layout.keyword_newline"
28 }
29 fn description(&self) -> &'static str {
30 "Keyword clauses should follow a standard for being before/after newlines."
31 }
32 fn explanation(&self) -> &'static str {
33 "Major SQL clauses (FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT) should \
34 start on a new line for readability. Placing them on the same line as the \
35 previous clause makes the query harder to scan."
36 }
37 fn groups(&self) -> &[RuleGroup] {
38 &[RuleGroup::Layout]
39 }
40 fn is_fixable(&self) -> bool {
41 true
42 }
43
44 fn crawl_type(&self) -> CrawlType {
45 CrawlType::Segment(vec![SegmentType::SelectStatement])
46 }
47
48 fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
49 let children = ctx.segment.children();
50 let mut violations = Vec::new();
51
52 for (i, child) in children.iter().enumerate() {
53 if !CLAUSE_TYPES.contains(&child.segment_type()) {
54 continue;
55 }
56
57 let mut found_newline = false;
61 for prev in children[..i].iter().rev() {
62 let st = prev.segment_type();
63 if st == SegmentType::Newline {
64 found_newline = true;
65 break;
66 }
67 if st == SegmentType::Whitespace {
68 continue;
69 }
70 found_newline = has_trailing_newline(prev);
72 break;
73 }
74
75 if !found_newline && i > 0 {
76 let clause_name = match child.segment_type() {
77 SegmentType::FromClause => "FROM",
78 SegmentType::WhereClause => "WHERE",
79 SegmentType::GroupByClause => "GROUP BY",
80 SegmentType::HavingClause => "HAVING",
81 SegmentType::OrderByClause => "ORDER BY",
82 SegmentType::LimitClause => "LIMIT",
83 _ => "Clause",
84 };
85 let indent = get_line_indent(ctx.source, ctx.segment.span().start);
87 let newline_with_indent = format!("\n{}", indent);
88
89 let fix = if i > 0 {
91 let prev = &children[i - 1];
92 if prev.segment_type() == SegmentType::Whitespace {
93 vec![SourceEdit::replace(prev.span(), newline_with_indent)]
94 } else {
95 vec![SourceEdit::insert(child.span().start, &newline_with_indent)]
96 }
97 } else {
98 vec![SourceEdit::insert(child.span().start, &newline_with_indent)]
99 };
100 violations.push(LintViolation::with_fix_and_msg_key(
101 self.code(),
102 format!("{} clause should start on a new line.", clause_name),
103 child.span(),
104 fix,
105 "rules.LT14.msg",
106 vec![("clause".to_string(), clause_name.to_string())],
107 ));
108 }
109 }
110
111 violations
112 }
113}
114
115fn get_line_indent(source: &str, offset: u32) -> &str {
117 let bytes = source.as_bytes();
118 let pos = offset as usize;
119 let mut line_start = pos;
121 while line_start > 0 && bytes[line_start - 1] != b'\n' {
122 line_start -= 1;
123 }
124 let mut indent_end = line_start;
126 while indent_end < bytes.len() && (bytes[indent_end] == b' ' || bytes[indent_end] == b'\t') {
127 indent_end += 1;
128 }
129 &source[line_start..indent_end]
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::test_utils::lint_sql;
136
137 #[test]
138 fn test_lt14_accepts_newlines_before_clauses() {
139 let violations = lint_sql("SELECT a\nFROM t\nWHERE x = 1", RuleLT14);
140 assert_eq!(violations.len(), 0);
141 }
142
143 #[test]
144 fn test_lt14_flags_inline_clauses() {
145 let violations = lint_sql("SELECT a FROM t WHERE x = 1", RuleLT14);
146 assert!(!violations.is_empty());
147 }
148
149 #[test]
150 fn test_lt14_accepts_single_clause() {
151 let violations = lint_sql("SELECT 1", RuleLT14);
152 assert_eq!(violations.len(), 0);
153 }
154}