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::{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        // Get children of select_clause and the corresponding select keyword.
59        let child_segments = FunctionalContext::new(context).segment().children_all();
60        let select_keyword = child_segments.first().unwrap();
61
62        // See if we have a select_clause_modifier.
63        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        // Are there any newlines between the select keyword and the select clause
71        // modifier.
72        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        // Rule doesn't apply if select clause modifier is already on the same line as
78        // the select keyword.
79        if leading_newline_segments.is_empty() {
80            return Vec::new();
81        }
82        // We should check if there is whitespace before the select clause modifier and
83        // remove this during the lint fix.
84        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        // We should also check if the following select clause element
90        // is on the same line as the select clause modifier.
91        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        // We will insert these segments directly after the select keyword.
97        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        // Move select clause modifier after select keyword.
108        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        // Delete the original select clause modifier.
135        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}