bwq_lint/validation/rules/
operator_rules.rs1use crate::ast::*;
2use crate::error::LintError;
3use crate::validation::{ValidationContext, ValidationResult, ValidationRule};
4
5pub 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
83pub 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
181pub 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 ValidationResult::new()
192 }
193
194 fn can_validate(&self, _expr: &Expression) -> bool {
195 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 Expression::BooleanOp {
206 operator: BooleanOperator::Not,
207 left,
208 right: _,
209 ..
210 } => {
211 if let Expression::Term {
213 term: Term::Word { value },
214 ..
215 } = left.as_ref()
216 {
217 if value.is_empty() {
218 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
253pub 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 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 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}