LCOV - code coverage report
Current view: top level - dashql-parser/src - scanner.rs Coverage Total Hit
Test: DashQL d805a4b Lines: 94.8 % 442 419
Test Date: 2026-11-09 20:22:13

            Line data    Source code
       1              : //! The SQL scanner.
       2              : //!
       3              : //! The rules of every flex-style start condition are a logos lexer: the longest match wins, and
       4              : //! logos priorities break ties. The lexer of the current start condition matches one token at a
       5              : //! time, so that actions can switch the start condition and put text back with `yyless`. See
       6              : //! docs/design/core_quirks.md for the quirks of the scanner.
       7              : //!
       8              : //! [`scan`] then applies a lookahead filter, which turns `NOT`, `NULLS`, `WITH` and `.` into their
       9              : //! `*_LA` variants depending on the following tokens.
      10              : 
      11              : use crate::keywords::Keyword;
      12              : use crate::names::{NameId, NameRegistry};
      13              : use crate::span::TextSpan;
      14              : use crate::{symbol_kind::*, SymbolKind};
      15              : use logos::Logos;
      16              : use std::collections::VecDeque;
      17              : 
      18              : /// The semantic value of a scanned symbol
      19              : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
      20              : pub enum SymbolValue {
      21              :     None,
      22              :     /// An identifier
      23              :     Name(NameId),
      24              :     /// The text of a keyword
      25              :     Text(&'static str),
      26              : }
      27              : 
      28              : /// A scanned symbol
      29              : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
      30              : pub struct Symbol {
      31              :     /// The terminal symbol kind
      32              :     pub kind: SymbolKind,
      33              :     /// The byte span in the text
      34              :     pub location: TextSpan,
      35              :     /// The semantic value
      36              :     pub value: SymbolValue,
      37              : }
      38              : 
      39              : impl Symbol {
      40       155534 :     fn new(kind: SymbolKind, location: TextSpan) -> Self {
      41       155534 :         Self { kind, location, value: SymbolValue::None }
      42       155534 :     }
      43              : }
      44              : 
      45              : /// A literal that Hyper replaced with a typed marker comment, e.g. `/*Integer(4888)*/`
      46              : #[derive(Clone, Debug, PartialEq, Eq)]
      47              : pub struct ObfuscatedLiteral {
      48              :     /// The location of the marker
      49              :     pub location: TextSpan,
      50              :     /// An executable literal derived from the marker hash, e.g. `18568`
      51              :     pub replacement: String,
      52              : }
      53              : 
      54              : /// A scanner error
      55              : #[derive(Clone, Debug, PartialEq, Eq)]
      56              : pub struct ScannerError {
      57              :     /// The byte span of the error
      58              :     pub location: TextSpan,
      59              :     /// The error message
      60              :     pub message: String,
      61              : }
      62              : 
      63              : /// The output of the scanner
      64              : #[derive(Clone, Debug, Default)]
      65              : pub struct ScannedScript {
      66              :     /// The origin id
      67              :     pub external_id: u32,
      68              :     /// The text version
      69              :     pub text_version: u64,
      70              :     /// The text
      71              :     pub text: String,
      72              :     /// The scanner errors
      73              :     pub errors: Vec<ScannerError>,
      74              :     /// The line breaks (`\n`, `\r\n` or `\r`), including those in comments and literals
      75              :     pub line_breaks: Vec<TextSpan>,
      76              :     /// The comments
      77              :     pub comments: Vec<TextSpan>,
      78              :     /// The obfuscated literals, ordered by offset
      79              :     pub obfuscated_literals: Vec<ObfuscatedLiteral>,
      80              :     /// The names
      81              :     pub names: NameRegistry,
      82              :     /// The symbols, ending with EOF
      83              :     pub symbols: Vec<Symbol>,
      84              : }
      85              : 
      86              : /// Scan a script.
      87         7997 : pub fn scan(text: &str, text_version: u64, external_id: u32) -> ScannedScript {
      88         7997 :     let mut out = ScannedScript {
      89         7997 :         external_id,
      90         7997 :         text_version,
      91         7997 :         text: text.to_string(),
      92         7997 :         symbols: Vec::with_capacity(text.len() / 4 + 8),
      93         7997 :         ..Default::default()
      94         7997 :     };
      95         7997 :     out.line_breaks = line_breaks(text.as_bytes());
      96         7997 :     let mut lexer = Lexer::new(text.as_bytes());
      97         7997 :     let mut lookahead: VecDeque<Symbol> = VecDeque::new();
      98              :     loop {
      99       350046 :         let symbol = next_symbol(&mut lexer, &mut out, &mut lookahead);
     100       350046 :         out.symbols.push(symbol);
     101       350046 :         if symbol.kind == S_YYEOF {
     102         7997 :             break;
     103       342049 :         }
     104              :     }
     105         7997 :     out
     106         7997 : }
     107              : 
     108              : /// Find the line breaks of a text, `\r\n` is one line break.
     109         7997 : fn line_breaks(text: &[u8]) -> Vec<TextSpan> {
     110         7997 :     let mut breaks = Vec::new();
     111         7997 :     let mut i = 0;
     112      2003860 :     while i < text.len() {
     113      1995863 :         match text[i] {
     114            2 :             b'\r' if text.get(i + 1) == Some(&b'\n') => {
     115            1 :                 breaks.push(Lexer::span(i, 2));
     116            1 :                 i += 1;
     117            1 :             }
     118        39047 :             b'\n' | b'\r' => breaks.push(Lexer::span(i, 1)),
     119      1956815 :             _ => {}
     120              :         }
     121      1995863 :         i += 1;
     122              :     }
     123         7997 :     breaks
     124         7997 : }
     125              : 
     126              : /// The lookahead filter of [`scan`].
     127       350046 : fn next_symbol(lexer: &mut Lexer, out: &mut ScannedScript, lookahead: &mut VecDeque<Symbol>) -> Symbol {
     128       350046 :     let current = match lookahead.pop_front() {
     129        14937 :         Some(symbol) => symbol,
     130       335109 :         None => lexer.lex(out),
     131              :     };
     132       350046 :     match current.kind {
     133        14937 :         S_NOT | S_NULLS_P | S_WITH | S_DOT => {}
     134       335109 :         _ => return current,
     135              :     }
     136        14937 :     let next = lexer.lex(out);
     137        14937 :     lookahead.push_back(next);
     138        14937 :     match (current.kind, next.kind) {
     139           90 :         (S_NOT, S_BETWEEN | S_IN_P | S_LIKE | S_ILIKE | S_GLOB | S_SIMILAR) => Symbol::new(S_NOT_LA, current.location),
     140         1457 :         (S_NULLS_P, S_FIRST_P | S_LAST_P) => Symbol::new(S_NULLS_LA, current.location),
     141           36 :         (S_WITH, S_TIME | S_ORDINALITY) => Symbol::new(S_WITH_LA, current.location),
     142           26 :         (S_DOT, S_STAR) => Symbol::new(S_DOT_STAR_LA, current.location),
     143        13328 :         _ => current,
     144              :     }
     145       350046 : }
     146              : 
     147              : /// The flex start conditions
     148              : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     149              : enum State {
     150              :     Initial,
     151              :     /// Bit string literal
     152              :     Xb,
     153              :     /// Backtick-quoted identifier
     154              :     Xbt,
     155              :     /// C-style comment
     156              :     Xc,
     157              :     /// Double-quoted identifier
     158              :     Xd,
     159              :     /// Hexadecimal string literal
     160              :     Xh,
     161              :     /// Quoted string
     162              :     Xq,
     163              :     /// Dollar-quoted string
     164              :     Xdq,
     165              : }
     166              : 
     167              : // The rules of every start condition form a logos lexer. Logos takes the longest match like flex,
     168              : // and resolves a tie by priority. The default priority of a pattern is 2 for every byte or class it
     169              : // has to match, which already ranks `{comment}`, `{xcstart}` and the two-character operators above
     170              : // `{operator}`. The explicit priorities rank the remaining ties.
     171              : //
     172              : // Like flex, the lexers match bytes, not characters (`utf8 = false` and byte-string patterns), so
     173              : // `\x80-\xFF` are identifier bytes.
     174              : 
     175              : /// The rules of INITIAL
     176              : #[derive(Logos, Clone, Copy, Debug, PartialEq, Eq)]
     177              : #[logos(utf8 = false)]
     178              : #[logos(subpattern space = br"[ \t\n\r\f]")]
     179              : #[logos(subpattern op_chars = br"[~!@#^&|+\-*/%<>=]")]
     180              : #[logos(subpattern integer = br"[0-9]+")]
     181              : #[logos(subpattern decimal = br"[0-9]*\.[0-9]+|[0-9]+\.[0-9]*")]
     182              : enum Initial {
     183              :     /// `{space}+`
     184              :     #[regex(br"(?&space)+")]
     185              :     Space,
     186              :     /// `{comment}`
     187              :     #[regex(br"--[^\n\r]*", allow_greedy = true)]
     188              :     Comment,
     189              :     /// The Hyper obfuscation markers, case-sensitive unlike the other rules
     190              :     #[regex(br"/\*String\([0-9A-F]{4}\)\*/", |_| S_SCONST)]
     191              :     #[regex(br"/\*Integer\([0-9A-F]{4}\)\*/", |_| S_ICONST)]
     192              :     #[regex(br"/\*Float\([0-9A-F]{4}\)\*/", |_| S_FCONST)]
     193              :     #[regex(br"/\*HexString\([0-9A-F]{4}\)\*/", |_| S_XCONST)]
     194              :     #[regex(br"/\*BitString\([0-9A-F]{4}\)\*/", |_| S_BCONST)]
     195              :     ObfuscatedLiteral(SymbolKind),
     196              :     /// `{xcstart}`: matches as much as `{operator}` would, the action puts the operator back
     197              :     #[regex(br"/\*(?&op_chars)*")]
     198              :     CommentStart,
     199              :     /// `{xbstart}`, `{xhstart}`, `{xqstart}`, `{xdstart}`, `{xbtstart}`
     200              :     #[regex(b"[bB]'", |_| State::Xb)]
     201              :     #[regex(b"[xX]'", |_| State::Xh)]
     202              :     #[token("'", |_| State::Xq)]
     203              :     #[token("\"", |_| State::Xd)]
     204              :     #[token("`", |_| State::Xbt)]
     205              :     #[token("$$", |_| State::Xdq)]
     206              :     Begin(State),
     207              :     /// The operators and punctuation with a token of their own
     208              :     #[token("::", |_| S_TYPECAST)]
     209              :     #[token("..", |_| S_DOT_DOT)]
     210              :     #[token(".", |_| S_DOT)]
     211              :     #[token(":=", |_| S_COLON_EQUALS)]
     212              :     #[token("=>", |_| S_EQUALS_GREATER)]
     213              :     #[token("<=", |_| S_LESS_EQUALS)]
     214              :     #[token(">=", |_| S_GREATER_EQUALS)]
     215              :     #[token("<>", |_| S_NOT_EQUALS)]
     216              :     #[token("!=", |_| S_NOT_EQUALS)]
     217              :     #[token(",", |_| S_COMMA)]
     218              :     #[token("(", |_| S_LRB)]
     219              :     #[token(")", |_| S_RRB)]
     220              :     #[token("[", |_| S_LSB)]
     221              :     #[token("]", |_| S_RSB)]
     222              :     #[token("{", |_| S_LCB)]
     223              :     #[token("}", |_| S_RCB)]
     224              :     #[token(";", |_| S_SEMICOLON)]
     225              :     #[token(":", |_| S_COLON)]
     226              :     #[token("+", |_| S_PLUS)]
     227              :     #[token("-", |_| S_MINUS)]
     228              :     #[token("/", |_| S_DIVIDE)]
     229              :     #[token("*", |_| S_STAR)]
     230              :     #[token("%", |_| S_MODULO)]
     231              :     #[token("^", |_| S_CIRCUMFLEX)]
     232              :     #[token("&", |_| S_AMPERSAND)]
     233              :     #[token("|", |_| S_PIPE)]
     234              :     #[token("!", |_| S_EXCLAMATION_MARK)]
     235              :     #[token("<", |_| S_LESS_THAN)]
     236              :     #[token(">", |_| S_GREATER_THAN)]
     237              :     #[token("=", |_| S_EQUALS)]
     238              :     #[token("$", |_| S_DOLLAR)]
     239              :     Token(SymbolKind),
     240              :     /// `{raw_dot_trailing}`, the action puts the space back
     241              :     #[regex(br"\.(?&space)")]
     242              :     DotTrailing,
     243              :     /// `{raw_question_mark}`, the action checks the trailing context
     244              :     #[token("?")]
     245              :     QuestionMark,
     246              :     /// `{operator}`, below the single-character tokens
     247              :     #[regex(br"(?&op_chars)+", priority = 1)]
     248              :     Operator,
     249              :     /// `{integer}`
     250              :     #[regex(br"(?&integer)")]
     251              :     Integer,
     252              :     /// `{decimal}`, `{real}`
     253              :     #[regex(br"(?&decimal)")]
     254              :     #[regex(br"((?&integer)|(?&decimal))[Ee][-+]?[0-9]+")]
     255              :     Float,
     256              :     /// `{decimalfail}`
     257              :     #[regex(br"(?&integer)\.\.")]
     258              :     DecimalFail,
     259              :     /// `{realfail1}`
     260              :     #[regex(br"((?&integer)|(?&decimal))[Ee]")]
     261              :     RealFail1,
     262              :     /// `{realfail2}`
     263              :     #[regex(br"((?&integer)|(?&decimal))[Ee][-+]")]
     264              :     RealFail2,
     265              :     /// `{identifier}`
     266              :     #[regex(br"[A-Za-z\x80-\xFF_][A-Za-z\x80-\xFF_0-9$]*")]
     267              :     Identifier,
     268              :     /// `{other}`, below everything else
     269              :     #[regex(br"[^\n]", priority = 0)]
     270              :     Other,
     271              : }
     272              : 
     273              : /// The rules of xc
     274              : #[derive(Logos, Clone, Copy, Debug, PartialEq, Eq)]
     275              : #[logos(utf8 = false)]
     276              : enum Xc {
     277              :     /// `{xcstart}`
     278              :     #[regex(br"/\*[~!@#^&|+\-*/%<>=]*")]
     279              :     CommentStart,
     280              :     /// `{xcstop}`
     281              :     #[regex(br"\*+/")]
     282              :     CommentStop,
     283              :     /// `{xcinside}`, `{op_chars}`, `\*+`
     284              :     #[regex(br"[^*/]+")]
     285              :     #[regex(br"\*+")]
     286              :     #[token("/")]
     287              :     Inside,
     288              : }
     289              : 
     290              : /// The rules of xb and xh
     291              : #[derive(Logos, Clone, Copy, Debug, PartialEq, Eq)]
     292              : #[logos(utf8 = false)]
     293              : enum XbXh {
     294              :     /// The closing quote, the action checks for a continuation (`{quotecontinue}`)
     295              :     #[token("'")]
     296              :     QuoteStop,
     297              :     /// `{xbinside}`, `{xhinside}`
     298              :     #[regex(br"[^']+")]
     299              :     Inside,
     300              : }
     301              : 
     302              : /// The rules of xq
     303              : #[derive(Logos, Clone, Copy, Debug, PartialEq, Eq)]
     304              : #[logos(utf8 = false)]
     305              : enum Xq {
     306              :     /// The closing quote
     307              :     #[token("'")]
     308              :     QuoteStop,
     309              :     /// `{xqdouble}`, `{xqinside}`
     310              :     #[token("''")]
     311              :     #[regex(br"[^']+")]
     312              :     Inside,
     313              : }
     314              : 
     315              : /// The rules of xdq, the text between `$$` is verbatim
     316              : #[derive(Logos, Clone, Copy, Debug, PartialEq, Eq)]
     317              : #[logos(utf8 = false)]
     318              : enum Xdq {
     319              :     /// The closing `$$`
     320              :     #[token("$$")]
     321              :     Stop,
     322              :     /// A lone `$` or text without one
     323              :     #[token("$")]
     324              :     #[regex(br"[^$]+")]
     325              :     Inside,
     326              : }
     327              : 
     328              : /// The rules of xd
     329              : #[derive(Logos, Clone, Copy, Debug, PartialEq, Eq)]
     330              : #[logos(utf8 = false)]
     331              : enum Xd {
     332              :     /// `{xddouble}`, `{xdinside}`
     333              :     #[token("\"\"")]
     334              :     #[regex(br#"[^"]+"#)]
     335              :     Inside,
     336              :     /// `{xdstop}`
     337              :     #[token("\"")]
     338              :     Stop,
     339              : }
     340              : 
     341              : /// The rules of xbt
     342              : #[derive(Logos, Clone, Copy, Debug, PartialEq, Eq)]
     343              : #[logos(utf8 = false)]
     344              : enum Xbt {
     345              :     /// `{xbtdouble}`, `{xbtinside}`
     346              :     #[token("``")]
     347              :     #[regex(br"[^`]+")]
     348              :     Inside,
     349              :     /// `{xbtstop}`
     350              :     #[token("`")]
     351              :     Stop,
     352              : }
     353              : 
     354              : /// `{ident_cont}`
     355           50 : fn is_ident_cont(c: u8) -> bool {
     356           50 :     c.is_ascii_alphanumeric() || c == b'_' || c == b'$' || c >= 0x80
     357           50 : }
     358              : 
     359              : /// The token of a single special character
     360           54 : fn special_character(c: u8) -> Option<SymbolKind> {
     361           54 :     Some(match c {
     362            0 :         b',' => S_COMMA,
     363            0 :         b'(' => S_LRB,
     364            0 :         b')' => S_RRB,
     365            0 :         b'[' => S_LSB,
     366            0 :         b']' => S_RSB,
     367            0 :         b'{' => S_LCB,
     368            0 :         b'}' => S_RCB,
     369            0 :         b'.' => S_DOT,
     370            0 :         b';' => S_SEMICOLON,
     371            0 :         b':' => S_COLON,
     372           17 :         b'+' => S_PLUS,
     373            0 :         b'-' => S_MINUS,
     374            3 :         b'*' => S_STAR,
     375            2 :         b'/' => S_DIVIDE,
     376            0 :         b'%' => S_MODULO,
     377            0 :         b'?' => S_QUESTION_MARK,
     378            0 :         b'^' => S_CIRCUMFLEX,
     379            0 :         b'&' => S_AMPERSAND,
     380            0 :         b'|' => S_PIPE,
     381            0 :         b'!' => S_EXCLAMATION_MARK,
     382           27 :         b'<' => S_LESS_THAN,
     383            2 :         b'>' => S_GREATER_THAN,
     384            3 :         b'=' => S_EQUALS,
     385            0 :         _ => return None,
     386              :     })
     387           54 : }
     388              : 
     389              : /// The two-character operator tokens that also match `{operator}`
     390           15 : fn two_char_operator(a: u8, b: u8) -> Option<SymbolKind> {
     391           15 :     Some(match (a, b) {
     392            5 :         (b'=', b'>') => S_EQUALS_GREATER,
     393            2 :         (b'<', b'=') => S_LESS_EQUALS,
     394            6 :         (b'>', b'=') => S_GREATER_EQUALS,
     395            2 :         (b'<', b'>') | (b'!', b'=') => S_NOT_EQUALS,
     396            0 :         _ => return None,
     397              :     })
     398           15 : }
     399              : 
     400              : /// The scanner, switches between the lexers of the start conditions like the flex scanner
     401              : struct Lexer<'a> {
     402              :     /// The script text
     403              :     text: &'a [u8],
     404              :     /// The byte offset of the next match
     405              :     pos: usize,
     406              :     /// The current start condition
     407              :     state: State,
     408              :     /// The start of the current extended token (quoted literal, identifier or comment)
     409              :     ext_begin: TextSpan,
     410              :     /// The nesting depth of C-style comments
     411              :     ext_depth: usize,
     412              :     /// A buffer for lower-casing and unescaping names
     413              :     buffer: Vec<u8>,
     414              : }
     415              : 
     416              : impl<'a> Lexer<'a> {
     417         7997 :     fn new(text: &'a [u8]) -> Self {
     418         7997 :         Self {
     419         7997 :             text,
     420         7997 :             pos: 0,
     421         7997 :             state: State::Initial,
     422         7997 :             ext_begin: TextSpan::default(),
     423         7997 :             ext_depth: 0,
     424         7997 :             buffer: Vec::with_capacity(64),
     425         7997 :         }
     426         7997 :     }
     427              : 
     428       635818 :     fn span(begin: usize, length: usize) -> TextSpan {
     429       635818 :         TextSpan::new(begin as u32, length as u32)
     430       635818 :     }
     431              : 
     432              :     /// The empty location at the end of the text
     433         8031 :     fn eof_location(&self) -> TextSpan {
     434         8031 :         Self::span(self.text.len(), 0)
     435         8031 :     }
     436              : 
     437              :     /// Match the rules of a start condition at the current position and consume the match.
     438       588738 :     fn next_match<T>(&mut self) -> (T, TextSpan)
     439       588738 :     where
     440       588738 :         T: Logos<'a, Source = [u8], Extras = ()>,
     441              :     {
     442       588738 :         let start = self.pos;
     443       588738 :         let mut lexer = T::lexer(&self.text[start..]);
     444              :         // Every byte starts a match in every start condition.
     445       588738 :         let Some(Ok(token)) = lexer.next() else {
     446            0 :             unreachable!("no rule matches at {start}")
     447              :         };
     448       588738 :         let length = lexer.span().end;
     449       588738 :         self.pos = start + length;
     450       588738 :         (token, Self::span(start, length))
     451       588738 :     }
     452              : 
     453              :     /// Scan the next raw token (before the lookahead filter)
     454       350046 :     fn lex(&mut self, out: &mut ScannedScript) -> Symbol {
     455              :         loop {
     456       596769 :             if self.pos >= self.text.len() {
     457         8031 :                 let eof = self.eof_location();
     458         8031 :                 let message = match self.state {
     459         7997 :                     State::Initial => return Symbol::new(S_YYEOF, eof),
     460            5 :                     State::Xc => "unterminated comment",
     461            2 :                     State::Xb => "unterminated bit string literal",
     462            2 :                     State::Xh => "unterminated hexadecimal bit string literal",
     463           19 :                     State::Xq => "unterminated quoted string",
     464            2 :                     State::Xdq => "unterminated dollar-quoted string",
     465            2 :                     State::Xd => "unterminated delimited identifier",
     466            2 :                     State::Xbt => "unterminated backtick-quoted identifier",
     467              :                 };
     468           34 :                 out.errors.push(ScannerError {
     469           34 :                     location: self.ext_begin.cover(eof),
     470           34 :                     message: message.to_string(),
     471           34 :                 });
     472           34 :                 self.state = State::Initial;
     473           34 :                 continue;
     474       588738 :             }
     475       588738 :             let symbol = match self.state {
     476              :                 State::Initial => {
     477       564262 :                     let (token, loc) = self.next_match::<Initial>();
     478       564262 :                     self.initial(out, token, loc)
     479              :                 }
     480         1808 :                 State::Xc => match self.next_match::<Xc>() {
     481           15 :                     (Xc::CommentStart, mut loc) => {
     482           15 :                         self.yyless(&mut loc, 2);
     483           15 :                         self.ext_depth += 1;
     484           15 :                         None
     485              :                     }
     486          502 :                     (Xc::CommentStop, loc) => {
     487          502 :                         self.ext_depth -= 1;
     488          502 :                         if self.ext_depth == 0 {
     489          487 :                             out.comments.push(self.ext_begin.cover(loc));
     490          487 :                             self.state = State::Initial;
     491          487 :                         }
     492          502 :                         None
     493              :                     }
     494         1291 :                     (Xc::Inside, _) => None,
     495              :                 },
     496           68 :                 State::Xb | State::Xh => match self.next_match::<XbXh>() {
     497           32 :                     (XbXh::QuoteStop, loc) => {
     498           32 :                         if self.quote_continue(out) {
     499            1 :                             None
     500              :                         } else {
     501           31 :                             Some(self.quote_stop(loc))
     502              :                         }
     503              :                     }
     504           36 :                     (XbXh::Inside, _) => None,
     505              :                 },
     506        20375 :                 State::Xq => match self.next_match::<Xq>() {
     507        10142 :                     (Xq::QuoteStop, loc) => Some(self.quote_stop(loc)),
     508        10233 :                     (Xq::Inside, _) => None,
     509              :                 },
     510           49 :                 State::Xdq => match self.next_match::<Xdq>() {
     511           21 :                     (Xdq::Stop, loc) => {
     512           21 :                         self.state = State::Initial;
     513           21 :                         Some(Symbol::new(S_DCONST, self.ext_begin.cover(loc)))
     514              :                     }
     515           28 :                     (Xdq::Inside, _) => None,
     516              :                 },
     517         1676 :                 State::Xd => match self.next_match::<Xd>() {
     518          819 :                     (Xd::Stop, loc) => Some(self.identifier_stop(out, loc, b'"', "zero-length delimited identifier")),
     519          857 :                     (Xd::Inside, _) => None,
     520              :                 },
     521          500 :                 State::Xbt => match self.next_match::<Xbt>() {
     522          238 :                     (Xbt::Stop, loc) => {
     523          238 :                         Some(self.identifier_stop(out, loc, b'`', "zero-length backtick-quoted identifier"))
     524              :                     }
     525          262 :                     (Xbt::Inside, _) => None,
     526              :                 },
     527              :             };
     528       588738 :             if let Some(symbol) = symbol {
     529       342049 :                 return symbol;
     530       246689 :             }
     531              :         }
     532       350046 :     }
     533              : 
     534              :     /// Put back all but the first `n` characters of the current match (`yyless(n)`).
     535          597 :     fn yyless(&mut self, loc: &mut TextSpan, n: usize) {
     536          597 :         self.pos = loc.offset as usize + n;
     537          597 :         loc.length = n as u32;
     538          597 :     }
     539              : 
     540              :     /// Run the action of an INITIAL rule. Returns a symbol if the action returns one.
     541       564262 :     fn initial(&mut self, out: &mut ScannedScript, token: Initial, mut loc: TextSpan) -> Option<Symbol> {
     542       564262 :         match token {
     543       221128 :             Initial::Space => None,
     544              :             Initial::Comment => {
     545          564 :                 out.comments.push(loc);
     546          564 :                 None
     547              :             }
     548           24 :             Initial::ObfuscatedLiteral(kind) => {
     549           24 :                 out.obfuscated_literals.push(obfuscated_literal(self.text, loc));
     550           24 :                 Some(Symbol::new(kind, loc))
     551              :             }
     552              :             Initial::CommentStart => {
     553          492 :                 self.yyless(&mut loc, 2);
     554          492 :                 self.ext_begin = loc;
     555          492 :                 self.ext_depth += 1;
     556          492 :                 self.state = State::Xc;
     557          492 :                 None
     558              :             }
     559        11280 :             Initial::Begin(state) => {
     560        11280 :                 self.ext_begin = loc;
     561        11280 :                 self.state = state;
     562        11280 :                 None
     563              :             }
     564       116404 :             Initial::Token(kind) => Some(Symbol::new(kind, loc)),
     565              :             Initial::DotTrailing => {
     566           17 :                 self.yyless(&mut loc, 1);
     567           17 :                 Some(Symbol::new(S_DOT_TRAILING, loc))
     568              :             }
     569              :             // A standalone ? is an unnamed parameter. ?name, ?1 and ?"name" are not parameters of Hyper,
     570              :             // the grammar has no rule for QUESTION_MARK, so they are syntax errors.
     571           52 :             Initial::QuestionMark => match self.text.get(self.pos) {
     572           50 :                 Some(&c) if is_ident_cont(c) || c == b'"' => Some(Symbol::new(S_QUESTION_MARK, loc)),
     573           48 :                 _ => Some(Symbol::new(S_PARAM, loc)),
     574              :             },
     575          225 :             Initial::Operator => Some(self.operator(out, loc)),
     576        18058 :             Initial::Integer => Some(self.number(loc)),
     577          948 :             Initial::Float => Some(Symbol::new(S_FCONST, loc)),
     578              :             Initial::DecimalFail | Initial::RealFail1 | Initial::RealFail2 => {
     579              :                 // Put back the dots, or the exponent marker and its sign
     580            4 :                 let putback = if token == Initial::RealFail1 { 1 } else { 2 };
     581            4 :                 let keep = loc.length as usize - putback;
     582            4 :                 self.yyless(&mut loc, keep);
     583            4 :                 Some(self.number(loc))
     584              :             }
     585       195064 :             Initial::Identifier => Some(self.read_identifier(out, loc)),
     586            2 :             Initial::Other => Some(Symbol::new(S_RAW_CHAR, loc)),
     587              :         }
     588       564262 :     }
     589              : 
     590              :     /// A number literal: integers that do not fit into an `i64` are FCONST, like in Postgres
     591        18062 :     fn number(&self, loc: TextSpan) -> Symbol {
     592        18062 :         let text = &self.text[loc.offset as usize..loc.end() as usize];
     593        18062 :         let is_integer = utf8(text).parse::<i64>().is_ok();
     594        18062 :         Symbol::new(if is_integer { S_ICONST } else { S_FCONST }, loc)
     595        18062 :     }
     596              : 
     597              :     /// The closing quote of a string, bit string or hex string literal
     598        10173 :     fn quote_stop(&mut self, loc: TextSpan) -> Symbol {
     599        10173 :         let kind = self.literal_kind();
     600        10173 :         self.state = State::Initial;
     601        10173 :         Symbol::new(kind, self.ext_begin.cover(loc))
     602        10173 :     }
     603              : 
     604              :     /// `{quotecontinue}`: a bit or hex string continues after whitespace with a line break and
     605              :     /// another quote. Consumes the whitespace and the quote and records the comments in it.
     606           32 :     fn quote_continue(&mut self, out: &mut ScannedScript) -> bool {
     607           32 :         let mut i = self.pos;
     608           32 :         let mut comments = Vec::new();
     609           32 :         let mut has_line_break = false;
     610           49 :         while i < self.text.len() {
     611           43 :             match self.text[i] {
     612           15 :                 b' ' | b'\t' | b'\x0C' => i += 1,
     613            1 :                 b'\n' | b'\r' => {
     614            1 :                     has_line_break = true;
     615            1 :                     i += 1;
     616            1 :                 }
     617           11 :                 b'-' if self.text.get(i + 1) == Some(&b'-') => {
     618            5 :                     let length = self.text[i..].iter().position(|&c| c == b'\n' || c == b'\r');
     619            1 :                     let length = length.unwrap_or(self.text.len() - i);
     620            1 :                     comments.push(Self::span(i, length));
     621            1 :                     i += length;
     622              :                 }
     623            1 :                 b'\'' if has_line_break => {
     624            1 :                     out.comments.extend(comments);
     625            1 :                     self.pos = i + 1;
     626            1 :                     return true;
     627              :                 }
     628           25 :                 _ => break,
     629              :             }
     630              :         }
     631           31 :         false
     632           32 :     }
     633              : 
     634              :     /// The `{xdstop}` and `{xbtstop}` actions
     635         1057 :     fn identifier_stop(&mut self, out: &mut ScannedScript, loc: TextSpan, quote: u8, message: &str) -> Symbol {
     636         1057 :         let loc = self.ext_begin.cover(loc);
     637         1057 :         if loc.length == 2 {
     638            3 :             out.errors.push(ScannerError {
     639            3 :                 location: loc,
     640            3 :                 message: message.to_string(),
     641            3 :             });
     642         1054 :         }
     643         1057 :         self.state = State::Initial;
     644         1057 :         self.read_quoted_identifier(out, loc, quote)
     645         1057 :     }
     646              : 
     647              :     /// The token kind of the literal that the current start condition scans
     648        10173 :     fn literal_kind(&self) -> SymbolKind {
     649        10173 :         match self.state {
     650           18 :             State::Xb => S_BCONST,
     651           13 :             State::Xh => S_XCONST,
     652        10142 :             _ => S_SCONST,
     653              :         }
     654        10173 :     }
     655              : 
     656         1057 :     fn read_quoted_identifier(&mut self, out: &mut ScannedScript, loc: TextSpan, quote: u8) -> Symbol {
     657         1057 :         let text = &self.text[loc.offset as usize..loc.end() as usize];
     658         1057 :         let inner = &text[1..text.len() - 1];
     659         1057 :         let name = if inner.contains(&quote) {
     660           32 :             let name = &mut self.buffer;
     661           32 :             name.clear();
     662           32 :             let mut i = 0;
     663          214 :             while i < inner.len() {
     664          182 :                 name.push(inner[i]);
     665          182 :                 if inner[i] == quote && i + 1 < inner.len() && inner[i + 1] == quote {
     666           32 :                     i += 1;
     667          150 :                 }
     668          182 :                 i += 1;
     669              :             }
     670           32 :             &self.buffer[..]
     671              :         } else {
     672         1025 :             inner
     673              :         };
     674         1057 :         let id = out.names.register(utf8(name), loc);
     675         1057 :         Symbol {
     676         1057 :             kind: S_IDENT,
     677         1057 :             location: loc,
     678         1057 :             value: SymbolValue::Name(id),
     679         1057 :         }
     680         1057 :     }
     681              : 
     682       195064 :     fn read_identifier(&mut self, out: &mut ScannedScript, loc: TextSpan) -> Symbol {
     683       195064 :         let text = &self.text[loc.offset as usize..loc.end() as usize];
     684       195064 :         self.buffer.clear();
     685       195064 :         self.buffer.extend(text.iter().map(u8::to_ascii_lowercase));
     686       195064 :         let lower = &self.buffer[..];
     687       195064 :         if let Some(keyword) = Keyword::find(lower) {
     688        93887 :             return Symbol {
     689        93887 :                 kind: keyword.symbol,
     690        93887 :                 location: loc,
     691        93887 :                 value: SymbolValue::Text(keyword.name),
     692        93887 :             };
     693       101177 :         }
     694       101177 :         let id = out.names.register(utf8(lower), loc);
     695       101177 :         Symbol {
     696       101177 :             kind: S_IDENT,
     697       101177 :             location: loc,
     698       101177 :             value: SymbolValue::Name(id),
     699       101177 :         }
     700       195064 :     }
     701              : 
     702              :     /// The `{operator}` action
     703          225 :     fn operator(&mut self, out: &mut ScannedScript, mut loc: TextSpan) -> Symbol {
     704          225 :         let start = loc.offset as usize;
     705          225 :         let yyleng = loc.length as usize;
     706          225 :         let text = &self.text[start..start + yyleng];
     707              : 
     708              :         // Embedded slash-star or dash-dash start a comment, the operator stops there.
     709          832 :         let find = |pattern: &[u8]| text.windows(2).position(|w| w == pattern);
     710          225 :         let mut nchars = yyleng;
     711          225 :         if let Some(i) = [find(b"/*"), find(b"--")].into_iter().flatten().min() {
     712            8 :             nchars = i;
     713          217 :         }
     714              :         // '+' and '-' cannot end a multi-char operator unless it contains chars that are not in
     715              :         // SQL operators.
     716          294 :         while nchars > 1 && matches!(text[nchars - 1], b'+' | b'-') {
     717          110 :             if text[..nchars - 1].iter().any(|c| b"~!@#^&|?%".contains(c)) {
     718           10 :                 break;
     719           69 :             }
     720           69 :             nchars -= 1;
     721              :         }
     722          225 :         if nchars < yyleng {
     723           69 :             self.yyless(&mut loc, nchars);
     724           69 :             if nchars == 1 {
     725           54 :                 if let Some(kind) = special_character(text[0]) {
     726           54 :                     return Symbol::new(kind, loc);
     727            0 :                 }
     728           15 :             }
     729           15 :             if nchars == 2 {
     730           15 :                 if let Some(kind) = two_char_operator(text[0], text[1]) {
     731           15 :                     return Symbol::new(kind, loc);
     732            0 :                 }
     733            0 :             }
     734          156 :         }
     735          156 :         if nchars >= 64 {
     736            2 :             out.errors.push(ScannerError {
     737            2 :                 location: loc,
     738            2 :                 message: "operator too long: operators longer than 64 bytes are not supported".to_string(),
     739            2 :             });
     740          154 :         }
     741          156 :         Symbol::new(S_Op, loc)
     742          225 :     }
     743              : }
     744              : 
     745              : /// Derive an executable literal from the hash of a marker.
     746           24 : fn obfuscated_literal(text: &[u8], location: TextSpan) -> ObfuscatedLiteral {
     747           24 :     let marker = &text[location.offset as usize..location.end() as usize];
     748          240 :     let open = marker.iter().position(|&c| c == b'(').unwrap();
     749           24 :     let kind = &marker[2..open];
     750           24 :     let hash = std::str::from_utf8(&marker[open + 1..open + 5]).unwrap();
     751           24 :     let value = u32::from_str_radix(hash, 16).unwrap();
     752           24 :     let replacement = match kind {
     753           24 :         b"String" => format!("'{hash}'"),
     754           22 :         b"Integer" => value.to_string(),
     755            3 :         b"Float" => format!("{value}.0"),
     756            2 :         b"HexString" => format!("X'{hash}'"),
     757              :         _ => {
     758           16 :             let bits: String = (0..16).rev().map(|bit| if (value >> bit) & 1 == 1 { '1' } else { '0' }).collect();
     759            1 :             format!("B'{bits}'")
     760              :         }
     761              :     };
     762           24 :     ObfuscatedLiteral { location, replacement }
     763           24 : }
     764              : 
     765              : /// Names are cut out of valid UTF-8 at ASCII boundaries, so they are valid UTF-8 themselves.
     766       120296 : fn utf8(bytes: &[u8]) -> &str {
     767       120296 :     std::str::from_utf8(bytes).expect("names are cut at ASCII boundaries")
     768       120296 : }
     769              : 
     770              : #[cfg(test)]
     771              : mod tests {
     772              :     use super::*;
     773              : 
     774           17 :     fn kinds(text: &str) -> Vec<SymbolKind> {
     775           17 :         scan(text, 0, 0).symbols.iter().map(|s| s.kind).collect()
     776           17 :     }
     777              : 
     778            4 :     fn spans(text: &str) -> Vec<(u32, u32)> {
     779           15 :         scan(text, 0, 0).symbols.iter().map(|s| (s.location.offset, s.location.length)).collect()
     780            4 :     }
     781              : 
     782              :     #[test]
     783            1 :     fn scans_keywords_and_identifiers() {
     784            1 :         let out = scan("SELECT Foo, foo", 0, 0);
     785            1 :         let kinds: Vec<_> = out.symbols.iter().map(|s| s.kind).collect();
     786            1 :         assert_eq!(kinds, vec![S_SELECT, S_IDENT, S_COMMA, S_IDENT, S_YYEOF]);
     787            1 :         assert_eq!(out.symbols[0].value, SymbolValue::Text("select"));
     788            1 :         assert_eq!(out.symbols[1].value, SymbolValue::Name(0));
     789            1 :         assert_eq!(out.symbols[3].value, SymbolValue::Name(0));
     790            1 :         assert_eq!(out.names.get(0).text, "foo");
     791            1 :         assert_eq!(out.names.get(0).occurrences, 2);
     792            1 :         assert_eq!(out.symbols[4].location, TextSpan::new(15, 0));
     793            1 :     }
     794              : 
     795              :     #[test]
     796            1 :     fn scans_numbers() {
     797            1 :         assert_eq!(kinds("1..2"), vec![S_ICONST, S_DOT_DOT, S_ICONST, S_YYEOF]);
     798            1 :         assert_eq!(spans("1..2"), vec![(0, 1), (1, 2), (3, 1), (4, 0)]);
     799            1 :         assert_eq!(kinds("1e5 1.5 .5 1. 1e"), vec![S_FCONST, S_FCONST, S_FCONST, S_FCONST, S_ICONST, S_IDENT, S_YYEOF]);
     800            1 :         assert_eq!(kinds("1.5e+"), vec![S_FCONST, S_IDENT, S_PLUS, S_YYEOF]);
     801              :         // Integers that do not fit into an i64 are FCONST
     802            1 :         assert_eq!(kinds("9223372036854775807 9223372036854775808"), vec![S_ICONST, S_FCONST, S_YYEOF]);
     803            1 :     }
     804              : 
     805              :     #[test]
     806            1 :     fn scans_trailing_dots() {
     807            1 :         assert_eq!(kinds("a. b"), vec![S_IDENT, S_DOT_TRAILING, S_IDENT, S_YYEOF]);
     808            1 :         assert_eq!(spans("a. b")[1], (1, 1));
     809            1 :         let out = scan("a.\n", 0, 0);
     810            1 :         assert_eq!(out.line_breaks, vec![TextSpan::new(2, 1)]);
     811            1 :     }
     812              : 
     813              :     #[test]
     814            1 :     fn trims_trailing_signs_and_comments_from_operators() {
     815              :         // Like Postgres, the operator keeps the characters before the comment start or trailing sign.
     816            1 :         assert_eq!(kinds("+-- foo"), vec![S_PLUS, S_YYEOF]);
     817            1 :         assert_eq!(spans("+-- foo")[0], (0, 1));
     818            1 :         assert_eq!(kinds("=- <=> !="), vec![S_EQUALS, S_MINUS, S_Op, S_NOT_EQUALS, S_YYEOF]);
     819            1 :         assert_eq!(kinds("a>=-1"), vec![S_IDENT, S_GREATER_EQUALS, S_MINUS, S_ICONST, S_YYEOF]);
     820            1 :         assert_eq!(spans("a>=-1")[1], (1, 2));
     821            1 :         assert_eq!(kinds("a=>-1"), vec![S_IDENT, S_EQUALS_GREATER, S_MINUS, S_ICONST, S_YYEOF]);
     822            1 :         assert_eq!(kinds("1 +/* c */ 2"), vec![S_ICONST, S_PLUS, S_ICONST, S_YYEOF]);
     823            1 :         assert_eq!(kinds("*/*"), vec![S_STAR, S_YYEOF]);
     824            1 :     }
     825              : 
     826              :     #[test]
     827            1 :     fn applies_the_lookahead_filter() {
     828            1 :         assert_eq!(kinds("not in"), vec![S_NOT_LA, S_IN_P, S_YYEOF]);
     829            1 :         assert_eq!(kinds("not glob"), vec![S_NOT_LA, S_GLOB, S_YYEOF]);
     830            1 :         assert_eq!(kinds("not foo"), vec![S_NOT, S_IDENT, S_YYEOF]);
     831            1 :         assert_eq!(kinds("a.*"), vec![S_IDENT, S_DOT_STAR_LA, S_STAR, S_YYEOF]);
     832            1 :     }
     833              : 
     834              :     #[test]
     835            1 :     fn scans_literals_and_comments() {
     836            1 :         let out = scan("'a' --c\n-1", 0, 0);
     837            1 :         assert_eq!(out.symbols[0].location, TextSpan::new(0, 3));
     838            1 :         assert_eq!(out.line_breaks, vec![TextSpan::new(7, 1)]);
     839            1 :         assert_eq!(out.comments, vec![TextSpan::new(4, 3)]);
     840            1 :         let out = scan("'abc'  -- c", 0, 0);
     841            1 :         assert_eq!(out.symbols[0].location, TextSpan::new(0, 5));
     842            1 :         assert_eq!(out.comments, vec![TextSpan::new(7, 4)]);
     843            1 :         let out = scan("'a''b' ", 0, 0);
     844            1 :         assert_eq!(out.symbols[0].location, TextSpan::new(0, 6));
     845            1 :         let out = scan("/* a /* b */ c */ 1", 0, 0);
     846            1 :         assert_eq!(out.comments, vec![TextSpan::new(0, 17)]);
     847            1 :         let out = scan("'unterminated", 0, 0);
     848            1 :         assert_eq!(out.errors[0].location, TextSpan::new(0, 13));
     849            1 :         assert_eq!(out.errors[0].message, "unterminated quoted string");
     850            1 :     }
     851              : 
     852              :     #[test]
     853            1 :     fn continues_bit_and_hex_strings() {
     854            1 :         let out = scan("x'AB' -- c\n  'CD' 1", 0, 0);
     855            1 :         assert_eq!(out.symbols.iter().map(|s| s.kind).collect::<Vec<_>>(), vec![S_XCONST, S_ICONST, S_YYEOF]);
     856            1 :         assert_eq!(out.symbols[0].location, TextSpan::new(0, 17));
     857            1 :         assert_eq!(out.comments, vec![TextSpan::new(6, 4)]);
     858              :         // Without a line break, the next quote starts another literal
     859            1 :         assert_eq!(kinds("b'01' '1'"), vec![S_BCONST, S_SCONST, S_YYEOF]);
     860              :         // Strings do not continue
     861            1 :         assert_eq!(kinds("'a'\n'b'"), vec![S_SCONST, S_SCONST, S_YYEOF]);
     862            1 :     }
     863              : 
     864              :     #[test]
     865            1 :     fn records_line_breaks() {
     866            1 :         let out = scan("a\r\nb\rc\n'd\ne' -- f\n", 0, 0);
     867            5 :         let breaks: Vec<_> = out.line_breaks.iter().map(|s| (s.offset, s.length)).collect();
     868            1 :         assert_eq!(breaks, vec![(1, 2), (4, 1), (6, 1), (9, 1), (17, 1)]);
     869            1 :     }
     870              : 
     871              :     #[test]
     872            1 :     fn unescapes_quoted_identifiers() {
     873            1 :         let out = scan(r#"select "trailing""", """leading", "a""b", `trailing```, `` `a``b`"#, 0, 0);
     874            5 :         for name in ["trailing\"", "\"leading", "a\"b", "trailing`", "a`b"] {
     875            5 :             assert!(out.names.find(name).is_some(), "{name}");
     876              :         }
     877            1 :     }
     878              : 
     879              :     #[test]
     880            1 :     fn records_obfuscated_literals() {
     881            1 :         let out = scan("/*Integer(1A2B)*/", 0, 0);
     882            1 :         assert_eq!(out.symbols[0].kind, S_ICONST);
     883            1 :         assert_eq!(out.obfuscated_literals[0].replacement, "6699");
     884            1 :     }
     885              : }
        

Generated by: LCOV version 2.0-1