sqruff_lib_core/parser/
lookahead.rs

1use ahash::AHashSet;
2
3use super::context::ParseContext;
4use super::match_algorithms::skip_start_index_forward_to_code;
5use super::match_result::MatchResult;
6use super::matchable::{Matchable, MatchableCacheKey, MatchableTrait, next_matchable_cache_key};
7use super::segments::ErasedSegment;
8use crate::dialects::syntax::SyntaxSet;
9use crate::errors::SQLParseError;
10
11/// A matcher that excludes patterns based on lookahead.
12///
13/// This is useful for cases where we need to exclude a token (like "WITH")
14/// only when it's followed by a specific pattern (like "(").
15#[derive(Debug, Clone, PartialEq)]
16pub struct LookaheadExclude {
17    /// The first token to match (e.g., "WITH")
18    first_token: &'static str,
19    /// The lookahead token to check for (e.g., "(")
20    lookahead_token: &'static str,
21    /// Unique cache key for this matcher
22    cache_key: MatchableCacheKey,
23}
24
25impl LookaheadExclude {
26    /// Create a new LookaheadExclude matcher.
27    pub fn new(first_token: &'static str, lookahead_token: &'static str) -> Self {
28        Self {
29            first_token,
30            lookahead_token,
31            cache_key: next_matchable_cache_key(),
32        }
33    }
34}
35
36impl MatchableTrait for LookaheadExclude {
37    fn elements(&self) -> &[Matchable] {
38        &[]
39    }
40
41    fn is_optional(&self) -> bool {
42        // Exclude patterns are not optional - they either match or don't
43        false
44    }
45
46    fn simple(
47        &self,
48        _parse_context: &ParseContext,
49        _crumbs: Option<Vec<&str>>,
50    ) -> Option<(AHashSet<String>, SyntaxSet)> {
51        // LookaheadExclude doesn't have simple matching
52        None
53    }
54
55    fn match_segments(
56        &self,
57        segments: &[ErasedSegment],
58        idx: u32,
59        _parse_context: &mut ParseContext,
60    ) -> Result<MatchResult, SQLParseError> {
61        // Check if we're at a valid position
62        if idx >= segments.len() as u32 {
63            return Ok(MatchResult::empty_at(idx));
64        }
65
66        // Check if current token matches first pattern (case-insensitive)
67        let current_raw = segments[idx as usize].raw();
68        if current_raw.eq_ignore_ascii_case(self.first_token) {
69            // Look ahead for second token, skipping any whitespace
70            let next_idx =
71                skip_start_index_forward_to_code(segments, idx + 1, segments.len() as u32);
72
73            if next_idx < segments.len() as u32 {
74                let next_raw = segments[next_idx as usize].raw();
75                if next_raw.eq_ignore_ascii_case(self.lookahead_token) {
76                    // Match found - return a match to indicate this should be excluded
77                    return Ok(MatchResult::from_span(idx, idx + 1));
78                }
79            }
80        }
81
82        // No match - don't exclude
83        Ok(MatchResult::empty_at(idx))
84    }
85
86    fn cache_key(&self) -> MatchableCacheKey {
87        self.cache_key
88    }
89}