sqruff_lib/rules/convention/
cv01.rs

1use ahash::AHashMap;
2use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
3use sqruff_lib_core::lint_fix::LintFix;
4use sqruff_lib_core::parser::segments::SegmentBuilder;
5
6use crate::core::config::Value;
7use crate::core::rules::context::RuleContext;
8use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
9use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
10use crate::utils::functional::context::FunctionalContext;
11
12#[derive(Debug, Default, Clone)]
13pub struct RuleCV01 {
14    preferred_not_equal_style: PreferredNotEqualStyle,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Default)]
18enum PreferredNotEqualStyle {
19    #[default]
20    Consistent,
21    CStyle,
22    Ansi,
23}
24
25impl Rule for RuleCV01 {
26    fn load_from_config(&self, config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
27        if let Some(value) = config["preferred_not_equal_style"].as_string() {
28            let preferred_not_equal_style = match value {
29                "consistent" => PreferredNotEqualStyle::Consistent,
30                "c_style" => PreferredNotEqualStyle::CStyle,
31                "ansi" => PreferredNotEqualStyle::Ansi,
32                _ => {
33                    return Err(format!(
34                        "Invalid value for preferred_not_equal_style: {value}"
35                    ));
36                }
37            };
38            Ok(RuleCV01 {
39                preferred_not_equal_style,
40            }
41            .erased())
42        } else {
43            Err("Missing value for preferred_not_equal_style".to_string())
44        }
45    }
46
47    fn name(&self) -> &'static str {
48        "convention.not_equal"
49    }
50
51    fn description(&self) -> &'static str {
52        "Consistent usage of ``!=`` or ``<>`` for \"not equal to\" operator."
53    }
54
55    fn long_description(&self) -> &'static str {
56        r#"
57**Anti-pattern**
58
59Consistent usage of `!=` or `<>` for "not equal to" operator.
60
61```sql
62SELECT * FROM X WHERE 1 <> 2 AND 3 != 4;
63```
64
65**Best practice**
66
67Ensure all "not equal to" comparisons are consistent, not mixing `!=` and `<>`.
68
69```sql
70SELECT * FROM X WHERE 1 != 2 AND 3 != 4;
71```
72"#
73    }
74
75    fn groups(&self) -> &'static [RuleGroups] {
76        &[RuleGroups::All, RuleGroups::Convention]
77    }
78
79    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
80        // Get the comparison operator children
81        let segment = FunctionalContext::new(context).segment();
82        let raw_comparison_operators = segment.children(None);
83
84        // Only check ``<>`` or ``!=`` operators
85        let raw_operator_list = raw_comparison_operators
86            .iter()
87            .map(|r| r.raw())
88            .collect::<Vec<_>>();
89        if raw_operator_list != ["<", ">"] && raw_operator_list != ["!", "="] {
90            return Vec::new();
91        }
92
93        // If style is consistent, add the style of the first occurrence to memory
94        let preferred_style =
95            if self.preferred_not_equal_style == PreferredNotEqualStyle::Consistent {
96                if let Some(preferred_style) = context.try_get::<PreferredNotEqualStyle>() {
97                    preferred_style
98                } else {
99                    let style = if raw_operator_list == ["<", ">"] {
100                        PreferredNotEqualStyle::Ansi
101                    } else {
102                        PreferredNotEqualStyle::CStyle
103                    };
104                    context.set(style);
105                    style
106                }
107            } else {
108                self.preferred_not_equal_style
109            };
110
111        // Define the replacement
112        let replacement = match preferred_style {
113            PreferredNotEqualStyle::CStyle => {
114                vec!["!", "="]
115            }
116            PreferredNotEqualStyle::Ansi => {
117                vec!["<", ">"]
118            }
119            PreferredNotEqualStyle::Consistent => {
120                unreachable!("Consistent style should have been handled earlier")
121            }
122        };
123
124        // This operator already matches the existing style
125        if raw_operator_list == replacement {
126            return Vec::new();
127        }
128
129        // Provide a fix and replace ``<>`` with ``!=``
130        // As each symbol is a separate symbol this is done in two steps:
131        // Depending on style type, flip any inconsistent operators
132        // 1. Flip < and !
133        // 2. Flip > and =
134        let fixes = vec![
135            LintFix::replace(
136                raw_comparison_operators[0].clone(),
137                vec![
138                    SegmentBuilder::token(
139                        context.tables.next_id(),
140                        replacement[0],
141                        SyntaxKind::ComparisonOperator,
142                    )
143                    .finish(),
144                ],
145                None,
146            ),
147            LintFix::replace(
148                raw_comparison_operators[1].clone(),
149                vec![
150                    SegmentBuilder::token(
151                        context.tables.next_id(),
152                        replacement[1],
153                        SyntaxKind::ComparisonOperator,
154                    )
155                    .finish(),
156                ],
157                None,
158            ),
159        ];
160
161        vec![LintResult::new(
162            context.segment.clone().into(),
163            fixes,
164            None,
165            None,
166        )]
167    }
168
169    fn is_fix_compatible(&self) -> bool {
170        true
171    }
172
173    fn crawl_behaviour(&self) -> Crawler {
174        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::ComparisonOperator]) })
175            .into()
176    }
177}