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_and_msg_key(
77                    self.code(),
78                    "Use '!=' instead of '<>'.",
79                    t.token.span,
80                    vec![SourceEdit::replace(t.token.span, "!=")],
81                    "rules.CV01.msg.use_ne",
82                    vec![],
83                )]
84            }
85            NotEqualStyle::AnsiStyle if text == "!=" => {
86                vec![LintViolation::with_fix_and_msg_key(
87                    self.code(),
88                    "Use '<>' instead of '!='.",
89                    t.token.span,
90                    vec![SourceEdit::replace(t.token.span, "<>")],
91                    "rules.CV01.msg.use_ltgt",
92                    vec![],
93                )]
94            }
95            _ => vec![],
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::test_utils::lint_sql;
104
105    #[test]
106    fn test_cv01_flags_ansi_neq() {
107        let violations = lint_sql("SELECT * FROM t WHERE a <> b", RuleCV01::default());
108        assert_eq!(violations.len(), 1);
109    }
110
111    #[test]
112    fn test_cv01_accepts_cstyle_neq() {
113        let violations = lint_sql("SELECT * FROM t WHERE a != b", RuleCV01::default());
114        assert_eq!(violations.len(), 0);
115    }
116
117    #[test]
118    fn test_cv01_ansi_policy_flags_cstyle() {
119        let rule = RuleCV01 {
120            preferred: NotEqualStyle::AnsiStyle,
121        };
122        let violations = lint_sql("SELECT * FROM t WHERE a != b", rule);
123        assert_eq!(violations.len(), 1);
124    }
125}