Skip to main content

rigsql_rules/convention/
cv11.rs

1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// CV11: Enforce consistent type casting style.
7///
8/// By default, prefer CAST(x AS type) over :: syntax.
9#[derive(Debug)]
10pub struct RuleCV11 {
11    pub preferred_style: CastingStyle,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum CastingStyle {
16    /// Prefer CAST(x AS type)
17    Cast,
18    /// Prefer x::type (PostgreSQL)
19    DoubleColon,
20}
21
22impl Default for RuleCV11 {
23    fn default() -> Self {
24        Self {
25            preferred_style: CastingStyle::Cast,
26        }
27    }
28}
29
30impl Rule for RuleCV11 {
31    fn code(&self) -> &'static str {
32        "CV11"
33    }
34    fn name(&self) -> &'static str {
35        "convention.casting_style"
36    }
37    fn description(&self) -> &'static str {
38        "Enforce consistent type casting style."
39    }
40    fn explanation(&self) -> &'static str {
41        "SQL has multiple ways to cast types: CAST(x AS type), x::type, and CONVERT(). \
42         Using a consistent style improves readability. By default, the ANSI CAST() \
43         syntax is preferred."
44    }
45    fn groups(&self) -> &[RuleGroup] {
46        &[RuleGroup::Convention]
47    }
48    fn is_fixable(&self) -> bool {
49        false
50    }
51
52    fn configure(&mut self, settings: &std::collections::HashMap<String, String>) {
53        if let Some(val) = settings.get("preferred_type_casting_style") {
54            self.preferred_style = match val.as_str() {
55                "::" | "shorthand" => CastingStyle::DoubleColon,
56                _ => CastingStyle::Cast,
57            };
58        }
59    }
60
61    fn crawl_type(&self) -> CrawlType {
62        CrawlType::Segment(vec![
63            SegmentType::CastExpression,
64            SegmentType::TypeCastExpression,
65        ])
66    }
67
68    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
69        let st = ctx.segment.segment_type();
70
71        match self.preferred_style {
72            CastingStyle::Cast if st == SegmentType::TypeCastExpression => {
73                vec![LintViolation::new(
74                    self.code(),
75                    "Use CAST(x AS type) instead of :: syntax.",
76                    ctx.segment.span(),
77                )]
78            }
79            CastingStyle::DoubleColon if st == SegmentType::CastExpression => {
80                vec![LintViolation::new(
81                    self.code(),
82                    "Use :: syntax instead of CAST(x AS type).",
83                    ctx.segment.span(),
84                )]
85            }
86            _ => vec![],
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use crate::test_utils::lint_sql;
95
96    #[test]
97    fn test_cv11_accepts_cast_by_default() {
98        let violations = lint_sql("SELECT CAST(x AS INT) FROM t", RuleCV11::default());
99        assert_eq!(violations.len(), 0);
100    }
101
102    #[test]
103    fn test_cv11_double_colon_policy_flags_cast() {
104        let rule = RuleCV11 {
105            preferred_style: CastingStyle::DoubleColon,
106        };
107        let violations = lint_sql("SELECT CAST(x AS INT) FROM t", rule);
108        assert_eq!(violations.len(), 1);
109    }
110}