1use crate::token::{LexError, Lexed, Token};
4use crate::{BodyDelimiter, LexOptions};
5use sql_dialect_fmt_syntax::{Dialect, SyntaxKind};
6use sql_dialect_fmt_text::LineIndex;
7
8pub fn tokenize(input: &str) -> Lexed<'_> {
12 tokenize_with_options(input, LexOptions::default())
13}
14
15pub fn tokenize_with_options<'a>(input: &'a str, options: LexOptions<'_>) -> Lexed<'a> {
16 Lexer::new(input, options).run()
17}
18
19pub fn tokenize_for_dialect(input: &str, dialect: Dialect) -> Lexed<'_> {
24 tokenize_with_options(input, LexOptions::default().with_dialect(dialect))
25}
26
27struct Lexer<'a, 'cfg> {
28 input: &'a str,
29 bytes: &'a [u8],
30 line_index: LineIndex<'a>,
31 options: LexOptions<'cfg>,
32 pos: usize,
33 tokens: Vec<Token<'a>>,
34 errors: Vec<LexError>,
35}
36
37impl<'a, 'cfg> Lexer<'a, 'cfg> {
38 fn new(input: &'a str, options: LexOptions<'cfg>) -> Self {
39 Lexer {
40 input,
41 bytes: input.as_bytes(),
42 line_index: LineIndex::new(input),
43 options,
44 pos: 0,
45 tokens: Vec::new(),
46 errors: Vec::new(),
47 }
48 }
49
50 #[inline]
51 fn at_end(&self) -> bool {
52 self.pos >= self.bytes.len()
53 }
54
55 #[inline]
57 fn peek(&self) -> u8 {
58 if self.pos < self.bytes.len() {
59 self.bytes[self.pos]
60 } else {
61 0
62 }
63 }
64
65 #[inline]
66 fn peek_at(&self, n: usize) -> u8 {
67 let i = self.pos + n;
68 if i < self.bytes.len() {
69 self.bytes[i]
70 } else {
71 0
72 }
73 }
74
75 #[inline]
77 fn bump(&mut self) -> u8 {
78 let c = self.bytes[self.pos];
79 self.pos += 1;
80 c
81 }
82
83 #[inline]
84 fn eat_while(&mut self, mut pred: impl FnMut(u8) -> bool) {
85 while !self.at_end() && pred(self.peek()) {
86 self.pos += 1;
87 }
88 }
89
90 #[inline]
91 fn starts_with(&self, text: &str) -> bool {
92 self.bytes[self.pos..].starts_with(text.as_bytes())
93 }
94
95 #[inline]
96 fn push(&mut self, kind: SyntaxKind, start: usize) {
97 let text = &self.input[start..self.pos];
100 self.tokens.push(Token { kind, text });
101 }
102
103 #[inline]
107 fn error(&mut self, message: impl Into<String>, offset: usize) {
108 self.errors.push(LexError {
109 message: message.into(),
110 offset,
111 len: self.pos.saturating_sub(offset),
112 line_column: Some(self.line_index.line_column(offset)),
113 });
114 }
115
116 fn run(mut self) -> Lexed<'a> {
117 let dollar_quoting = self.options.dialect.supports_dollar_quoting();
120 while !self.at_end() {
121 let start = self.pos;
122 if dollar_quoting {
123 if let Some(delimiter) = self.body_delimiter_at() {
124 self.delimited_body(start, delimiter);
125 self.push(SyntaxKind::DOLLAR_STRING, start);
126 continue;
127 }
128 }
129 match self.peek() {
130 b' ' | b'\t' => {
131 self.eat_while(|c| c == b' ' || c == b'\t');
132 self.push(SyntaxKind::WHITESPACE, start);
133 }
134 b'\n' => {
135 self.pos += 1;
136 self.push(SyntaxKind::NEWLINE, start);
137 }
138 b'\r' => {
139 self.pos += 1;
140 if self.peek() == b'\n' {
141 self.pos += 1;
142 }
143 self.push(SyntaxKind::NEWLINE, start);
144 }
145 b'-' if self.peek_at(1) == b'-' => {
147 self.line_comment();
148 self.push(SyntaxKind::COMMENT, start);
149 }
150 b'/' if self.options.dialect.supports_double_slash_comments()
151 && self.peek_at(1) == b'/' =>
152 {
153 self.line_comment();
154 self.push(SyntaxKind::COMMENT, start);
155 }
156 b'/' if self.peek_at(1) == b'*' => {
158 self.block_comment(start);
159 self.push(SyntaxKind::BLOCK_COMMENT, start);
160 }
161 b'\'' => {
162 self.string_body(start, true);
163 self.push(SyntaxKind::STRING, start);
164 }
165 b'r' | b'R'
166 if self.options.dialect.supports_prefixed_strings()
167 && self.peek_at(1) == b'\'' =>
168 {
169 self.pos += 1; self.string_body(start, false);
171 self.push(SyntaxKind::STRING, start);
172 }
173 b'x' | b'X'
174 if self.options.dialect.supports_prefixed_strings()
175 && self.peek_at(1) == b'\'' =>
176 {
177 self.pos += 1; self.string_body(start, false);
179 self.push(SyntaxKind::STRING, start);
180 }
181 b'"' => {
182 self.quoted_ident_body(start);
183 self.push(SyntaxKind::QUOTED_IDENT, start);
184 }
185 b'`' if self.options.dialect.supports_backtick_identifiers() => {
186 self.backtick_ident_body(start);
187 self.push(SyntaxKind::QUOTED_IDENT, start);
188 }
189 b'$' if dollar_quoting
192 && (is_ident_start(self.peek_at(1)) || self.peek_at(1).is_ascii_digit()) =>
193 {
194 self.pos += 1; self.eat_while(is_ident_continue);
196 self.push(SyntaxKind::VARIABLE, start);
197 }
198 c if c.is_ascii_digit() => self.number(start),
199 b'.' if self.peek_at(1).is_ascii_digit() => self.number(start),
201 c if is_ident_start(c)
202 && self.options.dialect.supports_stage_refs()
203 && self.at_file_uri_start() =>
204 {
205 self.file_uri();
206 self.push(SyntaxKind::FILE_URI, start);
207 }
208 c if is_ident_start(c) => {
209 self.eat_while(is_ident_continue);
210 self.push(SyntaxKind::IDENT, start);
211 }
212 _ => self.operator(start),
213 }
214 }
215 Lexed {
216 tokens: self.tokens,
217 errors: self.errors,
218 }
219 }
220
221 fn at_file_uri_start(&self) -> bool {
225 self.bytes
226 .get(self.pos..self.pos.saturating_add(b"file://".len()))
227 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"file://"))
228 }
229
230 fn file_uri(&mut self) {
234 self.eat_while(|c| !c.is_ascii_whitespace() && c != b';');
235 }
236
237 fn line_comment(&mut self) {
239 self.pos += 2;
240 while !self.at_end() {
241 let c = self.peek();
242 if c == b'\n' || c == b'\r' {
243 break;
244 }
245 self.pos += 1;
246 }
247 }
248
249 fn block_comment(&mut self, start: usize) {
251 self.pos += 2; loop {
253 if self.at_end() {
254 self.error("unterminated block comment", start);
255 break;
256 }
257 if self.peek() == b'*' && self.peek_at(1) == b'/' {
258 self.pos += 2;
259 break;
260 }
261 self.pos += 1;
262 }
263 }
264
265 fn string_body(&mut self, start: usize, backslash_escapes: bool) {
268 self.pos += 1; loop {
270 if self.at_end() {
271 self.error("unterminated string literal", start);
272 break;
273 }
274 match self.bump() {
275 b'\\' if backslash_escapes => {
276 if !self.at_end() {
278 self.pos += 1;
279 }
280 }
281 b'\'' => {
282 if self.peek() == b'\'' {
283 self.pos += 1; } else {
285 break; }
287 }
288 _ => {}
289 }
290 }
291 }
292
293 fn quoted_ident_body(&mut self, start: usize) {
295 self.pos += 1; loop {
297 if self.at_end() {
298 self.error("unterminated quoted identifier", start);
299 break;
300 }
301 if self.bump() == b'"' {
302 if self.peek() == b'"' {
303 self.pos += 1; } else {
305 break; }
307 }
308 }
309 }
310
311 fn backtick_ident_body(&mut self, start: usize) {
313 self.pos += 1; loop {
315 if self.at_end() {
316 self.error("unterminated backtick identifier", start);
317 break;
318 }
319 if self.bump() == b'`' {
320 if self.peek() == b'`' {
321 self.pos += 1; } else {
323 break; }
325 }
326 }
327 }
328
329 fn body_delimiter_at(&self) -> Option<BodyDelimiter> {
330 self.options
331 .body_delimiters
332 .iter()
333 .copied()
334 .filter(|delimiter| {
335 !delimiter.opener.is_empty()
336 && !delimiter.closer.is_empty()
337 && self.starts_with(delimiter.opener)
338 })
339 .max_by_key(|delimiter| delimiter.opener.len())
340 }
341
342 fn delimited_body(&mut self, start: usize, delimiter: BodyDelimiter) {
344 self.pos += delimiter.opener.len();
345 loop {
346 if self.at_end() {
347 self.error(format!("unterminated {}", delimiter.name), start);
348 break;
349 }
350 if self.starts_with(delimiter.closer) {
351 self.pos += delimiter.closer.len();
352 break;
353 }
354 self.pos += 1;
355 }
356 }
357
358 fn number(&mut self, start: usize) {
360 let mut is_float = false;
361 if self.peek() == b'.' {
362 is_float = true;
364 self.pos += 1;
365 self.eat_while(|c| c.is_ascii_digit());
366 } else {
367 self.eat_while(|c| c.is_ascii_digit());
368 if self.peek() == b'.' {
369 is_float = true;
371 self.pos += 1;
372 self.eat_while(|c| c.is_ascii_digit());
373 }
374 }
375 if self.peek() == b'e' || self.peek() == b'E' {
377 let save = self.pos;
378 self.pos += 1;
379 if self.peek() == b'+' || self.peek() == b'-' {
380 self.pos += 1;
381 }
382 if self.peek().is_ascii_digit() {
383 is_float = true;
384 self.eat_while(|c| c.is_ascii_digit());
385 } else {
386 self.pos = save; }
388 }
389 self.push(
390 if is_float {
391 SyntaxKind::FLOAT_NUMBER
392 } else {
393 SyntaxKind::INT_NUMBER
394 },
395 start,
396 );
397 }
398
399 fn operator(&mut self, start: usize) {
401 let c = self.peek();
402 if c >= 0x80 {
406 if let Some(ch) = self.input[self.pos..].chars().next() {
407 self.pos += ch.len_utf8();
408 self.error(format!("unexpected character {ch:?}"), start);
409 } else {
410 self.pos += 1;
411 self.error("unexpected byte", start);
412 }
413 self.push(SyntaxKind::ERROR, start);
414 return;
415 }
416
417 self.pos += 1; let kind = match c {
419 b'(' => SyntaxKind::L_PAREN,
420 b')' => SyntaxKind::R_PAREN,
421 b'[' => SyntaxKind::L_BRACKET,
422 b']' => SyntaxKind::R_BRACKET,
423 b'{' => SyntaxKind::L_BRACE,
424 b'}' => SyntaxKind::R_BRACE,
425 b',' => SyntaxKind::COMMA,
426 b';' => SyntaxKind::SEMICOLON,
427 b'+' => SyntaxKind::PLUS,
428 b'*' => SyntaxKind::STAR,
429 b'/' => SyntaxKind::SLASH,
430 b'%' => SyntaxKind::PERCENT,
431 b'&' => SyntaxKind::AMP,
432 b'^' => SyntaxKind::CARET,
433 b'~' => SyntaxKind::TILDE,
434 b'@' if self.options.dialect.supports_stage_refs() => SyntaxKind::AT,
437 b'?' => SyntaxKind::QUESTION,
438 b'.' => SyntaxKind::DOT,
439 b'$' => SyntaxKind::DOLLAR,
440 b'=' => {
441 if self.peek() == b'>' {
442 self.pos += 1;
443 SyntaxKind::FAT_ARROW
444 } else {
445 SyntaxKind::EQ
446 }
447 }
448 b'!' => {
449 if self.peek() == b'=' {
450 self.pos += 1;
451 SyntaxKind::NEQ
452 } else {
453 self.error("unexpected '!'", start);
454 SyntaxKind::BANG
455 }
456 }
457 b'<' => {
458 if self.options.dialect.supports_null_safe_eq()
459 && self.peek() == b'='
460 && self.peek_at(1) == b'>'
461 {
462 self.pos += 2;
463 SyntaxKind::NULL_SAFE_EQ
464 } else if self.peek() == b'=' {
465 self.pos += 1;
466 SyntaxKind::LTE
467 } else if self.peek() == b'>' {
468 self.pos += 1;
469 SyntaxKind::NEQ
470 } else {
471 SyntaxKind::LT
472 }
473 }
474 b'>' => {
475 if self.peek() == b'=' {
476 self.pos += 1;
477 SyntaxKind::GTE
478 } else {
479 SyntaxKind::GT
480 }
481 }
482 b':' => {
483 if self.peek() == b':' {
484 self.pos += 1;
485 SyntaxKind::COLON2
486 } else if self.peek() == b'=' {
487 self.pos += 1;
488 SyntaxKind::ASSIGN
489 } else {
490 SyntaxKind::COLON
491 }
492 }
493 b'-' => {
494 if self.peek() == b'>' && self.peek_at(1) == b'>' {
495 self.pos += 2;
496 SyntaxKind::FLOW_PIPE
497 } else if self.peek() == b'>' {
498 self.pos += 1;
499 SyntaxKind::ARROW
500 } else {
501 SyntaxKind::MINUS
502 }
503 }
504 b'|' => {
505 if self.peek() == b'>' {
506 self.pos += 1;
507 SyntaxKind::PIPE_GT
508 } else if self.peek() == b'|' {
509 self.pos += 1;
510 SyntaxKind::CONCAT
511 } else {
512 SyntaxKind::PIPE
513 }
514 }
515 other => {
516 self.error(format!("unexpected character {:?}", other as char), start);
517 SyntaxKind::ERROR
518 }
519 };
520 self.push(kind, start);
521 }
522}
523
524#[inline]
525fn is_ident_start(c: u8) -> bool {
526 c.is_ascii_alphabetic() || c == b'_'
527}
528
529#[inline]
530fn is_ident_continue(c: u8) -> bool {
531 c.is_ascii_alphanumeric() || c == b'_' || c == b'$'
532}