Skip to main content

bwq_lint/
parser.rs

1use crate::ast::*;
2use crate::error::{LintError, LintResult, LintWarning, Span};
3use crate::lexer::{Token, TokenType};
4
5/// Result type that includes both the parsed query and any parser warnings
6pub struct ParseResult {
7    pub query: Query,
8    pub warnings: Vec<LintWarning>,
9}
10
11/// Recursive descent parser for Brandwatch boolean queries
12pub struct Parser {
13    tokens: Vec<Token>,
14    current: usize,
15    implicit_and_spans: Vec<Span>,
16}
17
18impl Parser {
19    pub fn new(tokens: Vec<Token>) -> Self {
20        Self {
21            tokens,
22            current: 0,
23            implicit_and_spans: Vec::new(),
24        }
25    }
26
27    /// Parse the tokens into a Query AST
28    pub fn parse(&mut self) -> LintResult<ParseResult> {
29        let expression = self.parse_expression()?;
30        let span = expression.span().clone();
31
32        // Ensure we've consumed all tokens except EOF
33        if !self.is_at_end() && !matches!(self.peek().token_type, TokenType::Eof) {
34            return Err(LintError::UnexpectedToken {
35                span: self.peek().span.clone(),
36                token: self.peek().token_type.to_string(),
37            });
38        }
39
40        // Generate warnings for implicit AND operations
41        let mut warnings = Vec::new();
42        for span in &self.implicit_and_spans {
43            warnings.push(LintWarning::PotentialTypo {
44                span: span.clone(),
45                suggestion: "Consider using explicit 'AND' operator for clarity".to_string(),
46            });
47        }
48
49        Ok(ParseResult {
50            query: Query { expression, span },
51            warnings,
52        })
53    }
54
55    fn parse_expression(&mut self) -> LintResult<Expression> {
56        let mut left = self.parse_and_expression()?;
57
58        while {
59            // Skip any comments between expressions
60            self.skip_comments();
61            self.match_token(&TokenType::Or)
62        } {
63            let operator = BooleanOperator::Or;
64            let _operator_span = self.previous().span.clone();
65            let right = self.parse_and_expression()?;
66
67            let span = Span::new(left.span().start.clone(), right.span().end.clone());
68            left = Expression::BooleanOp {
69                operator,
70                left: Box::new(left),
71                right: Some(Box::new(right)),
72                span,
73            };
74        }
75
76        Ok(left)
77    }
78
79    fn parse_and_expression(&mut self) -> LintResult<Expression> {
80        let mut left = self.parse_not_expression()?;
81
82        loop {
83            // Skip any comments between expressions
84            self.skip_comments();
85
86            if self.match_token(&TokenType::And) {
87                // Explicit AND
88                let operator = BooleanOperator::And;
89                let _operator_span = self.previous().span.clone();
90                let right = self.parse_not_expression()?;
91
92                let span = Span::new(left.span().start.clone(), right.span().end.clone());
93                left = Expression::BooleanOp {
94                    operator,
95                    left: Box::new(left),
96                    right: Some(Box::new(right)),
97                    span,
98                };
99            } else if self.is_implicit_and_candidate() {
100                // Implicit AND (space-separated terms)
101                let right = self.parse_not_expression()?;
102
103                let span = Span::new(left.span().start.clone(), right.span().end.clone());
104                left = Expression::BooleanOp {
105                    operator: BooleanOperator::And,
106                    left: Box::new(left),
107                    right: Some(Box::new(right)),
108                    span: span.clone(),
109                };
110
111                // Mark this for warning generation
112                self.implicit_and_spans.push(span);
113            } else {
114                break;
115            }
116        }
117
118        Ok(left)
119    }
120
121    fn parse_not_expression(&mut self) -> LintResult<Expression> {
122        // Handle leading NOT operator
123        if self.match_token(&TokenType::Not) {
124            let operator_span = self.previous().span.clone();
125            let dummy_left = Expression::Term {
126                term: Term::Word {
127                    value: "".to_string(),
128                },
129                span: operator_span.clone(),
130            };
131            let right = self.parse_proximity_expression()?;
132
133            let span = Span::new(operator_span.start.clone(), right.span().end.clone());
134            return Ok(Expression::BooleanOp {
135                operator: BooleanOperator::Not,
136                left: Box::new(dummy_left),
137                right: Some(Box::new(right)),
138                span,
139            });
140        }
141
142        let mut left = self.parse_proximity_expression()?;
143
144        while {
145            // Skip any comments between expressions
146            self.skip_comments();
147            self.match_token(&TokenType::Not)
148        } {
149            let operator = BooleanOperator::Not;
150            let _operator_span = self.previous().span.clone();
151            let right = self.parse_proximity_expression()?;
152
153            let span = Span::new(left.span().start.clone(), right.span().end.clone());
154            left = Expression::BooleanOp {
155                operator,
156                left: Box::new(left),
157                right: Some(Box::new(right)),
158                span,
159            };
160        }
161
162        Ok(left)
163    }
164
165    fn parse_proximity_expression(&mut self) -> LintResult<Expression> {
166        let left = self.parse_primary()?;
167
168        // Handle proximity operators
169        if self.match_token(&TokenType::Tilde) {
170            let tilde_span = self.previous().span.clone();
171            let mut distance = None;
172
173            // Check for optional distance number
174            if let TokenType::Number(num_str) = &self.peek().token_type {
175                distance = num_str.parse::<u32>().ok();
176                self.advance();
177            }
178
179            // Validate tilde usage according to Brandwatch rules
180            // Tilde is only valid:
181            // 1. After quoted phrases for proximity: "apple juice"~5
182            // 2. After grouped expressions for proximity: ((apple OR orange) AND (smartphone OR phone))~5
183            // 3. After single terms for fuzzy matching: apple~5 (but NOT apple ~5 juice)
184
185            let is_valid_tilde_context = match &left {
186                // Valid: quoted phrases
187                Expression::Term {
188                    term: Term::Phrase { .. },
189                    ..
190                } => true,
191                // Valid: grouped expressions
192                Expression::Group { .. } => true,
193                // Valid: single terms for fuzzy matching (no additional terms after tilde)
194                Expression::Term { .. } => {
195                    // Check if there are additional terms after the tilde
196                    // If so, this is invalid syntax like "apple ~5 juice"
197                    self.is_at_end()
198                        || matches!(
199                            self.peek().token_type,
200                            TokenType::And
201                                | TokenType::Or
202                                | TokenType::Not
203                                | TokenType::RightParen
204                                | TokenType::LeftParen
205                                | TokenType::Eof
206                        )
207                }
208                _ => false,
209            };
210
211            if !is_valid_tilde_context {
212                // This handles the invalid case like "apple ~5 juice"
213                return Err(LintError::ValidationError {
214                    span: tilde_span,
215                    message: "The ~ character should be used after a search term or quoted phrase (to specify fuzzy matching), or after a sub-query (to specify proximity matching). If this should be part of a search term, it must be quoted (or escaped using the \\ character).".to_string(),
216                });
217            }
218
219            let terms = vec![left];
220            let end_span = tilde_span.end.clone();
221            let span = Span::new(terms[0].span().start.clone(), end_span);
222
223            return Ok(Expression::Proximity {
224                operator: ProximityOperator::Proximity { distance },
225                terms,
226                span,
227            });
228        }
229
230        // Handle NEAR operators
231        if let TokenType::Near(distance) = &self.peek().token_type {
232            let distance = *distance;
233            self.advance();
234            let _operator_span = self.previous().span.clone();
235            let right = self.parse_primary()?;
236
237            let span = Span::new(left.span().start.clone(), right.span().end.clone());
238            return Ok(Expression::Proximity {
239                operator: ProximityOperator::Near { distance },
240                terms: vec![left, right],
241                span,
242            });
243        }
244
245        if let TokenType::NearForward(distance) = &self.peek().token_type {
246            let distance = *distance;
247            self.advance();
248            let _operator_span = self.previous().span.clone();
249            let right = self.parse_primary()?;
250
251            let span = Span::new(left.span().start.clone(), right.span().end.clone());
252            return Ok(Expression::Proximity {
253                operator: ProximityOperator::NearForward { distance },
254                terms: vec![left, right],
255                span,
256            });
257        }
258
259        Ok(left)
260    }
261
262    fn parse_primary(&mut self) -> LintResult<Expression> {
263        // Handle parenthesized expressions
264        if self.match_token(&TokenType::LeftParen) {
265            let start_span = self.previous().span.clone();
266            let expr = self.parse_expression()?;
267
268            if !self.match_token(&TokenType::RightParen) {
269                return Err(LintError::ExpectedToken {
270                    span: self.peek().span.clone(),
271                    expected: ")".to_string(),
272                    found: self.peek().token_type.to_string(),
273                });
274            }
275
276            let end_span = self.previous().span.clone();
277            let span = Span::new(start_span.start, end_span.end);
278
279            return Ok(Expression::Group {
280                expression: Box::new(expr),
281                span,
282            });
283        }
284
285        // Handle case-sensitive terms {word}
286        if self.match_token(&TokenType::LeftBrace) {
287            let start_span = self.previous().span.clone();
288
289            if let TokenType::Word(word) = &self.peek().token_type {
290                let word = word.clone();
291                self.advance();
292                let _word_span = self.previous().span.clone();
293
294                if !self.match_token(&TokenType::RightBrace) {
295                    return Err(LintError::ExpectedToken {
296                        span: self.peek().span.clone(),
297                        expected: "}".to_string(),
298                        found: self.peek().token_type.to_string(),
299                    });
300                }
301
302                let end_span = self.previous().span.clone();
303                let span = Span::new(start_span.start, end_span.end);
304
305                return Ok(Expression::Term {
306                    term: Term::CaseSensitive { value: word },
307                    span,
308                });
309            } else {
310                return Err(LintError::ExpectedToken {
311                    span: self.peek().span.clone(),
312                    expected: "word".to_string(),
313                    found: self.peek().token_type.to_string(),
314                });
315            }
316        }
317
318        // Handle ranges [x TO y]
319        if self.match_token(&TokenType::LeftBracket) {
320            return self.parse_range();
321        }
322
323        // Handle comments <<<text>>>
324        if self.match_token(&TokenType::CommentStart) {
325            return self.parse_comment();
326        }
327
328        // Handle field operations
329        if let TokenType::Word(word) = &self.peek().token_type {
330            let word = word.clone();
331            let word_span = self.peek().span.clone();
332
333            // Look ahead for colon to determine if this is a field operation
334            if self.peek_ahead(1).map(|t| &t.token_type) == Some(&TokenType::Colon) {
335                self.advance(); // consume field name
336                self.advance(); // consume colon
337
338                let value = Box::new(self.parse_primary()?);
339
340                let value = if let Expression::Range {
341                    start,
342                    end,
343                    span: range_span,
344                    ..
345                } = value.as_ref()
346                {
347                    if let Some(field_type) = FieldType::parse(&word) {
348                        Box::new(Expression::Range {
349                            field: Some(field_type),
350                            start: start.clone(),
351                            end: end.clone(),
352                            span: range_span.clone(),
353                        })
354                    } else {
355                        value
356                    }
357                } else {
358                    value
359                };
360
361                let span = Span::new(word_span.start, value.span().end.clone());
362
363                if let Some(field_type) = FieldType::parse(&word) {
364                    return Ok(Expression::Field {
365                        field: field_type,
366                        value,
367                        span,
368                    });
369                } else {
370                    return Ok(Expression::Term {
371                        term: Term::Word {
372                            value: format!(
373                                "{}:{}",
374                                word,
375                                match value.as_ref() {
376                                    Expression::Term {
377                                        term: Term::Word { value },
378                                        ..
379                                    } => value.clone(),
380                                    Expression::Term {
381                                        term: Term::Phrase { value },
382                                        ..
383                                    } => format!("\"{}\"", value),
384                                    _ => "unknown".to_string(),
385                                }
386                            ),
387                        },
388                        span,
389                    });
390                }
391            }
392        }
393
394        self.parse_term()
395    }
396
397    fn parse_range(&mut self) -> LintResult<Expression> {
398        let start_span = self.previous().span.clone();
399
400        let start_value = match &self.peek().token_type {
401            TokenType::Word(w) | TokenType::Number(w) => {
402                let val = w.clone();
403                self.advance();
404                val
405            }
406            _ => {
407                return Err(LintError::ExpectedToken {
408                    span: self.peek().span.clone(),
409                    expected: "number or word".to_string(),
410                    found: self.peek().token_type.to_string(),
411                });
412            }
413        };
414
415        if !self.match_token(&TokenType::To) {
416            return Err(LintError::ExpectedToken {
417                span: self.peek().span.clone(),
418                expected: "TO".to_string(),
419                found: self.peek().token_type.to_string(),
420            });
421        }
422
423        let end_value = match &self.peek().token_type {
424            TokenType::Word(w) | TokenType::Number(w) => {
425                let val = w.clone();
426                self.advance();
427                val
428            }
429            _ => {
430                return Err(LintError::ExpectedToken {
431                    span: self.peek().span.clone(),
432                    expected: "number or word".to_string(),
433                    found: self.peek().token_type.to_string(),
434                });
435            }
436        };
437
438        if !self.match_token(&TokenType::RightBracket) {
439            return Err(LintError::ExpectedToken {
440                span: self.peek().span.clone(),
441                expected: "]".to_string(),
442                found: self.peek().token_type.to_string(),
443            });
444        }
445
446        let end_span = self.previous().span.clone();
447        let span = Span::new(start_span.start, end_span.end);
448
449        Ok(Expression::Range {
450            field: None,
451            start: start_value,
452            end: end_value,
453            span,
454        })
455    }
456
457    fn parse_comment(&mut self) -> LintResult<Expression> {
458        let start_span = self.previous().span.clone();
459        let mut comment_text = String::new();
460
461        while !self.is_at_end() && !matches!(self.peek().token_type, TokenType::CommentEnd) {
462            match &self.peek().token_type {
463                TokenType::Word(w) => comment_text.push_str(w),
464                TokenType::QuotedString(s) => comment_text.push_str(&format!("\"{}\"", s)),
465                TokenType::Number(n) => comment_text.push_str(n),
466                TokenType::Whitespace => comment_text.push(' '),
467                _ => comment_text.push_str(&self.peek().raw),
468            }
469            self.advance();
470        }
471
472        if !self.match_token(&TokenType::CommentEnd) {
473            return Err(LintError::ExpectedToken {
474                span: self.peek().span.clone(),
475                expected: ">>>".to_string(),
476                found: self.peek().token_type.to_string(),
477            });
478        }
479
480        let end_span = self.previous().span.clone();
481        let span = Span::new(start_span.start, end_span.end);
482
483        Ok(Expression::Comment {
484            text: comment_text.trim().to_string(),
485            span,
486        })
487    }
488
489    fn parse_term(&mut self) -> LintResult<Expression> {
490        let token = self.peek().clone();
491
492        match &token.token_type {
493            TokenType::Word(word) => {
494                self.advance();
495                let term = if word.contains('*') {
496                    Term::Wildcard {
497                        value: word.clone(),
498                    }
499                } else if word.contains('?') {
500                    Term::Replacement {
501                        value: word.clone(),
502                    }
503                } else {
504                    Term::Word {
505                        value: word.clone(),
506                    }
507                };
508
509                Ok(Expression::Term {
510                    term,
511                    span: token.span,
512                })
513            }
514            TokenType::QuotedString(string) => {
515                self.advance();
516                Ok(Expression::Term {
517                    term: Term::Phrase {
518                        value: string.clone(),
519                    },
520                    span: token.span,
521                })
522            }
523            TokenType::Number(number) => {
524                self.advance();
525                Ok(Expression::Term {
526                    term: Term::Word {
527                        value: number.clone(),
528                    },
529                    span: token.span,
530                })
531            }
532            TokenType::Hashtag(hashtag) => {
533                self.advance();
534                Ok(Expression::Term {
535                    term: Term::Hashtag {
536                        value: hashtag.clone(),
537                    },
538                    span: token.span,
539                })
540            }
541            TokenType::Mention(mention) => {
542                self.advance();
543                Ok(Expression::Term {
544                    term: Term::Mention {
545                        value: mention.clone(),
546                    },
547                    span: token.span,
548                })
549            }
550            _ => Err(LintError::UnexpectedToken {
551                span: token.span,
552                token: token.token_type.to_string(),
553            }),
554        }
555    }
556
557    fn match_token(&mut self, token_type: &TokenType) -> bool {
558        if self.check(token_type) {
559            self.advance();
560            true
561        } else {
562            false
563        }
564    }
565
566    fn check(&self, token_type: &TokenType) -> bool {
567        if self.is_at_end() {
568            false
569        } else {
570            std::mem::discriminant(&self.peek().token_type) == std::mem::discriminant(token_type)
571        }
572    }
573
574    fn advance(&mut self) -> &Token {
575        if !self.is_at_end() {
576            self.current += 1;
577        }
578        self.previous()
579    }
580
581    fn is_at_end(&self) -> bool {
582        matches!(self.peek().token_type, TokenType::Eof) || self.current >= self.tokens.len()
583    }
584
585    fn peek(&self) -> &Token {
586        &self.tokens[self.current]
587    }
588
589    fn peek_ahead(&self, offset: usize) -> Option<&Token> {
590        let index = self.current + offset;
591        if index < self.tokens.len() {
592            Some(&self.tokens[index])
593        } else {
594            None
595        }
596    }
597
598    fn previous(&self) -> &Token {
599        &self.tokens[self.current - 1]
600    }
601
602    fn is_implicit_and_candidate(&self) -> bool {
603        if self.is_at_end() {
604            return false;
605        }
606
607        match &self.peek().token_type {
608            TokenType::Word(_)
609            | TokenType::QuotedString(_)
610            | TokenType::Number(_)
611            | TokenType::Hashtag(_)
612            | TokenType::Mention(_)
613            | TokenType::LeftParen
614            | TokenType::LeftBrace => true,
615
616            TokenType::Or
617            | TokenType::RightParen
618            | TokenType::RightBracket
619            | TokenType::RightBrace
620            | TokenType::Eof => false,
621
622            TokenType::And | TokenType::Not => false,
623
624            _ => false,
625        }
626    }
627
628    /// Skip any comments at the current position
629    fn skip_comments(&mut self) {
630        while self.match_token(&TokenType::CommentStart) {
631            while !self.is_at_end() && !matches!(self.peek().token_type, TokenType::CommentEnd) {
632                self.advance();
633            }
634            self.match_token(&TokenType::CommentEnd);
635        }
636    }
637}
638
639impl Expression {
640    pub fn span(&self) -> &Span {
641        match self {
642            Expression::BooleanOp { span, .. } => span,
643            Expression::Group { span, .. } => span,
644            Expression::Proximity { span, .. } => span,
645            Expression::Field { span, .. } => span,
646            Expression::Range { span, .. } => span,
647            Expression::Term { span, .. } => span,
648            Expression::Comment { span, .. } => span,
649        }
650    }
651}
652
653#[cfg(test)]
654mod tests {
655    use super::*;
656    use crate::lexer::Lexer;
657
658    #[test]
659    fn test_basic_parsing() {
660        let mut lexer = Lexer::new("apple AND juice");
661        let tokens = lexer.tokenize().unwrap();
662        let mut parser = Parser::new(tokens);
663        let result = parser.parse().unwrap();
664
665        match result.query.expression {
666            Expression::BooleanOp { operator, .. } => {
667                assert_eq!(operator, BooleanOperator::And);
668            }
669            _ => panic!("Expected BooleanOp"),
670        }
671    }
672
673    #[test]
674    fn test_quoted_phrase() {
675        let mut lexer = Lexer::new("\"apple juice\"");
676        let tokens = lexer.tokenize().unwrap();
677        let mut parser = Parser::new(tokens);
678        let result = parser.parse().unwrap();
679
680        match result.query.expression {
681            Expression::Term {
682                term: Term::Phrase { value },
683                ..
684            } => {
685                assert_eq!(value, "apple juice");
686            }
687            _ => panic!("Expected Term with Phrase"),
688        }
689    }
690
691    #[test]
692    fn test_field_operation() {
693        let mut lexer = Lexer::new("title:\"apple juice\"");
694        let tokens = lexer.tokenize().unwrap();
695        let mut parser = Parser::new(tokens);
696        let result = parser.parse().unwrap();
697
698        match result.query.expression {
699            Expression::Field { field, .. } => {
700                assert_eq!(field, FieldType::Title);
701            }
702            _ => panic!("Expected Field operation"),
703        }
704    }
705
706    #[test]
707    fn test_implicit_and() {
708        let mut lexer = Lexer::new("apple banana");
709        let tokens = lexer.tokenize().unwrap();
710        let mut parser = Parser::new(tokens);
711        let result = parser.parse().unwrap();
712
713        match result.query.expression {
714            Expression::BooleanOp { operator, .. } => {
715                assert_eq!(operator, BooleanOperator::And);
716            }
717            _ => panic!("Expected BooleanOp with implicit AND"),
718        }
719
720        assert!(!result.warnings.is_empty());
721    }
722}