rigsql_rules/convention/
cv02.rs1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6#[derive(Debug, Default)]
10pub struct RuleCV02;
11
12impl Rule for RuleCV02 {
13 fn code(&self) -> &'static str {
14 "CV02"
15 }
16 fn name(&self) -> &'static str {
17 "convention.coalesce"
18 }
19 fn description(&self) -> &'static str {
20 "Use COALESCE instead of IFNULL or NVL."
21 }
22 fn explanation(&self) -> &'static str {
23 "COALESCE is the ANSI SQL standard function for handling NULL values. \
24 IFNULL (MySQL) and NVL (Oracle) are database-specific alternatives. \
25 Using COALESCE improves portability and consistency."
26 }
27 fn groups(&self) -> &[RuleGroup] {
28 &[RuleGroup::Convention]
29 }
30 fn is_fixable(&self) -> bool {
31 true
32 }
33
34 fn crawl_type(&self) -> CrawlType {
35 CrawlType::Segment(vec![SegmentType::FunctionCall])
36 }
37
38 fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
39 let children = ctx.segment.children();
40
41 let func_name = children.iter().find(|c| !c.segment_type().is_trivia());
43
44 if let Some(Segment::Token(t)) = func_name {
45 let name = t.token.text.as_str();
46 if name.eq_ignore_ascii_case("IFNULL") || name.eq_ignore_ascii_case("NVL") {
47 return vec![LintViolation::new(
48 self.code(),
49 format!("Use COALESCE instead of '{}'.", name),
50 t.token.span,
51 )];
52 }
53 }
54
55 vec![]
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use crate::test_utils::lint_sql;
63
64 #[test]
65 fn test_cv02_flags_ifnull() {
66 let violations = lint_sql("SELECT IFNULL(a, 0) FROM t", RuleCV02);
67 assert_eq!(violations.len(), 1);
68 }
69
70 #[test]
71 fn test_cv02_flags_nvl() {
72 let violations = lint_sql("SELECT NVL(a, 0) FROM t", RuleCV02);
73 assert_eq!(violations.len(), 1);
74 }
75
76 #[test]
77 fn test_cv02_accepts_coalesce() {
78 let violations = lint_sql("SELECT COALESCE(a, 0) FROM t", RuleCV02);
79 assert_eq!(violations.len(), 0);
80 }
81}