Skip to main content

qql_core/params/
scan.rs

1//! Lexical scanner helpers for parameter placeholders and protected spans.
2
3/// Returns true if the `:` at byte offset `i` is at a valid token boundary to begin a parameter placeholder.
4pub fn is_placeholder_start(bytes: &[u8], i: usize) -> bool {
5    if i == 0 {
6        return true;
7    }
8    !matches!(
9        bytes[i - 1],
10        b'0'..=b'9'
11            | b'A'..=b'Z'
12            | b'a'..=b'z'
13            | b'_'
14            | b'$'
15            | b'\''
16            | b'"'
17            | b'`'
18            | b'}'
19            | b']'
20    )
21}
22
23/// Check if a byte is a valid parameter identifier start character.
24#[inline]
25pub fn is_ident_start(b: u8) -> bool {
26    b.is_ascii_alphabetic() || b == b'_'
27}
28
29/// Check if a byte is a valid parameter identifier continue character.
30#[inline]
31pub fn is_ident_continue(b: u8) -> bool {
32    b.is_ascii_alphanumeric() || b == b'_'
33}
34
35/// Advance past a `--` line comment. `i` is the first `-`. Stops before `\n`.
36pub fn scan_line_comment(bytes: &[u8], mut i: usize) -> usize {
37    while i < bytes.len() && bytes[i] != b'\n' {
38        i += 1;
39    }
40    i
41}
42
43/// Advance past a backtick-quoted span including the closing `` ` `` if present.
44pub fn scan_backtick(bytes: &[u8], mut i: usize) -> usize {
45    i += 1;
46    while i < bytes.len() {
47        if bytes[i] == b'`' {
48            return i + 1;
49        }
50        i += 1;
51    }
52    i
53}
54
55/// Advance past a single-quoted literal, honoring `''` and `\` escapes.
56pub fn scan_single_quoted(bytes: &[u8], mut i: usize) -> usize {
57    i += 1;
58    while i < bytes.len() {
59        let sc = bytes[i];
60        i += 1;
61        if sc == b'\\' && i < bytes.len() {
62            i += 1;
63        } else if sc == b'\'' {
64            if i < bytes.len() && bytes[i] == b'\'' {
65                i += 1;
66            } else {
67                break;
68            }
69        }
70    }
71    i
72}
73
74#[inline]
75fn has_closing_triple(bytes: &[u8], start: usize, triple: &[u8; 3]) -> bool {
76    start + 2 < bytes.len() && &bytes[start..start + 3] == triple
77}
78
79/// Advance past a triple-quoted string (`'''...'''` or `"""..."""`).
80pub fn scan_triple_quoted(bytes: &[u8], mut i: usize, quote: u8) -> usize {
81    i += 3;
82    let triple = [quote, quote, quote];
83    while i < bytes.len() {
84        if has_closing_triple(bytes, i, &triple) {
85            return i + 3;
86        }
87        i += 1;
88    }
89    i
90}
91
92/// Advance past a raw string literal `r'...'` or `r"..."`.
93pub fn scan_raw_string(bytes: &[u8], mut i: usize, quote: u8) -> usize {
94    i += 2;
95    while i < bytes.len() {
96        if bytes[i] == quote {
97            return i + 1;
98        }
99        i += 1;
100    }
101    i
102}
103
104/// Advance past a double-quoted string literal (which may contain `\"`).
105pub fn scan_double_quoted(bytes: &[u8], mut i: usize) -> usize {
106    i += 1;
107    while i < bytes.len() {
108        let sc = bytes[i];
109        i += 1;
110        if sc == b'\\' && i < bytes.len() {
111            i += 1;
112        } else if sc == b'"' {
113            break;
114        }
115    }
116    i
117}
118
119/// If `bytes[i]` begins a comment, string literal, or backtick identifier,
120/// returns `Some(next_offset)` skipping the protected region. Otherwise `None`.
121pub fn skip_protected(bytes: &[u8], i: usize) -> Option<usize> {
122    match bytes[i] {
123        b'-' if i + 1 < bytes.len() && bytes[i + 1] == b'-' => {
124            Some(scan_line_comment(bytes, i + 2))
125        }
126        b'`' => Some(scan_backtick(bytes, i)),
127        b'\'' => {
128            if i + 2 < bytes.len() && bytes[i + 1] == b'\'' && bytes[i + 2] == b'\'' {
129                Some(scan_triple_quoted(bytes, i, b'\''))
130            } else {
131                Some(scan_single_quoted(bytes, i))
132            }
133        }
134        b'"' => {
135            if i + 2 < bytes.len() && bytes[i + 1] == b'"' && bytes[i + 2] == b'"' {
136                Some(scan_triple_quoted(bytes, i, b'"'))
137            } else {
138                Some(scan_double_quoted(bytes, i))
139            }
140        }
141        b'r' if i + 1 < bytes.len() && (bytes[i + 1] == b'\'' || bytes[i + 1] == b'"') => {
142            let quote = bytes[i + 1];
143            if i + 3 < bytes.len() && bytes[i + 2] == quote && bytes[i + 3] == quote {
144                Some(scan_triple_quoted(bytes, i + 1, quote))
145            } else {
146                Some(scan_raw_string(bytes, i, quote))
147            }
148        }
149        _ => None,
150    }
151}
152
153/// ASCII identifier slice at `[start, end)`. Infallible because the scanner
154/// only advances over `is_ident_start` / `is_ident_continue` bytes.
155pub fn ident_at(source: &str, start: usize, end: usize) -> &str {
156    source.get(start..end).unwrap_or("")
157}