Skip to main content

sqruff_lib/rules/structure/
st10.rs

1use hashbrown::HashMap;
2use smol_str::StrExt;
3use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
4use sqruff_lib_core::parser::segments::ErasedSegment;
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};
10
11#[derive(Default, Debug, Clone)]
12pub struct RuleST10;
13
14impl Rule for RuleST10 {
15    fn load_from_config(&self, _config: &HashMap<String, Value>) -> Result<ErasedRule, String> {
16        Ok(RuleST10.erased())
17    }
18
19    fn name(&self) -> &'static str {
20        "structure.constant_expression"
21    }
22
23    fn description(&self) -> &'static str {
24        "Redundant constant expression."
25    }
26
27    fn long_description(&self) -> &'static str {
28        r#"
29Including an expression that always evaluates to either `TRUE` or `FALSE`
30regardless of the input columns is unnecessary and makes statements harder
31to read and understand.
32
33**Anti-pattern**
34
35```sql
36SELECT *
37FROM my_table
38-- This following WHERE clause is redundant.
39WHERE my_table.col = my_table.col
40```
41
42**Best practice**
43
44```sql
45SELECT *
46FROM my_table
47-- Replace with a condition that includes meaningful logic,
48-- or remove the condition entirely.
49WHERE my_table.col > 3
50```
51"#
52    }
53
54    fn groups(&self) -> &'static [RuleGroups] {
55        &[RuleGroups::All, RuleGroups::Structure]
56    }
57
58    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
59        let subsegments = context.segment.segments();
60        let count_subsegments = subsegments.len();
61
62        let allowable_literal_expressions = ["1 = 1", "1 = 0"];
63
64        let mut results = Vec::new();
65
66        for (idx, seg) in subsegments.iter().enumerate() {
67            if !seg.is_type(SyntaxKind::ComparisonOperator) {
68                continue;
69            }
70
71            let raw_op = seg.raw();
72            if raw_op.as_str() != "=" && raw_op.as_str() != "!=" && raw_op.as_str() != "<>" {
73                continue;
74            }
75
76            // Check for other comparison/binary operators before this one
77            // (precedence concerns). Following SQLFluff's approach: only check
78            // segments before the current operator. This means the first
79            // comparison operator in an expression is always checked, while
80            // later ones are skipped due to precedence ambiguity.
81            let has_other_operators_before = subsegments[..idx].iter().any(|s| {
82                s.is_type(SyntaxKind::ComparisonOperator) || s.is_type(SyntaxKind::BinaryOperator)
83            });
84
85            if has_other_operators_before {
86                continue;
87            }
88
89            // Find LHS: first non-whitespace segment before the operator
90            let lhs = subsegments[..idx]
91                .iter()
92                .rev()
93                .find(|s| !is_whitespace_or_newline(s));
94
95            // Find RHS: first non-whitespace segment after the operator
96            let rhs = subsegments[idx + 1..count_subsegments]
97                .iter()
98                .find(|s| !is_whitespace_or_newline(s));
99
100            let (lhs, rhs) = match (lhs, rhs) {
101                (Some(l), Some(r)) => (l, r),
102                _ => continue,
103            };
104
105            // Skip templated segments
106            if lhs.is_templated() || rhs.is_templated() {
107                continue;
108            }
109
110            // Handle literal comparisons with allowlist
111            if lhs.is_type(SyntaxKind::NumericLiteral) && rhs.is_type(SyntaxKind::NumericLiteral) {
112                let expr = format!(
113                    "{} {} {}",
114                    lhs.raw().to_uppercase_smolstr(),
115                    raw_op,
116                    rhs.raw().to_uppercase_smolstr()
117                );
118                if allowable_literal_expressions.contains(&expr.as_str()) {
119                    continue;
120                }
121            } else if is_literal(lhs) && is_literal(rhs) {
122                // Non-numeric literals (e.g. quoted strings) - always flag
123            } else {
124                // Non-literal comparison: check type and value match
125                if lhs.get_type() != rhs.get_type() {
126                    continue;
127                }
128                if lhs.raw().to_uppercase_smolstr() != rhs.raw().to_uppercase_smolstr() {
129                    continue;
130                }
131            }
132
133            // Attach violation to the comparison operator
134            results.push(LintResult::new(seg.clone().into(), Vec::new(), None, None));
135        }
136
137        results
138    }
139
140    fn crawl_behaviour(&self) -> Crawler {
141        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::Expression]) }).into()
142    }
143}
144
145fn is_whitespace_or_newline(seg: &ErasedSegment) -> bool {
146    matches!(seg.get_type(), SyntaxKind::Whitespace | SyntaxKind::Newline)
147}
148
149fn is_literal(seg: &ErasedSegment) -> bool {
150    matches!(
151        seg.get_type(),
152        SyntaxKind::Literal
153            | SyntaxKind::NumericLiteral
154            | SyntaxKind::QuotedLiteral
155            | SyntaxKind::BooleanLiteral
156            | SyntaxKind::NullLiteral
157    )
158}