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::{ErasedSegment, SegmentBuilder};
6
7use crate::core::config::Value;
8use crate::core::rules::context::RuleContext;
9use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
10use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
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_all();
60 let select_keyword = child_segments.first().unwrap();
61
62 let Some(select_clause_modifier) = child_segments
64 .iter()
65 .find(|sp| sp.is_type(SyntaxKind::SelectClauseModifier))
66 else {
67 return Vec::new();
68 };
69
70 let leading_newline_segments = child_segments
73 .after(select_keyword)
74 .take_while(|seg| seg.is_whitespace() || seg.is_meta())
75 .filter(|seg: &ErasedSegment| seg.is_type(SyntaxKind::Newline));
76
77 if leading_newline_segments.is_empty() {
80 return Vec::new();
81 }
82 let leading_whitespace_segments = child_segments
85 .after(select_keyword)
86 .take_while(|seg| seg.is_whitespace() || seg.is_meta())
87 .filter(|seg: &ErasedSegment| seg.is_type(SyntaxKind::Whitespace));
88
89 let trailing_newline_segments = child_segments
92 .after(select_clause_modifier)
93 .take_while(|seg| seg.is_whitespace() || seg.is_meta())
94 .filter(|seg: &ErasedSegment| seg.is_type(SyntaxKind::Newline));
95
96 let mut edit_segments = vec![
98 SegmentBuilder::whitespace(context.tables.next_id(), " "),
99 select_clause_modifier.clone(),
100 ];
101
102 if trailing_newline_segments.is_empty() {
103 edit_segments.push(SegmentBuilder::newline(context.tables.next_id(), "\n"));
104 }
105
106 let mut fixes = Vec::new();
107 fixes.push(LintFix::create_after(
109 select_keyword.clone(),
110 edit_segments,
111 None,
112 ));
113
114 if trailing_newline_segments.is_empty() {
115 fixes.extend(leading_newline_segments.into_iter().map(LintFix::delete));
116 } else {
117 let segments = chain(leading_newline_segments, leading_whitespace_segments);
118 fixes.extend(segments.map(LintFix::delete));
119 }
120
121 let trailing_whitespace_segments = child_segments
122 .after(select_clause_modifier)
123 .take_while(|seg| seg.is_type(SyntaxKind::Whitespace) || seg.is_meta())
124 .filter(|segment: &ErasedSegment| segment.is_whitespace());
125
126 if !trailing_whitespace_segments.is_empty() {
127 fixes.extend(
128 trailing_whitespace_segments
129 .into_iter()
130 .map(LintFix::delete),
131 );
132 }
133
134 fixes.push(LintFix::delete(select_clause_modifier.clone()));
136
137 vec![LintResult::new(
138 context.segment.clone().into(),
139 fixes,
140 None,
141 None,
142 )]
143 }
144
145 fn is_fix_compatible(&self) -> bool {
146 true
147 }
148
149 fn crawl_behaviour(&self) -> Crawler {
150 SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectClause]) }).into()
151 }
152}