sqruff_lib/rules/convention/
cv03.rs

1use ahash::AHashMap;
2use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
3use sqruff_lib_core::lint_fix::LintFix;
4use sqruff_lib_core::parser::segments::base::{ErasedSegment, SegmentBuilder};
5
6use crate::core::config::Value;
7use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
8use crate::core::rules::context::RuleContext;
9use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
10use crate::utils::functional::context::FunctionalContext;
11
12#[derive(Debug, Clone)]
13pub struct RuleCV03 {
14    select_clause_trailing_comma: String,
15}
16
17impl Default for RuleCV03 {
18    fn default() -> Self {
19        RuleCV03 {
20            select_clause_trailing_comma: "require".to_string(),
21        }
22    }
23}
24
25impl Rule for RuleCV03 {
26    fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
27        Ok(RuleCV03 {
28            select_clause_trailing_comma: _config
29                .get("select_clause_trailing_comma")
30                .unwrap()
31                .as_string()
32                .unwrap()
33                .to_owned(),
34        }
35        .erased())
36    }
37
38    fn name(&self) -> &'static str {
39        "convention.select_trailing_comma"
40    }
41
42    fn description(&self) -> &'static str {
43        "Trailing commas within select clause"
44    }
45
46    fn long_description(&self) -> &'static str {
47        r#"
48**Anti-pattern**
49
50In this example, the last selected column has a trailing comma.
51
52```sql
53SELECT
54    a,
55    b,
56FROM foo
57```
58
59**Best practice**
60
61Remove the trailing comma.
62
63```sql
64SELECT
65    a,
66    b
67FROM foo
68```
69"#
70    }
71
72    fn groups(&self) -> &'static [RuleGroups] {
73        &[RuleGroups::All, RuleGroups::Core, RuleGroups::Convention]
74    }
75
76    fn eval(&self, rule_cx: &RuleContext) -> Vec<LintResult> {
77        let segment = FunctionalContext::new(rule_cx).segment();
78        let children = segment.children(None);
79
80        let last_content: ErasedSegment = children
81            .clone()
82            .last()
83            .cloned()
84            .filter(|sp: &ErasedSegment| sp.is_code())
85            .unwrap();
86
87        let mut fixes = Vec::new();
88
89        if self.select_clause_trailing_comma == "forbid" {
90            if last_content.is_type(SyntaxKind::Comma) {
91                if last_content.get_position_marker().is_none() {
92                    fixes = vec![LintFix::delete(last_content.clone())];
93                } else {
94                    let comma_pos = last_content
95                        .get_position_marker()
96                        .unwrap()
97                        .source_position();
98
99                    for seg in rule_cx.segment.segments() {
100                        if seg.is_type(SyntaxKind::Comma) {
101                            if seg.get_position_marker().is_none() {
102                                continue;
103                            }
104                        } else if seg.get_position_marker().unwrap().source_position() == comma_pos
105                        {
106                            if seg != &last_content {
107                                break;
108                            }
109                        } else {
110                            fixes = vec![LintFix::delete(last_content.clone())];
111                        }
112                    }
113                }
114
115                return vec![LintResult::new(
116                    Some(last_content),
117                    fixes,
118                    "Trailing comma in select statement forbidden"
119                        .to_owned()
120                        .into(),
121                    None,
122                )];
123            }
124        } else if self.select_clause_trailing_comma == "require"
125            && !last_content.is_type(SyntaxKind::Comma)
126        {
127            let new_comma = SegmentBuilder::comma(rule_cx.tables.next_id());
128
129            let fix: Vec<LintFix> = vec![LintFix::replace(
130                last_content.clone(),
131                vec![last_content.clone(), new_comma],
132                None,
133            )];
134
135            return vec![LintResult::new(
136                Some(last_content),
137                fix,
138                "Trailing comma in select statement required"
139                    .to_owned()
140                    .into(),
141                None,
142            )];
143        }
144        Vec::new()
145    }
146
147    fn crawl_behaviour(&self) -> Crawler {
148        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectClause]) }).into()
149    }
150}