Skip to main content

rigsql_rules/convention/
cv01.rs

1use rigsql_core::{Segment, SegmentType, TokenKind};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6/// CV01: Use consistent not-equal operator.
7///
8/// By default, prefer `!=` over `<>`.
9#[derive(Debug)]
10pub struct RuleCV01 {
11    pub preferred: NotEqualStyle,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum NotEqualStyle {
16    /// Prefer `!=`
17    CStyle,
18    /// Prefer `<>`
19    AnsiStyle,
20}
21
22impl Default for RuleCV01 {
23    fn default() -> Self {
24        Self {
25            preferred: NotEqualStyle::CStyle,
26        }
27    }
28}
29
30impl Rule for RuleCV01 {
31    fn code(&self) -> &'static str {
32        "CV01"
33    }
34    fn name(&self) -> &'static str {
35        "convention.not_equal"
36    }
37    fn description(&self) -> &'static str {
38        "Consistent not-equal operator."
39    }
40    fn explanation(&self) -> &'static str {
41        "SQL has two not-equal operators: '!=' and '<>'. Using one consistently \
42         improves readability. The ANSI standard uses '<>' but '!=' is more common \
43         in modern SQL and programming."
44    }
45    fn groups(&self) -> &[RuleGroup] {
46        &[RuleGroup::Convention]
47    }
48    fn is_fixable(&self) -> bool {
49        true
50    }
51
52    fn configure(&mut self, settings: &std::collections::HashMap<String, String>) {
53        if let Some(val) = settings.get("preferred_not_equal") {
54            self.preferred = match val.as_str() {
55                "ansi" | "<>" => NotEqualStyle::AnsiStyle,
56                _ => NotEqualStyle::CStyle,
57            };
58        }
59    }
60
61    fn crawl_type(&self) -> CrawlType {
62        CrawlType::Segment(vec![SegmentType::ComparisonOperator])
63    }
64
65    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
66        let Segment::Token(t) = ctx.segment else {
67            return vec![];
68        };
69        if t.token.kind != TokenKind::Neq {
70            return vec![];
71        }
72
73        let text = t.token.text.as_str();
74        match self.preferred {
75            NotEqualStyle::CStyle if text == "<>" => {
76                vec![LintViolation::with_fix(
77                    self.code(),
78                    "Use '!=' instead of '<>'.",
79                    t.token.span,
80                    vec![SourceEdit::replace(t.token.span, "!=")],
81                )]
82            }
83            NotEqualStyle::AnsiStyle if text == "!=" => {
84                vec![LintViolation::with_fix(
85                    self.code(),
86                    "Use '<>' instead of '!='.",
87                    t.token.span,
88                    vec![SourceEdit::replace(t.token.span, "<>")],
89                )]
90            }
91            _ => vec![],
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::test_utils::lint_sql;
100
101    #[test]
102    fn test_cv01_flags_ansi_neq() {
103        let violations = lint_sql("SELECT * FROM t WHERE a <> b", RuleCV01::default());
104        assert_eq!(violations.len(), 1);
105    }
106
107    #[test]
108    fn test_cv01_accepts_cstyle_neq() {
109        let violations = lint_sql("SELECT * FROM t WHERE a != b", RuleCV01::default());
110        assert_eq!(violations.len(), 0);
111    }
112
113    #[test]
114    fn test_cv01_ansi_policy_flags_cstyle() {
115        let rule = RuleCV01 {
116            preferred: NotEqualStyle::AnsiStyle,
117        };
118        let violations = lint_sql("SELECT * FROM t WHERE a != b", rule);
119        assert_eq!(violations.len(), 1);
120    }
121}