LCOV - code coverage report
Current view: top level - dashql-parser/src - scanner.rs Coverage Total Hit
Test: DashQL 6e74948 Lines: 94.7 % 436 413
Test Date: 2026-11-06 17:46:50

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

Generated by: LCOV version 2.0-1