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::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}