Skip to main content

squawk_parser/
lexed_str.rs

1// based on https://github.com/rust-lang/rust-analyzer/blob/d8887c0758bbd2d5f752d5bd405d4491e90e7ed6/crates/parser/src/lexed_str.rs
2
3use std::{num::IntErrorKind, ops};
4
5use squawk_lexer::tokenize;
6
7use crate::SyntaxKind;
8
9pub struct LexedStr<'a> {
10    text: &'a str,
11    kind: Vec<SyntaxKind>,
12    start: Vec<u32>,
13    error: Vec<LexError>,
14}
15
16struct LexError {
17    msg: String,
18    range: ops::Range<u32>,
19}
20
21impl<'a> LexedStr<'a> {
22    // TODO: rust-analyzer has an edition thing to specify things that are only
23    // available in certain version, we can do that later
24    pub fn new(text: &'a str) -> LexedStr<'a> {
25        let mut conv = Converter::new(text);
26
27        for token in tokenize(&text[conv.offset..]) {
28            let token_text = &text[conv.offset..][..token.len as usize];
29
30            conv.extend_token(&token.kind, token_text);
31        }
32
33        conv.finalize_with_eof()
34    }
35
36    // pub(crate) fn single_token(text: &'a str) -> Option<(SyntaxKind, Option<String>)> {
37    //     if text.is_empty() {
38    //         return None;
39    //     }
40
41    //     let token = tokenize(text).next()?;
42    //     if token.len as usize != text.len() {
43    //         return None;
44    //     }
45
46    //     let mut conv = Converter::new(text);
47    //     conv.extend_token(&token.kind, text);
48    //     match &*conv.res.kind {
49    //         [kind] => Some((*kind, conv.res.error.pop().map(|it| it.msg))),
50    //         _ => None,
51    //     }
52    // }
53
54    // pub(crate) fn as_str(&self) -> &str {
55    //     self.text
56    // }
57
58    pub(crate) fn len(&self) -> usize {
59        self.kind.len() - 1
60    }
61
62    // pub(crate) fn is_empty(&self) -> bool {
63    //     self.len() == 0
64    // }
65
66    pub(crate) fn kind(&self, i: usize) -> SyntaxKind {
67        assert!(i < self.len());
68        self.kind[i]
69    }
70
71    pub(crate) fn text(&self, i: usize) -> &str {
72        self.range_text(i..i + 1)
73    }
74
75    pub(crate) fn range_text(&self, r: ops::Range<usize>) -> &str {
76        assert!(r.start < r.end && r.end <= self.len());
77        let lo = self.start[r.start] as usize;
78        let hi = self.start[r.end] as usize;
79        &self.text[lo..hi]
80    }
81
82    // Naming is hard.
83    pub fn text_range(&self, i: usize) -> ops::Range<usize> {
84        assert!(i < self.len());
85        let lo = self.start[i] as usize;
86        let hi = self.start[i + 1] as usize;
87        lo..hi
88    }
89    pub fn text_start(&self, i: usize) -> usize {
90        assert!(i <= self.len());
91        self.start[i] as usize
92    }
93    // pub(crate) fn text_len(&self, i: usize) -> usize {
94    //     assert!(i < self.len());
95    //     let r = self.text_range(i);
96    //     r.end - r.start
97    // }
98
99    // pub(crate) fn error(&self, i: usize) -> Option<&str> {
100    //     assert!(i < self.len());
101    //     let err = self
102    //         .error
103    //         .binary_search_by_key(&(i as u32), |i| i.token)
104    //         .ok()?;
105    //     Some(self.error[err].msg.as_str())
106    // }
107
108    pub fn errors(&self) -> impl Iterator<Item = (&ops::Range<u32>, &str)> + '_ {
109        self.error.iter().map(|it| (&it.range, it.msg.as_str()))
110    }
111
112    fn push(&mut self, kind: SyntaxKind, offset: usize) {
113        self.kind.push(kind);
114        self.start.push(offset as u32);
115    }
116}
117
118struct Converter<'a> {
119    res: LexedStr<'a>,
120    offset: usize,
121    prefixed_string_continuation: Option<PrefixedStringKind>,
122}
123
124#[derive(Clone, Copy)]
125enum PrefixedStringKind {
126    Bit,
127    Byte,
128    Escape,
129}
130
131fn is_empty_quoted_ident(token_text: &str, uescape: bool) -> bool {
132    let inner = if uescape {
133        token_text
134            .strip_prefix(['u', 'U'])
135            .and_then(|s| s.strip_prefix('&'))
136    } else {
137        Some(token_text)
138    };
139    inner == Some("\"\"")
140}
141
142impl<'a> Converter<'a> {
143    fn new(text: &'a str) -> Self {
144        Self {
145            res: LexedStr {
146                text,
147                kind: Vec::new(),
148                start: Vec::new(),
149                error: Vec::new(),
150            },
151            offset: 0,
152            prefixed_string_continuation: None,
153        }
154    }
155
156    fn finalize_with_eof(mut self) -> LexedStr<'a> {
157        self.res.push(SyntaxKind::EOF, self.offset);
158        self.res
159    }
160
161    fn push(&mut self, kind: SyntaxKind, len: usize, err: Option<(&str, ops::Range<u32>)>) {
162        let token_start = self.offset as u32;
163        self.res.push(kind, self.offset);
164        self.offset += len;
165
166        if let Some((msg, err_range)) = err {
167            self.res.error.push(LexError {
168                msg: msg.to_owned(),
169                range: token_start + err_range.start..token_start + err_range.end,
170            });
171        }
172    }
173
174    fn extend_token(&mut self, kind: &squawk_lexer::TokenKind, token_text: &str) {
175        if !matches!(
176            kind,
177            squawk_lexer::TokenKind::Whitespace
178                | squawk_lexer::TokenKind::LineComment
179                | squawk_lexer::TokenKind::BlockComment { .. }
180                | squawk_lexer::TokenKind::Literal { .. }
181        ) {
182            self.prefixed_string_continuation = None;
183        }
184
185        // A note on an intended tradeoff:
186        // We drop some useful information here (see patterns with double dots `..`)
187        // Storing that info in `SyntaxKind` is not possible due to its layout requirements of
188        // being `u16` that come from `rowan::SyntaxKind`.
189        let mut err = "";
190        let mut err_range: Option<ops::Range<u32>> = None;
191
192        let syntax_kind = {
193            match kind {
194                squawk_lexer::TokenKind::LineComment => SyntaxKind::COMMENT,
195                squawk_lexer::TokenKind::BlockComment { terminated } => {
196                    if !terminated {
197                        err = "Missing trailing `*/` symbols to terminate the block comment";
198                    }
199                    SyntaxKind::COMMENT
200                }
201
202                squawk_lexer::TokenKind::Whitespace => SyntaxKind::WHITESPACE,
203                squawk_lexer::TokenKind::Ident => {
204                    SyntaxKind::from_keyword(token_text).unwrap_or(SyntaxKind::IDENT)
205                }
206                squawk_lexer::TokenKind::Literal { kind, .. } => {
207                    self.extend_literal(token_text, kind);
208                    return;
209                }
210                squawk_lexer::TokenKind::Semi => SyntaxKind::SEMICOLON,
211                squawk_lexer::TokenKind::Comma => SyntaxKind::COMMA,
212                squawk_lexer::TokenKind::Dot => SyntaxKind::DOT,
213                squawk_lexer::TokenKind::DotDot => SyntaxKind::DOT_DOT,
214                squawk_lexer::TokenKind::OpenParen => SyntaxKind::L_PAREN,
215                squawk_lexer::TokenKind::CloseParen => SyntaxKind::R_PAREN,
216                squawk_lexer::TokenKind::OpenBracket => SyntaxKind::L_BRACK,
217                squawk_lexer::TokenKind::CloseBracket => SyntaxKind::R_BRACK,
218                squawk_lexer::TokenKind::OpenCurly => SyntaxKind::L_CURLY,
219                squawk_lexer::TokenKind::CloseCurly => SyntaxKind::R_CURLY,
220                squawk_lexer::TokenKind::At => SyntaxKind::AT,
221                squawk_lexer::TokenKind::Pound => SyntaxKind::POUND,
222                squawk_lexer::TokenKind::Tilde => SyntaxKind::TILDE,
223                squawk_lexer::TokenKind::Question => SyntaxKind::QUESTION,
224                squawk_lexer::TokenKind::Colon => SyntaxKind::COLON,
225                squawk_lexer::TokenKind::Eq => SyntaxKind::EQ,
226                squawk_lexer::TokenKind::Bang => SyntaxKind::BANG,
227                squawk_lexer::TokenKind::Lt => SyntaxKind::L_ANGLE,
228                squawk_lexer::TokenKind::Gt => SyntaxKind::R_ANGLE,
229                squawk_lexer::TokenKind::Minus => SyntaxKind::MINUS,
230                squawk_lexer::TokenKind::And => SyntaxKind::AMP,
231                squawk_lexer::TokenKind::Or => SyntaxKind::PIPE,
232                squawk_lexer::TokenKind::Plus => SyntaxKind::PLUS,
233                squawk_lexer::TokenKind::Star => SyntaxKind::STAR,
234                squawk_lexer::TokenKind::Slash => SyntaxKind::SLASH,
235                squawk_lexer::TokenKind::Caret => SyntaxKind::CARET,
236                squawk_lexer::TokenKind::Percent => SyntaxKind::PERCENT,
237                squawk_lexer::TokenKind::Unknown => SyntaxKind::ERROR,
238                squawk_lexer::TokenKind::Eof => SyntaxKind::EOF,
239                squawk_lexer::TokenKind::Backtick => SyntaxKind::BACKTICK,
240                squawk_lexer::TokenKind::PositionalParam {
241                    trailing_junk_start,
242                } => {
243                    let digits = &token_text[1..*trailing_junk_start as usize];
244                    if digits.is_empty() {
245                        err = "missing parameter number";
246                        err_range = Some(0..1);
247                    } else if digits
248                        .parse::<i32>()
249                        .is_err_and(|err| matches!(err.kind(), IntErrorKind::PosOverflow))
250                    {
251                        err = "parameter number too large";
252                        err_range = Some(0..*trailing_junk_start);
253                    } else if (*trailing_junk_start as usize) < token_text.len() {
254                        err = "trailing junk after positional parameter";
255                        err_range = Some(*trailing_junk_start..token_text.len() as u32);
256                    }
257                    SyntaxKind::POSITIONAL_PARAM
258                }
259                squawk_lexer::TokenKind::QuotedIdent {
260                    terminated,
261                    uescape,
262                } => {
263                    if !terminated {
264                        err = "Missing trailing \" to terminate the quoted identifier"
265                    } else if is_empty_quoted_ident(token_text, *uescape) {
266                        err = "empty delimited identifier";
267                    }
268                    SyntaxKind::IDENT
269                }
270            }
271        };
272
273        let err = if err.is_empty() { None } else { Some(err) };
274        let err = err.map(|msg| (msg, err_range.unwrap_or(0..token_text.len() as u32)));
275        self.push(syntax_kind, token_text.len(), err);
276    }
277
278    fn extend_literal(&mut self, token_text: &str, kind: &squawk_lexer::LiteralKind) {
279        let mut err: Option<String> = None;
280        let mut err_range: Option<ops::Range<u32>> = None;
281        let continuation = self.prefixed_string_continuation.take();
282
283        let syntax_kind = match *kind {
284            squawk_lexer::LiteralKind::Int {
285                empty_int,
286                base,
287                trailing_junk_start,
288            } => {
289                if empty_int {
290                    err = Some("Missing digits after the integer base prefix".into());
291                } else {
292                    if matches!(base, squawk_lexer::Base::Binary | squawk_lexer::Base::Octal) {
293                        let prefix_len = 2u32;
294                        let digits = &token_text[prefix_len as usize..trailing_junk_start as usize];
295                        let base = base as u32;
296                        let token_start = self.offset as u32;
297                        for (i, c) in digits.char_indices() {
298                            if c != '_' && c.to_digit(base).is_none() {
299                                let start = token_start + prefix_len + i as u32;
300                                let end = start + c.len_utf8() as u32;
301                                self.res.error.push(LexError {
302                                    msg: format!("invalid digit for a base {base} literal"),
303                                    range: start..end,
304                                });
305                            }
306                        }
307                    }
308                    if (trailing_junk_start as usize) < token_text.len() {
309                        err = Some("trailing junk after numeric literal".into());
310                        err_range = Some(trailing_junk_start..token_text.len() as u32);
311                    }
312                }
313                SyntaxKind::INT_NUMBER
314            }
315            squawk_lexer::LiteralKind::Numeric {
316                empty_exponent_start,
317                trailing_junk_start,
318            } => {
319                if let Some(exponent_start) = empty_exponent_start {
320                    err = Some("Missing digits after the exponent symbol".into());
321                    err_range = Some(exponent_start..exponent_start + 1);
322                } else if (trailing_junk_start as usize) < token_text.len() {
323                    err = Some("trailing junk after numeric literal".into());
324                    err_range = Some(trailing_junk_start..token_text.len() as u32);
325                }
326                SyntaxKind::NUMERIC_NUMBER
327            }
328            squawk_lexer::LiteralKind::Str { terminated } => {
329                if !terminated {
330                    err =
331                        Some("Missing trailing `'` symbol to terminate the string literal".into());
332                } else if let Some(kind) = continuation {
333                    self.validate_prefixed_string_content(token_text, 1, kind);
334                    self.prefixed_string_continuation = Some(kind);
335                }
336                SyntaxKind::STRING
337            }
338            squawk_lexer::LiteralKind::NationalStr { terminated } => {
339                if !terminated {
340                    err = Some(
341                        "Missing trailing `'` symbol to terminate the national character string literal"
342                            .into(),
343                    );
344                }
345                SyntaxKind::NATIONAL_STRING
346            }
347            squawk_lexer::LiteralKind::ByteStr { terminated } => {
348                if !terminated {
349                    err = Some(
350                        "Missing trailing `'` symbol to terminate the hex bit string literal"
351                            .into(),
352                    );
353                } else {
354                    self.validate_prefixed_string_content(token_text, 2, PrefixedStringKind::Byte);
355                    self.prefixed_string_continuation = Some(PrefixedStringKind::Byte);
356                }
357                SyntaxKind::BYTE_STRING
358            }
359            squawk_lexer::LiteralKind::BitStr { terminated } => {
360                if !terminated {
361                    err = Some(
362                        "Missing trailing `'` symbol to terminate the bit string literal".into(),
363                    );
364                } else {
365                    self.validate_prefixed_string_content(token_text, 2, PrefixedStringKind::Bit);
366                    self.prefixed_string_continuation = Some(PrefixedStringKind::Bit);
367                }
368                SyntaxKind::BIT_STRING
369            }
370            squawk_lexer::LiteralKind::DollarQuotedString { terminated } => {
371                if !terminated {
372                    // TODO: we could be fancier and say the ending string we're looking for
373                    err = Some("Unterminated dollar quoted string literal".into());
374                }
375                SyntaxKind::DOLLAR_QUOTED_STRING
376            }
377            squawk_lexer::LiteralKind::UnicodeEscStr { terminated } => {
378                if !terminated {
379                    err = Some(
380                        "Missing trailing `'` symbol to terminate the unicode escape string literal"
381                            .into(),
382                    );
383                }
384                // validated in squawk_syntax
385                SyntaxKind::UNICODE_ESC_STRING
386            }
387            squawk_lexer::LiteralKind::EscStr { terminated } => {
388                if !terminated {
389                    err = Some(
390                        "Missing trailing `'` symbol to terminate the escape string literal".into(),
391                    );
392                } else {
393                    self.validate_prefixed_string_content(
394                        token_text,
395                        2,
396                        PrefixedStringKind::Escape,
397                    );
398                    self.prefixed_string_continuation = Some(PrefixedStringKind::Escape);
399                }
400                SyntaxKind::ESC_STRING
401            }
402        };
403
404        let err = err
405            .as_deref()
406            .map(|msg| (msg, err_range.unwrap_or(0..token_text.len() as u32)));
407        self.push(syntax_kind, token_text.len(), err);
408    }
409
410    fn validate_prefixed_string_content(
411        &mut self,
412        token_text: &str,
413        inner_start: usize,
414        kind: PrefixedStringKind,
415    ) {
416        let inner = &token_text[inner_start..token_text.len() - 1];
417        match kind {
418            PrefixedStringKind::Bit => {
419                for (i, c) in inner.char_indices() {
420                    if !matches!(c, '0' | '1') {
421                        self.push_content_error(
422                            format!(r#""{c}" is not a valid binary digit"#),
423                            inner_start + i,
424                            c.len_utf8(),
425                        );
426                    }
427                }
428            }
429            PrefixedStringKind::Byte => {
430                for (i, c) in inner.char_indices() {
431                    if !c.is_ascii_hexdigit() {
432                        self.push_content_error(
433                            format!(r#""{c}" is not a valid hexadecimal digit"#),
434                            inner_start + i,
435                            c.len_utf8(),
436                        );
437                    }
438                }
439            }
440            PrefixedStringKind::Escape => {
441                let mut chars = inner.char_indices().peekable();
442                while let Some((escape_start, c)) = chars.next() {
443                    if c != '\\' {
444                        continue;
445                    }
446                    let Some((next_pos, next_c)) = chars.next() else {
447                        break;
448                    };
449                    let (required, example) = match next_c {
450                        'u' => (4usize, r"\uXXXX"),
451                        'U' => (8usize, r"\UXXXXXXXX"),
452                        _ => continue,
453                    };
454                    let mut end = next_pos + next_c.len_utf8();
455                    let mut got_all = true;
456                    for _ in 0..required {
457                        match chars.peek() {
458                            Some(&(i, ch)) if ch.is_ascii_hexdigit() => {
459                                end = i + ch.len_utf8();
460                                chars.next();
461                            }
462                            _ => {
463                                got_all = false;
464                                break;
465                            }
466                        }
467                    }
468                    if !got_all {
469                        self.push_content_error(
470                            format!("Unicode escape requires {required} hex digits: {example}"),
471                            inner_start + escape_start,
472                            end - escape_start,
473                        );
474                    }
475                }
476            }
477        }
478    }
479
480    fn push_content_error(&mut self, msg: String, start: usize, len: usize) {
481        let token_start = self.offset as u32;
482        let start = token_start + start as u32;
483        self.res.error.push(LexError {
484            msg,
485            range: start..start + len as u32,
486        });
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle};
493    use insta::{assert_debug_snapshot, assert_snapshot};
494
495    use super::LexedStr;
496
497    fn lex(text: &str) -> String {
498        let lexed = LexedStr::new(text);
499        let renderer = Renderer::plain().decor_style(DecorStyle::Unicode);
500        let mut res = String::new();
501
502        for (range, msg) in lexed.errors() {
503            let span = range.start as usize..range.end as usize;
504            let group = Level::ERROR.primary_title(msg).element(
505                Snippet::source(text)
506                    .fold(true)
507                    .annotation(AnnotationKind::Primary.span(span)),
508            );
509            res.push_str(&renderer.render(&[group]).to_string());
510            res.push('\n');
511        }
512
513        res
514    }
515
516    fn lex_errors(text: &str) -> Vec<(std::ops::Range<u32>, String)> {
517        LexedStr::new(text)
518            .errors()
519            .map(|(range, msg)| (range.clone(), msg.to_owned()))
520            .collect()
521    }
522
523    #[test]
524    fn prefixed_string_content_errors() {
525        assert_debug_snapshot!(lex_errors("B'102' X'1G' E'\\u00'"), @r#"
526        [
527            (
528                4..5,
529                "\"2\" is not a valid binary digit",
530            ),
531            (
532                10..11,
533                "\"G\" is not a valid hexadecimal digit",
534            ),
535            (
536                15..19,
537                "Unicode escape requires 4 hex digits: \\uXXXX",
538            ),
539        ]
540        "#);
541    }
542
543    #[test]
544    fn prefixed_string_continuations_use_the_initial_string_kind() {
545        assert_debug_snapshot!(lex_errors("B'0'\n'2'"), @r#"
546        [
547            (
548                6..7,
549                "\"2\" is not a valid binary digit",
550            ),
551        ]
552        "#);
553        assert_debug_snapshot!(lex_errors("X'F'\n'G'"), @r#"
554        [
555            (
556                6..7,
557                "\"G\" is not a valid hexadecimal digit",
558            ),
559        ]
560        "#);
561        assert_debug_snapshot!(lex_errors("E'ok'\n'\\u0'"), @r#"
562        [
563            (
564                7..10,
565                "Unicode escape requires 4 hex digits: \\uXXXX",
566            ),
567        ]
568        "#);
569    }
570
571    #[test]
572    fn prefixed_string_continuation_state_resets() {
573        assert_debug_snapshot!(
574            lex_errors("B'01' || '2'; X'0F' N'x' 'G'; E'ok' $tag$x$tag$ '\\u0'"),
575            @"[]"
576        );
577        assert_debug_snapshot!(lex_errors("B'01' 2 '2'; X'0F' 1.5 'G'"), @"[]");
578    }
579
580    #[test]
581    fn empty_int_error() {
582        assert_snapshot!(lex("select 0x;"), @"
583        error: Missing digits after the integer base prefix
584          ╭▸ 
585        1 │ select 0x;
586          ╰╴       ━━
587        ");
588    }
589
590    #[test]
591    fn empty_int_with_trailing_ident_error() {
592        assert_snapshot!(lex("select 0xg;"), @"
593        error: trailing junk after numeric literal
594          ╭▸ 
595        1 │ select 0xg;
596          ╰╴         ━
597        ");
598    }
599
600    #[test]
601    fn invalid_octal_digits_error() {
602        assert_snapshot!(lex("select 0o999;"), @"
603        error: invalid digit for a base 8 literal
604          ╭▸ 
605        1 │ select 0o999;
606          ╰╴         ━
607        error: invalid digit for a base 8 literal
608          ╭▸ 
609        1 │ select 0o999;
610          ╰╴          ━
611        error: invalid digit for a base 8 literal
612          ╭▸ 
613        1 │ select 0o999;
614          ╰╴           ━
615        ");
616    }
617
618    #[test]
619    fn invalid_binary_digits_error() {
620        assert_snapshot!(lex("select 0b234;"), @"
621        error: invalid digit for a base 2 literal
622          ╭▸ 
623        1 │ select 0b234;
624          ╰╴         ━
625        error: invalid digit for a base 2 literal
626          ╭▸ 
627        1 │ select 0b234;
628          ╰╴          ━
629        error: invalid digit for a base 2 literal
630          ╭▸ 
631        1 │ select 0b234;
632          ╰╴           ━
633        ");
634    }
635
636    #[test]
637    fn invalid_octal_digits_after_valid_error() {
638        assert_snapshot!(lex("select 0o7889;"), @"
639        error: invalid digit for a base 8 literal
640          ╭▸ 
641        1 │ select 0o7889;
642          ╰╴          ━
643        error: invalid digit for a base 8 literal
644          ╭▸ 
645        1 │ select 0o7889;
646          ╰╴           ━
647        error: invalid digit for a base 8 literal
648          ╭▸ 
649        1 │ select 0o7889;
650          ╰╴            ━
651        ");
652    }
653
654    #[test]
655    fn empty_exponent_error() {
656        assert_snapshot!(lex("select 1e;"), @"
657        error: Missing digits after the exponent symbol
658          ╭▸ 
659        1 │ select 1e;
660          ╰╴        ━
661        ");
662    }
663
664    #[test]
665    fn unterminated_string_error() {
666        assert_snapshot!(lex("select 'hello;"), @"
667        error: Missing trailing `'` symbol to terminate the string literal
668          ╭▸ 
669        1 │ select 'hello;
670          ╰╴       ━━━━━━━
671        ");
672    }
673
674    #[test]
675    fn unterminated_hex_bit_string_error() {
676        assert_snapshot!(lex("select X'1F;"), @"
677        error: Missing trailing `'` symbol to terminate the hex bit string literal
678          ╭▸ 
679        1 │ select X'1F;
680          ╰╴       ━━━━━
681        ");
682    }
683
684    #[test]
685    fn unterminated_bit_string_error() {
686        assert_snapshot!(lex("select B'101;"), @"
687        error: Missing trailing `'` symbol to terminate the bit string literal
688          ╭▸ 
689        1 │ select B'101;
690          ╰╴       ━━━━━━
691        ");
692    }
693
694    #[test]
695    fn unterminated_dollar_quoted_string_error() {
696        assert_snapshot!(lex("select $tag$hello;"), @"
697        error: Unterminated dollar quoted string literal
698          ╭▸ 
699        1 │ select $tag$hello;
700          ╰╴       ━━━━━━━━━━━
701        ");
702    }
703
704    #[test]
705    fn unterminated_unicode_escape_string_error() {
706        assert_snapshot!(lex("select U&'hello;"), @"
707        error: Missing trailing `'` symbol to terminate the unicode escape string literal
708          ╭▸ 
709        1 │ select U&'hello;
710          ╰╴       ━━━━━━━━━
711        ");
712    }
713
714    #[test]
715    fn unterminated_escape_string_error() {
716        assert_snapshot!(lex("select E'hello;"), @"
717        error: Missing trailing `'` symbol to terminate the escape string literal
718          ╭▸ 
719        1 │ select E'hello;
720          ╰╴       ━━━━━━━━
721        ");
722    }
723}