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(
101 self.code(),
102 format!("{} clause should start on a new line.", clause_name),
103 child.span(),
104 fix,
105 ));
106 }
107 }
108
109 violations
110 }
111}
112
113fn get_line_indent(source: &str, offset: u32) -> &str {
115 let bytes = source.as_bytes();
116 let pos = offset as usize;
117 let mut line_start = pos;
119 while line_start > 0 && bytes[line_start - 1] != b'\n' {
120 line_start -= 1;
121 }
122 let mut indent_end = line_start;
124 while indent_end < bytes.len() && (bytes[indent_end] == b' ' || bytes[indent_end] == b'\t') {
125 indent_end += 1;
126 }
127 &source[line_start..indent_end]
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::test_utils::lint_sql;
134
135 #[test]
136 fn test_lt14_accepts_newlines_before_clauses() {
137 let violations = lint_sql("SELECT a\nFROM t\nWHERE x = 1", RuleLT14);
138 assert_eq!(violations.len(), 0);
139 }
140
141 #[test]
142 fn test_lt14_flags_inline_clauses() {
143 let violations = lint_sql("SELECT a FROM t WHERE x = 1", RuleLT14);
144 assert!(!violations.is_empty());
145 }
146
147 #[test]
148 fn test_lt14_accepts_single_clause() {
149 let violations = lint_sql("SELECT 1", RuleLT14);
150 assert_eq!(violations.len(), 0);
151 }
152}