rigsql_rules/convention/
cv07.rs1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6#[derive(Debug, Default)]
20pub struct RuleCV07;
21
22impl Rule for RuleCV07 {
23 fn code(&self) -> &'static str {
24 "CV07"
25 }
26 fn name(&self) -> &'static str {
27 "convention.prefer_coalesce"
28 }
29 fn description(&self) -> &'static str {
30 "Prefer COALESCE over CASE with IS NULL pattern."
31 }
32 fn explanation(&self) -> &'static str {
33 "A CASE expression like 'CASE WHEN x IS NULL THEN y ELSE x END' can be \
34 simplified to 'COALESCE(x, y)'. COALESCE is more concise and clearly \
35 expresses the intent of providing a fallback value for NULL."
36 }
37 fn groups(&self) -> &[RuleGroup] {
38 &[RuleGroup::Convention]
39 }
40 fn is_fixable(&self) -> bool {
41 false
42 }
43
44 fn crawl_type(&self) -> CrawlType {
45 CrawlType::Segment(vec![SegmentType::CaseExpression])
46 }
47
48 fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
49 let children = ctx.segment.children();
50
51 let non_trivia: Vec<_> = children
58 .iter()
59 .filter(|c| !c.segment_type().is_trivia())
60 .collect();
61
62 if non_trivia.len() < 4 {
65 return vec![];
66 }
67
68 let when_clauses: Vec<_> = non_trivia
70 .iter()
71 .filter(|c| c.segment_type() == SegmentType::WhenClause)
72 .collect();
73
74 if when_clauses.len() != 1 {
75 return vec![];
76 }
77
78 let else_clauses: Vec<_> = non_trivia
80 .iter()
81 .filter(|c| c.segment_type() == SegmentType::ElseClause)
82 .collect();
83
84 if else_clauses.len() != 1 {
85 return vec![];
86 }
87
88 let when_clause = when_clauses[0];
89 let else_clause = else_clauses[0];
90
91 let when_children: Vec<_> = when_clause
94 .children()
95 .iter()
96 .filter(|c| !c.segment_type().is_trivia())
97 .collect();
98
99 let has_is_null = when_children
101 .iter()
102 .any(|c| c.segment_type() == SegmentType::IsNullExpression);
103
104 if !has_is_null {
105 return vec![];
106 }
107
108 let is_null_expr = when_children
110 .iter()
111 .find(|c| c.segment_type() == SegmentType::IsNullExpression);
112
113 let Some(is_null_expr) = is_null_expr else {
114 return vec![];
115 };
116
117 let tested_col = get_is_null_subject(is_null_expr);
118
119 let else_expr = get_else_expression(else_clause);
121
122 if let (Some(tested), Some(else_val)) = (tested_col, else_expr) {
125 if tested.eq_ignore_ascii_case(&else_val) {
126 return vec![LintViolation::new(
127 self.code(),
128 "Use COALESCE instead of CASE WHEN IS NULL pattern.",
129 ctx.segment.span(),
130 )];
131 }
132 }
133
134 vec![]
135 }
136}
137
138fn get_is_null_subject(segment: &Segment) -> Option<String> {
140 let children = segment.children();
141 let non_trivia: Vec<_> = children
142 .iter()
143 .filter(|c| !c.segment_type().is_trivia())
144 .collect();
145
146 non_trivia.first().map(|s| s.raw().trim().to_string())
149}
150
151fn get_else_expression(segment: &Segment) -> Option<String> {
153 let children = segment.children();
154 let non_trivia: Vec<_> = children
155 .iter()
156 .filter(|c| !c.segment_type().is_trivia())
157 .collect();
158
159 if non_trivia.len() >= 2 {
161 let expr_parts: String = non_trivia[1..]
162 .iter()
163 .map(|s| s.raw())
164 .collect::<Vec<_>>()
165 .join("");
166 Some(expr_parts.trim().to_string())
167 } else {
168 None
169 }
170}