1mod cursor;
2mod lookup;
3mod token;
4mod token_kind;
5
6use crate::lexer::cursor::Cursor;
7use crate::Error;
8use crate::LimitTracker;
9pub use token::Token;
10pub use token_kind::TokenKind;
11
12#[derive(Clone, Debug)]
31pub struct Lexer<'a> {
32 finished: bool,
33 cursor: Cursor<'a>,
34 pub(crate) limit_tracker: LimitTracker,
35}
36
37#[derive(Debug)]
38enum State {
39 Start,
40 Ident,
41 StringLiteral,
42 StringLiteralStart,
43 BlockStringLiteral,
44 BlockStringLiteralBackslash,
45 StringLiteralBackslash,
46 LeadingZero,
47 IntegerPart,
48 DecimalPoint,
49 FractionalPart,
50 ExponentIndicator,
51 ExponentSign,
52 ExponentDigit,
53 Whitespace,
54 Comment,
55 SpreadOperator,
56 MinusSign,
57}
58
59impl<'a> Lexer<'a> {
60 pub fn new(input: &'a str) -> Self {
78 Self {
79 cursor: Cursor::new(input),
80 finished: false,
81 limit_tracker: LimitTracker::new(usize::MAX),
82 }
83 }
84
85 pub fn with_limit(mut self, limit: usize) -> Self {
86 self.limit_tracker = LimitTracker::new(limit);
87 self
88 }
89
90 pub fn lex(self) -> (Vec<Token<'a>>, Vec<Error>) {
92 let mut tokens = vec![];
93 let mut errors = vec![];
94
95 for item in self {
96 match item {
97 Ok(token) => tokens.push(token),
98 Err(error) => errors.push(error),
99 }
100 }
101
102 (tokens, errors)
103 }
104}
105
106impl<'a> Iterator for Lexer<'a> {
107 type Item = Result<Token<'a>, Error>;
108
109 fn next(&mut self) -> Option<Self::Item> {
110 if self.finished {
111 return None;
112 }
113
114 if self.limit_tracker.check_and_increment() {
115 self.finished = true;
116 return Some(Err(Error::limit(
117 "token limit reached, aborting lexing",
118 self.cursor.index(),
119 )));
120 }
121
122 match self.cursor.advance() {
123 Ok(token) => {
124 if matches!(token.kind(), TokenKind::Eof) {
125 self.finished = true;
126 }
127
128 Some(Ok(token))
129 }
130 Err(err) => Some(Err(err)),
131 }
132 }
133}
134
135impl<'a> Cursor<'a> {
136 fn advance(&mut self) -> Result<Token<'a>, Error> {
137 let mut state = State::Start;
138 let mut token = Token {
139 kind: TokenKind::Eof,
140 data: "",
141 index: self.index(),
142 };
143
144 loop {
145 let Some(c) = self.bump() else {
146 return self.eof(state, token);
147 };
148 match state {
149 State::Start => {
150 if let Some(t) = lookup::punctuation_kind(c) {
151 token.kind = t;
152 token.data = self.current_str();
153 return Ok(token);
154 }
155
156 if lookup::is_namestart(c) {
157 token.kind = TokenKind::Name;
158 state = State::Ident;
159
160 continue;
161 }
162
163 if c != '0' && c.is_ascii_digit() {
164 token.kind = TokenKind::Int;
165 state = State::IntegerPart;
166
167 continue;
168 }
169
170 match c {
171 '"' => {
172 token.kind = TokenKind::StringValue;
173 state = State::StringLiteralStart;
174 }
175 '#' => {
176 token.kind = TokenKind::Comment;
177 state = State::Comment;
178 }
179 '.' => {
180 token.kind = TokenKind::Spread;
181 state = State::SpreadOperator;
182 }
183 '-' => {
184 token.kind = TokenKind::Int;
185 state = State::MinusSign;
186 }
187 '0' => {
188 token.kind = TokenKind::Int;
189 state = State::LeadingZero;
190 }
191 c if is_whitespace_assimilated(c) => {
192 token.kind = TokenKind::Whitespace;
193 state = State::Whitespace;
194 }
195 c => {
196 return Err(Error::with_loc(
197 format!(r#"Unexpected character "{c}""#),
198 self.current_str().to_string(),
199 token.index,
200 ));
201 }
202 };
203 }
204 State::Ident => match c {
205 curr if is_name_continue(curr) => {}
206 _ => {
207 token.data = self.prev_str();
208 return self.done(token);
209 }
210 },
211 State::Whitespace => match c {
212 curr if is_whitespace_assimilated(curr) => {}
213 _ => {
214 token.data = self.prev_str();
215 return self.done(token);
216 }
217 },
218 State::BlockStringLiteral => match c {
219 '\\' => {
220 state = State::BlockStringLiteralBackslash;
221 }
222 '"'
223 if self.eatc('"') && self.eatc('"') => {
225 token.data = self.current_str();
226 return self.done(token);
227 }
228 _ => {}
229 },
230 State::StringLiteralStart => match c {
231 '"' => {
232 if self.eatc('"') {
233 state = State::BlockStringLiteral;
234
235 continue;
236 }
237
238 if self.is_pending() {
239 token.data = self.prev_str();
240 } else {
241 token.data = self.current_str();
242 }
243 return self.done(token);
244 }
245 '\\' => {
246 state = State::StringLiteralBackslash;
247 }
248 _ => {
249 state = State::StringLiteral;
250
251 continue;
252 }
253 },
254 State::StringLiteral => match c {
255 '"' => {
256 token.data = self.current_str();
257 return self.done(token);
258 }
259 curr if is_line_terminator(curr) => {
260 self.add_err(Error::with_loc(
261 "unexpected line terminator",
262 "".to_string(),
263 0,
264 ));
265 }
266 '\\' => {
267 state = State::StringLiteralBackslash;
268 }
269 _ => {}
270 },
271 State::BlockStringLiteralBackslash => match c {
272 '"' => {
273 if self.eatc('"') {
278 self.eatc('"');
279 }
280
281 state = State::BlockStringLiteral;
282 }
283 '\\' => {
284 }
288 _ => {
289 state = State::BlockStringLiteral;
290 }
291 },
292 State::StringLiteralBackslash => match c {
293 curr if is_escaped_char(curr) => {
294 state = State::StringLiteral;
295 }
296 'u' => {
297 if let Some((_, length)) = unicode_escape(&self.source[self.offset + 1..]) {
298 for _ in 0..length {
299 self.bump();
300 }
301 } else {
302 self.add_err(Error::with_loc(
303 "invalid unicode escape sequence",
304 "".to_owned(),
305 self.offset,
306 ));
307 }
308 state = State::StringLiteral;
309 }
310 _ => {
311 self.add_err(Error::with_loc(
312 "unexpected escaped character",
313 c.to_string(),
314 0,
315 ));
316
317 state = State::StringLiteral;
318 }
319 },
320 State::LeadingZero => match c {
321 '.' => {
322 token.kind = TokenKind::Float;
323 state = State::DecimalPoint;
324 }
325 'e' | 'E' => {
326 token.kind = TokenKind::Float;
327 state = State::ExponentIndicator;
328 }
329 _ if c.is_ascii_digit() => {
330 return Err(Error::with_loc(
331 "Numbers must not have non-significant leading zeroes",
332 self.current_str().to_string(),
333 token.index,
334 ));
335 }
336 _ if lookup::is_namestart(c) => {
337 return Err(Error::with_loc(
338 format!("Unexpected character `{c}` as integer suffix"),
339 self.current_str().to_string(),
340 token.index,
341 ));
342 }
343 _ => {
344 token.data = self.prev_str();
345 return self.done(token);
346 }
347 },
348 State::IntegerPart => match c {
349 curr if curr.is_ascii_digit() => {}
350 '.' => {
351 token.kind = TokenKind::Float;
352 state = State::DecimalPoint;
353 }
354 'e' | 'E' => {
355 token.kind = TokenKind::Float;
356 state = State::ExponentIndicator;
357 }
358 _ if lookup::is_namestart(c) => {
359 return Err(Error::with_loc(
360 format!("Unexpected character `{c}` as integer suffix"),
361 self.current_str().to_string(),
362 token.index,
363 ));
364 }
365 _ => {
366 token.data = self.prev_str();
367 return self.done(token);
368 }
369 },
370 State::DecimalPoint => match c {
371 curr if curr.is_ascii_digit() => {
372 state = State::FractionalPart;
373 }
374 _ => {
375 return Err(Error::with_loc(
376 format!("Unexpected character `{c}`, expected fractional digit"),
377 self.current_str().to_string(),
378 token.index,
379 ));
380 }
381 },
382 State::FractionalPart => match c {
383 curr if curr.is_ascii_digit() => {}
384 'e' | 'E' => {
385 state = State::ExponentIndicator;
386 }
387 _ if c == '.' || lookup::is_namestart(c) => {
388 return Err(Error::with_loc(
389 format!("Unexpected character `{c}` as float suffix"),
390 self.current_str().to_string(),
391 token.index,
392 ));
393 }
394 _ => {
395 token.data = self.prev_str();
396 return self.done(token);
397 }
398 },
399 State::ExponentIndicator => match c {
400 _ if c.is_ascii_digit() => {
401 state = State::ExponentDigit;
402 }
403 '+' | '-' => {
404 state = State::ExponentSign;
405 }
406 _ => {
407 return Err(Error::with_loc(
408 format!("Unexpected character `{c}`, expected exponent digit or sign"),
409 self.current_str().to_string(),
410 token.index,
411 ))
412 }
413 },
414 State::ExponentSign => match c {
415 _ if c.is_ascii_digit() => {
416 state = State::ExponentDigit;
417 }
418 _ => {
419 return Err(Error::with_loc(
420 format!("Unexpected character `{c}`, expected exponent digit"),
421 self.current_str().to_string(),
422 token.index,
423 ))
424 }
425 },
426 State::ExponentDigit => match c {
427 _ if c.is_ascii_digit() => {
428 state = State::ExponentDigit;
429 }
430 _ if c == '.' || lookup::is_namestart(c) => {
431 return Err(Error::with_loc(
432 format!("Unexpected character `{c}` as float suffix"),
433 self.current_str().to_string(),
434 token.index,
435 ));
436 }
437 _ => {
438 token.data = self.prev_str();
439 return self.done(token);
440 }
441 },
442 State::SpreadOperator => {
443 if c == '.' && self.eatc('.') {
444 token.data = self.current_str();
445 return Ok(token);
446 }
447 return self.unterminated_spread_operator(&token);
448 }
449 State::MinusSign => match c {
450 '0' => {
451 state = State::LeadingZero;
452 }
453 curr if curr.is_ascii_digit() => {
454 state = State::IntegerPart;
455 }
456 _ => {
457 return Err(Error::with_loc(
458 format!("Unexpected character `{c}`"),
459 self.current_str().to_string(),
460 token.index,
461 ))
462 }
463 },
464 State::Comment => match c {
465 curr if is_line_terminator(curr) => {
466 token.data = self.prev_str();
467 return self.done(token);
468 }
469 _ => {}
470 },
471 }
472 }
473 }
474
475 fn eof(&mut self, state: State, mut token: Token<'a>) -> Result<Token<'a>, Error> {
476 match state {
477 State::Start => {
478 let end = self.source.len();
480 self.offset = end;
481 token.index = end;
482 Ok(token)
483 }
484 State::StringLiteralStart => {
485 let curr = self.current_str();
486
487 Err(Error::with_loc(
488 "unexpected end of data while lexing string value",
489 curr.to_string(),
490 token.index,
491 ))
492 }
493 State::StringLiteral
494 | State::BlockStringLiteral
495 | State::BlockStringLiteralBackslash
496 | State::StringLiteralBackslash => {
497 let curr = self.drain();
498
499 Err(Error::with_loc(
500 "unterminated string value",
501 curr.to_string(),
502 token.index,
503 ))
504 }
505 State::SpreadOperator => self.unterminated_spread_operator(&token),
506 State::MinusSign => Err(Error::with_loc(
507 "Unexpected character \"-\"",
508 self.current_str().to_string(),
509 token.index,
510 )),
511 State::DecimalPoint | State::ExponentIndicator | State::ExponentSign => {
512 Err(Error::with_loc(
513 "Unexpected EOF in float value",
514 self.current_str().to_string(),
515 token.index,
516 ))
517 }
518 State::Ident
519 | State::LeadingZero
520 | State::IntegerPart
521 | State::FractionalPart
522 | State::ExponentDigit
523 | State::Whitespace
524 | State::Comment => {
525 if let Some(mut err) = self.err() {
526 err.set_data(self.current_str().to_string());
527 return Err(err);
528 }
529
530 token.data = self.current_str();
531
532 Ok(token)
533 }
534 }
535 }
536
537 fn unterminated_spread_operator(&mut self, token: &Token<'a>) -> Result<Token<'a>, Error> {
538 let data = if self.is_pending() {
539 self.prev_str()
540 } else {
541 self.current_str()
542 };
543
544 Err(Error::with_loc(
545 "Unterminated spread operator",
546 data.to_string(),
547 token.index,
548 ))
549 }
550
551 fn done(&mut self, token: Token<'a>) -> Result<Token<'a>, Error> {
552 if let Some(mut err) = self.err() {
553 err.set_data(token.data.to_string());
554 err.index = token.index;
555 self.err = None;
556 return Err(err);
557 }
558 Ok(token)
559 }
560}
561
562fn is_whitespace_assimilated(c: char) -> bool {
565 matches!(
566 c,
567 '\u{0009}' | '\u{0020}' | '\u{000A}' | '\u{000D}' | '\u{FEFF}' )
576}
577
578fn is_name_continue(c: char) -> bool {
580 matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_')
581}
582
583fn is_line_terminator(c: char) -> bool {
584 matches!(c, '\n' | '\r')
585}
586
587fn is_escaped_char(c: char) -> bool {
590 matches!(c, '"' | '\\' | '/' | 'b' | 'f' | 'n' | 'r' | 't')
591}
592
593pub(crate) fn unicode_escape(input: &str) -> Option<(char, usize)> {
595 let (value, length) = if input.starts_with('{') {
596 let end = input.find('}')?;
597 let digits = &input[1..end];
598 if digits.is_empty() || !digits.bytes().all(|c| c.is_ascii_hexdigit()) {
599 return None;
600 }
601 (u32::from_str_radix(digits, 16).ok()?, end + 1)
602 } else {
603 let digits = input.get(..4)?;
604 if !digits.bytes().all(|c| c.is_ascii_hexdigit()) {
605 return None;
606 }
607 let high = u32::from_str_radix(digits, 16).ok()?;
608 if (0xD800..=0xDBFF).contains(&high) {
609 if input.get(4..6)? != "\\u" {
610 return None;
611 }
612 let low = input.get(6..10)?;
613 if !low.bytes().all(|c| c.is_ascii_hexdigit()) {
614 return None;
615 }
616 let low = u32::from_str_radix(low, 16).ok()?;
617 if !(0xDC00..=0xDFFF).contains(&low) {
618 return None;
619 }
620 (0x10000 + ((high - 0xD800) << 10) + low - 0xDC00, 10)
621 } else {
622 (high, 4)
623 }
624 };
625 Some((char::from_u32(value)?, length))
626}
627
628#[cfg(test)]
629mod test {
630 use super::*;
631
632 #[test]
633 fn unterminated_string() {
634 let schema = r#"
635type Query {
636 name: String
637 format: String = "Y-m-d\\TH:i:sP"
638}
639 "#;
640 let (tokens, errors) = Lexer::new(schema).lex();
641 dbg!(tokens);
642 dbg!(errors);
643 }
644
645 #[test]
646 fn token_limit() {
647 let lexer = Lexer::new("type Query { a a a a a a a a a }").with_limit(10);
648 let (tokens, errors) = lexer.lex();
649 assert_eq!(tokens.len(), 10);
650 assert_eq!(
651 errors,
652 &[Error::limit("token limit reached, aborting lexing", 17)]
653 );
654 }
655
656 #[test]
657 fn token_limit_exact() {
658 let lexer = Lexer::new("type Query { a a a a a a a a a }").with_limit(26);
659 let (tokens, errors) = lexer.lex();
660 assert_eq!(tokens.len(), 26);
661 assert!(errors.is_empty());
662
663 let lexer = Lexer::new("type Query { a a a a a a a a a }").with_limit(25);
664 let (tokens, errors) = lexer.lex();
665 assert_eq!(tokens.len(), 25);
666 assert_eq!(
667 errors,
668 &[Error::limit("token limit reached, aborting lexing", 31)]
669 );
670 }
671
672 #[test]
673 fn errors_and_token_limit() {
674 let lexer = Lexer::new("type Query { ..a a a a a a a a a }").with_limit(10);
675 let (tokens, errors) = lexer.lex();
676 assert_eq!(tokens.len(), 9);
678 assert_eq!(
679 errors,
680 &[
681 Error::with_loc("Unterminated spread operator", "..".to_string(), 13),
682 Error::limit("token limit reached, aborting lexing", 18),
683 ],
684 );
685 }
686
687 #[test]
688 fn stream_produces_original_input() {
689 let schema = r#"
690type Query {
691 name: String
692 format: String = "Y-m-d\\TH:i:sP"
693}
694 "#;
695
696 let lexer = Lexer::new(schema);
697 let processed_schema = lexer
698 .into_iter()
699 .fold(String::new(), |acc, token| acc + token.unwrap().data());
700
701 assert_eq!(schema, processed_schema);
702 }
703
704 #[test]
705 fn quoted_block_comment() {
706 let input = r#"
707"""
708Not an escape character:
709'/\W/'
710Escape character:
711\"""
712\"""\"""
713Not escape characters:
714\" \""
715Escape character followed by a quote:
716\""""
717"""
718 "#;
719
720 let (tokens, errors) = Lexer::new(input).lex();
721 assert!(errors.is_empty());
722 assert_eq!(
724 tokens[1].data,
725 r#"
726"""
727Not an escape character:
728'/\W/'
729Escape character:
730\"""
731\"""\"""
732Not escape characters:
733\" \""
734Escape character followed by a quote:
735\""""
736"""
737"#
738 .trim(),
739 );
740
741 let input = r#"
742# String contents: """
743"""\""""""
744# Unclosed block string
745"""\"""
746 "#;
747 let (tokens, errors) = Lexer::new(input).lex();
748 assert_eq!(tokens[3].data, r#""""\"""""""#);
749 assert_eq!(
750 errors,
751 &[Error::with_loc(
752 "unterminated string value",
753 r#""""\"""
754 "#
755 .to_string(),
756 59,
757 )]
758 );
759 }
760
761 #[test]
762 fn unexpected_character() {
763 let schema = r#"
764type Query {
765 name: String
766}
767/
768 "#;
769 let (tokens, errors) = Lexer::new(schema).lex();
770 dbg!(tokens);
771 assert_eq!(
772 errors,
773 &[Error::with_loc(
774 "Unexpected character \"/\"",
775 "/".to_string(),
776 33,
777 )]
778 );
779 }
780}