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::with_msg_key(
74                    self.code(),
75                    "Use CAST(x AS type) instead of :: syntax.",
76                    ctx.segment.span(),
77                    "rules.CV11.msg.cast",
78                    vec![],
79                )]
80            }
81            CastingStyle::DoubleColon if st == SegmentType::CastExpression => {
82                vec![LintViolation::with_msg_key(
83                    self.code(),
84                    "Use :: syntax instead of CAST(x AS type).",
85                    ctx.segment.span(),
86                    "rules.CV11.msg.shorthand",
87                    vec![],
88                )]
89            }
90            _ => vec![],
91        }
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::test_utils::lint_sql;
99
100    #[test]
101    fn test_cv11_accepts_cast_by_default() {
102        let violations = lint_sql("SELECT CAST(x AS INT) FROM t", RuleCV11::default());
103        assert_eq!(violations.len(), 0);
104    }
105
106    #[test]
107    fn test_cv11_double_colon_policy_flags_cast() {
108        let rule = RuleCV11 {
109            preferred_style: CastingStyle::DoubleColon,
110        };
111        let violations = lint_sql("SELECT CAST(x AS INT) FROM t", rule);
112        assert_eq!(violations.len(), 1);
113    }
114}