sqruff_lib/rules/convention/
cv03.rs1use 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}