sqruff_lib/rules/layout/
lt10.rs

1use 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        // Get children of select_clause and the corresponding select keyword.
59        let child_segments = FunctionalContext::new(context).segment().children(None);
60        let select_keyword = child_segments.first().unwrap();
61
62        // See if we have a select_clause_modifier.
63        let select_clause_modifier_seg = child_segments.find_first(Some(|sp: &ErasedSegment| {
64            sp.is_type(SyntaxKind::SelectClauseModifier)
65        }));
66
67        // Rule doesn't apply if there's no select clause modifier.
68        if select_clause_modifier_seg.is_empty() {
69            return Vec::new();
70        }
71
72        // Are there any newlines between the select keyword and the select clause
73        // modifier.
74        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        // Rule doesn't apply if select clause modifier is already on the same line as
82        // the select keyword.
83        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        // We should check if there is whitespace before the select clause modifier and
90        // remove this during the lint fix.
91        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        // We should also check if the following select clause element
99        // is on the same line as the select clause modifier.
100        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        // We will insert these segments directly after the select keyword.
108        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        // Move select clause modifier after select keyword.
119        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        // Delete the original select clause modifier.
148        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}