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