sqruff_lib/rules/layout/
lt10.rs1use ahash::AHashMap;
2use itertools::chain;
3use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
4use sqruff_lib_core::lint_fix::LintFix;
5use sqruff_lib_core::parser::segments::base::{ErasedSegment, SegmentBuilder};
6
7use crate::core::config::Value;
8use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
9use crate::core::rules::context::RuleContext;
10use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
11use crate::utils::functional::context::FunctionalContext;
12
13#[derive(Debug, Default, Clone)]
14pub struct RuleLT10;
15
16impl Rule for RuleLT10 {
17 fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
18 Ok(RuleLT10.erased())
19 }
20 fn name(&self) -> &'static str {
21 "layout.select_modifiers"
22 }
23
24 fn description(&self) -> &'static str {
25 "'SELECT' modifiers (e.g. 'DISTINCT') must be on the same line as 'SELECT'."
26 }
27
28 fn long_description(&self) -> &'static str {
29 r#"
30**Anti-pattern**
31
32In this example, the `DISTINCT` modifier is on the next line after the `SELECT` keyword.
33
34```sql
35select
36 distinct a,
37 b
38from x
39```
40
41**Best practice**
42
43Move the `DISTINCT` modifier to the same line as the `SELECT` keyword.
44
45```sql
46select distinct
47 a,
48 b
49from x
50```
51"#
52 }
53
54 fn groups(&self) -> &'static [RuleGroups] {
55 &[RuleGroups::All, RuleGroups::Core, RuleGroups::Layout]
56 }
57 fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
58 let child_segments = FunctionalContext::new(context).segment().children(None);
60 let select_keyword = child_segments.first().unwrap();
61
62 let select_clause_modifier_seg = child_segments.find_first(Some(|sp: &ErasedSegment| {
64 sp.is_type(SyntaxKind::SelectClauseModifier)
65 }));
66
67 if select_clause_modifier_seg.is_empty() {
69 return Vec::new();
70 }
71
72 let leading_newline_segments = child_segments.select(
75 Some(|seg: &ErasedSegment| seg.is_type(SyntaxKind::Newline)),
76 Some(|seg| seg.is_whitespace() || seg.is_meta()),
77 select_keyword.into(),
78 None,
79 );
80
81 if leading_newline_segments.is_empty() {
84 return Vec::new();
85 }
86
87 let select_clause_modifier = select_clause_modifier_seg.first().unwrap();
88
89 let leading_whitespace_segments = child_segments.select(
92 Some(|seg: &ErasedSegment| seg.is_type(SyntaxKind::Whitespace)),
93 Some(|seg| seg.is_whitespace() || seg.is_meta()),
94 select_keyword.into(),
95 None,
96 );
97
98 let trailing_newline_segments = child_segments.select(
101 Some(|seg: &ErasedSegment| seg.is_type(SyntaxKind::Newline)),
102 Some(|seg| seg.is_whitespace() || seg.is_meta()),
103 select_clause_modifier.into(),
104 None,
105 );
106
107 let mut edit_segments = vec![
109 SegmentBuilder::whitespace(context.tables.next_id(), " "),
110 select_clause_modifier.clone(),
111 ];
112
113 if trailing_newline_segments.is_empty() {
114 edit_segments.push(SegmentBuilder::newline(context.tables.next_id(), "\n"));
115 }
116
117 let mut fixes = Vec::new();
118 fixes.push(LintFix::create_after(
120 select_keyword.clone(),
121 edit_segments,
122 None,
123 ));
124
125 if trailing_newline_segments.is_empty() {
126 fixes.extend(leading_newline_segments.into_iter().map(LintFix::delete));
127 } else {
128 let segments = chain(leading_newline_segments, leading_whitespace_segments);
129 fixes.extend(segments.map(LintFix::delete));
130 }
131
132 let trailing_whitespace_segments = child_segments.select(
133 Some(|segment: &ErasedSegment| segment.is_whitespace()),
134 Some(|seg| seg.is_type(SyntaxKind::Whitespace) || seg.is_meta()),
135 select_clause_modifier.into(),
136 None,
137 );
138
139 if !trailing_whitespace_segments.is_empty() {
140 fixes.extend(
141 trailing_whitespace_segments
142 .into_iter()
143 .map(LintFix::delete),
144 );
145 }
146
147 fixes.push(LintFix::delete(select_clause_modifier.clone()));
149
150 vec![LintResult::new(
151 context.segment.clone().into(),
152 fixes,
153 None,
154 None,
155 )]
156 }
157
158 fn is_fix_compatible(&self) -> bool {
159 true
160 }
161
162 fn crawl_behaviour(&self) -> Crawler {
163 SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectClause]) }).into()
164 }
165}