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::SegmentBuilder;
5
6use crate::core::config::Value;
7use crate::core::rules::context::RuleContext;
8use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
9use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
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_all();
79
80        let last_content = match children.into_iter().rev().find(|seg| seg.is_code()) {
81            Some(seg) => seg,
82            None => return Vec::new(),
83        };
84
85        let mut fixes = Vec::new();
86
87        if self.select_clause_trailing_comma == "forbid" {
88            if last_content.is_type(SyntaxKind::Comma) {
89                if last_content.get_position_marker().is_none() {
90                    fixes = vec![LintFix::delete(last_content.clone())];
91                } else {
92                    let comma_pos = last_content
93                        .get_position_marker()
94                        .unwrap()
95                        .source_position();
96
97                    for seg in rule_cx.segment.segments() {
98                        if seg.is_type(SyntaxKind::Comma) {
99                            if seg.get_position_marker().is_none() {
100                                continue;
101                            }
102                        } else if seg
103                            .get_position_marker()
104                            .map(|marker| marker.source_position() == comma_pos)
105                            .unwrap_or(false)
106                        {
107                            if seg != &last_content {
108                                break;
109                            }
110                        } else {
111                            fixes = vec![LintFix::delete(last_content.clone())];
112                        }
113                    }
114                }
115
116                return vec![LintResult::new(
117                    Some(last_content),
118                    fixes,
119                    "Trailing comma in select statement forbidden"
120                        .to_owned()
121                        .into(),
122                    None,
123                )];
124            }
125        } else if self.select_clause_trailing_comma == "require"
126            && !last_content.is_type(SyntaxKind::Comma)
127        {
128            let new_comma = SegmentBuilder::comma(rule_cx.tables.next_id());
129
130            let fix: Vec<LintFix> = vec![LintFix::replace(
131                last_content.clone(),
132                vec![last_content.clone(), new_comma],
133                None,
134            )];
135
136            return vec![LintResult::new(
137                Some(last_content),
138                fix,
139                "Trailing comma in select statement required"
140                    .to_owned()
141                    .into(),
142                None,
143            )];
144        }
145        Vec::new()
146    }
147
148    fn crawl_behaviour(&self) -> Crawler {
149        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectClause]) }).into()
150    }
151}