Skip to main content

bwq_lint/validation/rules/
operator_rules.rs

1use crate::ast::*;
2use crate::error::LintError;
3use crate::validation::{ValidationContext, ValidationResult, ValidationRule};
4
5// Mixed AND/OR validation rule
6pub struct MixedAndOrRule;
7
8impl ValidationRule for MixedAndOrRule {
9    fn name(&self) -> &'static str {
10        "mixed-and-or"
11    }
12
13    fn validate(&self, expr: &Expression, _ctx: &ValidationContext) -> ValidationResult {
14        match expr {
15            Expression::BooleanOp {
16                operator,
17                left,
18                right,
19                span,
20            } => {
21                if matches!(operator, BooleanOperator::And) {
22                    if let Some(right_expr) = right {
23                        if self.contains_or_at_top_level(right_expr)
24                            || self.contains_or_at_top_level(left)
25                        {
26                            return ValidationResult::with_error(LintError::ValidationError {
27                                span: span.clone(),
28                                message: "The AND and OR operators cannot be mixed in the same sub-query. Please use parentheses to disambiguate - e.g. vanilla AND (icecream OR cake).".to_string(),
29                            });
30                        }
31                    }
32                } else if matches!(operator, BooleanOperator::Or) {
33                    if let Some(right_expr) = right {
34                        if self.contains_and_at_top_level(right_expr)
35                            || self.contains_and_at_top_level(left)
36                        {
37                            return ValidationResult::with_error(LintError::ValidationError {
38                                span: span.clone(),
39                                message: "The AND and OR operators cannot be mixed in the same sub-query. Please use parentheses to disambiguate - e.g. vanilla AND (icecream OR cake).".to_string(),
40                            });
41                        }
42                    }
43                }
44                ValidationResult::new()
45            }
46            _ => ValidationResult::new(),
47        }
48    }
49
50    fn can_validate(&self, expr: &Expression) -> bool {
51        matches!(
52            expr,
53            Expression::BooleanOp {
54                operator: BooleanOperator::And | BooleanOperator::Or,
55                ..
56            }
57        )
58    }
59}
60
61impl MixedAndOrRule {
62    fn contains_and_at_top_level(&self, expr: &Expression) -> bool {
63        matches!(
64            expr,
65            Expression::BooleanOp {
66                operator: BooleanOperator::And,
67                ..
68            }
69        )
70    }
71
72    fn contains_or_at_top_level(&self, expr: &Expression) -> bool {
73        matches!(
74            expr,
75            Expression::BooleanOp {
76                operator: BooleanOperator::Or,
77                ..
78            }
79        )
80    }
81}
82
83// Mixed NEAR/boolean validation rule
84pub struct MixedNearRule;
85
86impl ValidationRule for MixedNearRule {
87    fn name(&self) -> &'static str {
88        "mixed-near"
89    }
90
91    fn validate(&self, expr: &Expression, ctx: &ValidationContext) -> ValidationResult {
92        match expr {
93            Expression::BooleanOp {
94                operator,
95                left,
96                right,
97                span,
98            } => {
99                if matches!(operator, BooleanOperator::And) {
100                    if let Some(right_expr) = right {
101                        if self.contains_near_at_top_level(right_expr)
102                            || self.contains_near_at_top_level(left)
103                        {
104                            return ValidationResult::with_error(LintError::ValidationError {
105                                span: span.clone(),
106                                message: "The AND operator cannot be used within the NEAR operator. Either remove this operator or disambiguate with parenthesis, e.g. (vanilla NEAR/5 ice-cream) AND cake.".to_string(),
107                            });
108                        }
109                    }
110                }
111                if !ctx.inside_group && matches!(operator, BooleanOperator::Or) {
112                    if let Some(right_expr) = right {
113                        if self.contains_near_at_top_level(right_expr)
114                            || self.contains_near_at_top_level(left)
115                        {
116                            return ValidationResult::with_error(LintError::ValidationError {
117                                span: span.clone(),
118                                message: "Please use parentheses for disambiguation when using the OR or NEAR operators with another NEAR operator - e.g. (vanilla OR chocolate) NEAR/5 (ice-cream NEAR/5 cake).".to_string(),
119                            });
120                        }
121                    }
122                }
123                ValidationResult::new()
124            }
125            Expression::Proximity { terms, span, .. } => {
126                for term in terms {
127                    if self.contains_or_at_top_level(term) || self.contains_and_at_top_level(term) {
128                        return ValidationResult::with_error(LintError::ValidationError {
129                            span: span.clone(),
130                            message: "Please use parentheses for disambiguation when using the OR or NEAR operators with another NEAR operator - e.g. (vanilla OR chocolate) NEAR/5 (ice-cream NEAR/5 cake).".to_string(),
131                        });
132                    }
133                }
134                ValidationResult::new()
135            }
136            _ => ValidationResult::new(),
137        }
138    }
139
140    fn can_validate(&self, expr: &Expression) -> bool {
141        matches!(
142            expr,
143            Expression::BooleanOp {
144                operator: BooleanOperator::And | BooleanOperator::Or,
145                ..
146            } | Expression::Proximity { .. }
147        )
148    }
149}
150
151impl MixedNearRule {
152    fn contains_and_at_top_level(&self, expr: &Expression) -> bool {
153        matches!(
154            expr,
155            Expression::BooleanOp {
156                operator: BooleanOperator::And,
157                ..
158            }
159        )
160    }
161
162    fn contains_or_at_top_level(&self, expr: &Expression) -> bool {
163        matches!(
164            expr,
165            Expression::BooleanOp {
166                operator: BooleanOperator::Or,
167                ..
168            }
169        )
170    }
171
172    fn contains_near_at_top_level(&self, expr: &Expression) -> bool {
173        match expr {
174            Expression::Proximity { .. } => true,
175            Expression::Group { .. } => false,
176            _ => false,
177        }
178    }
179}
180
181// Pure negative query validation rule
182pub struct PureNegativeRule;
183
184impl ValidationRule for PureNegativeRule {
185    fn name(&self) -> &'static str {
186        "pure-negative"
187    }
188
189    fn validate(&self, _expr: &Expression, _ctx: &ValidationContext) -> ValidationResult {
190        // This will be handled at the query level in the engine, not per expression
191        ValidationResult::new()
192    }
193
194    fn can_validate(&self, _expr: &Expression) -> bool {
195        // Only validate at the root query level
196        false
197    }
198}
199
200impl PureNegativeRule {
201    #[allow(clippy::only_used_in_recursion)]
202    pub fn is_pure_negative_query(&self, expr: &Expression) -> bool {
203        match expr {
204            // For binary NOT, check if we're starting with a NOT operation at the top level
205            Expression::BooleanOp {
206                operator: BooleanOperator::Not,
207                left,
208                right: _,
209                ..
210            } => {
211                // Check if this is a leading NOT (dummy left operand)
212                if let Expression::Term {
213                    term: Term::Word { value },
214                    ..
215                } = left.as_ref()
216                {
217                    if value.is_empty() {
218                        // This is a leading NOT - ANY leading NOT is pure negative
219                        // according to Brandwatch API behavior
220                        return true;
221                    }
222                }
223                false
224            }
225            Expression::BooleanOp {
226                operator: BooleanOperator::And,
227                left,
228                right,
229                ..
230            } => {
231                self.is_pure_negative_query(left)
232                    && right
233                        .as_ref()
234                        .is_none_or(|r| self.is_pure_negative_query(r))
235            }
236            Expression::BooleanOp {
237                operator: BooleanOperator::Or,
238                left,
239                right,
240                ..
241            } => {
242                self.is_pure_negative_query(left)
243                    && right
244                        .as_ref()
245                        .is_none_or(|r| self.is_pure_negative_query(r))
246            }
247            Expression::Group { expression, .. } => self.is_pure_negative_query(expression),
248            _ => false,
249        }
250    }
251}
252
253// Binary operator validation rule
254pub struct BinaryOperatorRule;
255
256impl ValidationRule for BinaryOperatorRule {
257    fn name(&self) -> &'static str {
258        "binary-operator"
259    }
260
261    fn validate(&self, expr: &Expression, _ctx: &ValidationContext) -> ValidationResult {
262        if let Expression::BooleanOp {
263            operator,
264            left,
265            right,
266            span,
267        } = expr
268        {
269            // Check for NOT operator with empty left operand (this is valid binary NOT)
270            if matches!(operator, BooleanOperator::Not) {
271                if let Expression::Term {
272                    term: Term::Word { value },
273                    ..
274                } = left.as_ref()
275                {
276                    if value.is_empty() {
277                        if right.is_none() {
278                            return ValidationResult::with_error(LintError::ValidationError {
279                                span: span.clone(),
280                                message: "NOT operator requires an operand".to_string(),
281                            });
282                        }
283                        return ValidationResult::new();
284                    }
285                }
286            }
287
288            // For non-NOT operators, ensure we have both operands
289            if right.is_none() && !matches!(operator, BooleanOperator::Not) {
290                return ValidationResult::with_error(LintError::ValidationError {
291                    span: span.clone(),
292                    message: format!("{} operator requires two operands", operator.as_str()),
293                });
294            }
295        }
296        ValidationResult::new()
297    }
298
299    fn can_validate(&self, expr: &Expression) -> bool {
300        matches!(expr, Expression::BooleanOp { .. })
301    }
302}