Skip to main content

hermes_parser/
token.rs

1//! Token and friends, ported from include/hermes/Parser/JSLexer.h (Token,
2//! RegExpLiteral, StoredComment, StoredToken).
3//!
4//! Unlike the C++, which holds `UniqueString *` pointers and `SMLoc` pointers
5//! into the buffer, the Rust port is offset-based: locations are `SMRange`
6//! (buffer id + byte offsets) and interned values are `AtomBytes` handles into
7//! the `AtomTable`. A `Token` carries its kind, source range, and (depending on
8//! the kind) a numeric value, an interned identifier/string-literal/raw value,
9//! or a `RegExpLiteral` — the complete `Token` surface, matching the C++.
10//!
11//! The full set of value getters/setters
12//! (numeric/identifier/string/template/regexp/bigint/jsx) is part of the
13//! faithful `Token` surface; the public accessors are part of the lexer's API
14//! (consumed by the parser), and the `pub(crate)` setters are all used by the
15//! scanners or exercised by tests.
16
17use hermes_atom_table::AtomBytes;
18use hermes_support::location::{SMLoc, SMRange, SourceId};
19
20use crate::token_kinds::TokenKind;
21
22/// Port of `JSLexer.h`'s `RegExpLiteral`: an interned body and flags.
23#[derive(Copy, Clone, Debug)]
24pub struct RegExpLiteral {
25    body: AtomBytes,
26    flags: AtomBytes,
27}
28
29impl RegExpLiteral {
30    /// A literal with the given interned body and flags.
31    pub fn new(body: AtomBytes, flags: AtomBytes) -> RegExpLiteral {
32        RegExpLiteral { body, flags }
33    }
34    /// \return the pattern between the delimiting slashes.
35    pub fn body(&self) -> AtomBytes {
36        self.body
37    }
38    /// \return the flags following the closing slash (possibly empty).
39    pub fn flags(&self) -> AtomBytes {
40        self.flags
41    }
42}
43
44/// Encapsulates the information contained in the current token.
45/// We only ever create one of these, but it is cleaner to keep the data
46/// in a separate class. Port of `Token`.
47#[derive(Clone, Debug)]
48pub struct Token {
49    kind: TokenKind,
50    range: SMRange,
51    numeric: f64,
52    ident: Option<AtomBytes>,
53
54    /// Representation of the string literal for tokens that are strings.
55    /// If the current token is part of a template literal, this is `None`
56    /// when it contains a NotEscapeSequence.
57    string_literal: Option<AtomBytes>,
58
59    regexp: Option<RegExpLiteral>,
60
61    /// Representation of one of these depending on the TokenKind:
62    /// - The Template Raw Value (TRV) associated with the token if it
63    ///   represents a part or whole of a template literal.
64    /// - The raw string of a JSXText.
65    raw_string: Option<AtomBytes>,
66
67    /// If the current token is a string literal, this flag indicates whether it
68    /// contains any escapes or new line continuations. We need this in order to
69    /// detect directives.
70    string_literal_contains_escapes: bool,
71}
72
73impl Token {
74    /// A fresh `none` token with an empty range in `source`.
75    pub fn new(source: SourceId) -> Token {
76        let loc = SMLoc { source, offset: 0 };
77        Token {
78            kind: TokenKind::none,
79            range: SMRange {
80                start: loc,
81                end: loc,
82            },
83            numeric: 0.0,
84            ident: None,
85            string_literal: None,
86            regexp: None,
87            raw_string: None,
88            string_literal_contains_escapes: false,
89        }
90    }
91
92    /// \return the kind of this token.
93    pub fn kind(&self) -> TokenKind {
94        self.kind
95    }
96    /// \return true if this token is a reserved word.
97    pub fn is_res_word(&self) -> bool {
98        self.kind.is_res_word()
99    }
100    /// \return true if this token is one of the four template-literal kinds.
101    pub fn is_template_literal(&self) -> bool {
102        matches!(
103            self.kind,
104            TokenKind::no_substitution_template
105                | TokenKind::template_head
106                | TokenKind::template_middle
107                | TokenKind::template_tail
108        )
109    }
110
111    /// \return the location of the first byte of the token.
112    pub fn start_loc(&self) -> SMLoc {
113        self.range.start
114    }
115    /// \return the location one past the last byte of the token.
116    pub fn end_loc(&self) -> SMLoc {
117        self.range.end
118    }
119    /// \return the half-open source range covered by the token.
120    pub fn source_range(&self) -> SMRange {
121        self.range
122    }
123
124    /// \return the value of a `numeric_literal` token.
125    pub fn get_numeric_literal(&self) -> f64 {
126        debug_assert_eq!(self.kind, TokenKind::numeric_literal);
127        self.numeric
128    }
129
130    /// \return the interned name of an `identifier` token.
131    pub fn get_identifier(&self) -> AtomBytes {
132        debug_assert_eq!(self.kind, TokenKind::identifier);
133        self.ident.unwrap()
134    }
135    /// \return the interned name of a `private_identifier` token, without
136    /// the leading `#` (which is still part of the token's source range).
137    pub fn get_private_identifier(&self) -> AtomBytes {
138        debug_assert_eq!(self.kind, TokenKind::private_identifier);
139        self.ident.unwrap()
140    }
141    /// \return the interned spelling of a reserved-word token.
142    pub fn get_res_word_identifier(&self) -> AtomBytes {
143        debug_assert!(self.is_res_word());
144        self.ident.unwrap()
145    }
146    /// \return the interned spelling of an identifier or reserved word,
147    /// for the many places where the grammar accepts either.
148    pub fn get_res_word_or_identifier(&self) -> AtomBytes {
149        debug_assert!(self.kind == TokenKind::identifier || self.is_res_word());
150        self.ident.unwrap()
151    }
152
153    /// \return the cooked value of a `string_literal` token, with escapes
154    /// and line continuations already processed.
155    pub fn get_string_literal(&self) -> AtomBytes {
156        debug_assert_eq!(self.kind, TokenKind::string_literal);
157        self.string_literal.unwrap()
158    }
159    /// \return the raw (undecoded) text of a JSX string literal. Only set by
160    /// `Token::set_jsx_string_literal`; the normal string path leaves it unset.
161    pub fn get_string_literal_raw_value(&self) -> AtomBytes {
162        debug_assert_eq!(self.kind, TokenKind::string_literal);
163        self.raw_string.unwrap()
164    }
165    /// \return whether a `string_literal` contained any escape or line
166    /// continuation. The parser uses this to build a directive's raw text:
167    /// with no escapes the raw equals the cooked value, otherwise it must
168    /// re-slice the source between the quotes.
169    pub fn get_string_literal_contains_escapes(&self) -> bool {
170        debug_assert_eq!(self.kind, TokenKind::string_literal);
171        self.string_literal_contains_escapes
172    }
173
174    /// \return whether the template literal token contains a NotEscapeSequence.
175    pub fn get_template_literal_contains_not_escapes(&self) -> bool {
176        debug_assert!(self.is_template_literal());
177        self.string_literal.is_none()
178    }
179    /// \return the cooked value of a template-literal token, or `None` if it
180    /// contains a NotEscapeSequence (legal only in a tagged template).
181    pub fn get_template_value(&self) -> Option<AtomBytes> {
182        debug_assert!(self.is_template_literal());
183        self.string_literal
184    }
185    /// \return the Template Raw Value (TRV) of a template-literal token.
186    pub fn get_template_raw_value(&self) -> AtomBytes {
187        debug_assert!(self.is_template_literal());
188        self.raw_string.unwrap()
189    }
190
191    /// \return the value of a `bigint_literal` token as text, never converted
192    /// to a number: the literal without its trailing `n` and with numeric
193    /// separators removed, but with any radix prefix kept (`0xF_Fn` ->
194    /// `0xFF`). This is the ESTree `bigint` property.
195    pub fn get_bigint_literal(&self) -> AtomBytes {
196        debug_assert_eq!(self.kind, TokenKind::bigint_literal);
197        self.string_literal.unwrap()
198    }
199    /// \return the raw source text of a `bigint_literal` token, including
200    /// any radix prefix and the trailing `n`.
201    pub fn get_bigint_literal_raw_value(&self) -> AtomBytes {
202        debug_assert_eq!(self.kind, TokenKind::bigint_literal);
203        self.raw_string.unwrap()
204    }
205
206    /// \return the body and flags of a `regexp_literal` token.
207    pub fn get_regexp_literal(&self) -> RegExpLiteral {
208        debug_assert_eq!(self.kind, TokenKind::regexp_literal);
209        self.regexp.unwrap()
210    }
211
212    /// \return the value of a `jsx_text` token, with HTML entities decoded.
213    pub fn get_jsx_text_value(&self) -> AtomBytes {
214        debug_assert_eq!(self.kind, TokenKind::jsx_text);
215        self.string_literal.unwrap()
216    }
217    /// \return the raw source text of a `jsx_text` token, with HTML entities
218    /// left as written.
219    pub fn get_jsx_text_raw(&self) -> AtomBytes {
220        debug_assert_eq!(self.kind, TokenKind::jsx_text);
221        self.raw_string.unwrap()
222    }
223
224    // ---- Setters (crate-visible: only the lexer mutates the token) ----------
225
226    pub(crate) fn set_start(&mut self, start: SMLoc) {
227        self.range.start = start;
228    }
229    pub(crate) fn set_end(&mut self, end: SMLoc) {
230        self.range.end = end;
231    }
232    pub(crate) fn set_range(&mut self, range: SMRange) {
233        self.range = range;
234    }
235
236    pub(crate) fn set_punctuator(&mut self, kind: TokenKind) {
237        self.kind = kind;
238    }
239    /// Set the TokenKind to a given IDENT_OP token.
240    pub(crate) fn set_ident_op(&mut self, kind: TokenKind) {
241        self.kind = kind;
242    }
243    pub(crate) fn set_eof(&mut self) {
244        self.kind = TokenKind::eof;
245    }
246
247    pub(crate) fn set_bigint_literal(&mut self, bigint: AtomBytes, raw: AtomBytes) {
248        self.kind = TokenKind::bigint_literal;
249        self.string_literal = Some(bigint);
250        self.raw_string = Some(raw);
251    }
252    pub(crate) fn set_numeric_literal(&mut self, literal: f64) {
253        self.kind = TokenKind::numeric_literal;
254        self.numeric = literal;
255    }
256    pub(crate) fn set_identifier(&mut self, ident: AtomBytes) {
257        self.kind = TokenKind::identifier;
258        self.ident = Some(ident);
259    }
260    pub(crate) fn set_private_identifier(&mut self, ident: AtomBytes) {
261        self.kind = TokenKind::private_identifier;
262        self.ident = Some(ident);
263    }
264    pub(crate) fn set_string_literal(&mut self, literal: AtomBytes, contains_escapes: bool) {
265        self.kind = TokenKind::string_literal;
266        self.string_literal = Some(literal);
267        self.string_literal_contains_escapes = contains_escapes;
268    }
269    /// Port of C++ `Token::setJSXStringLiteral`. The lexer's JSX string path
270    /// uses `set_string_literal` (matching the C++ `scanString`), so this is not
271    /// called by the lexer itself; it is kept for faithful `Token` surface
272    /// completeness and exercised by a unit test.
273    #[allow(dead_code)]
274    pub(crate) fn set_jsx_string_literal(&mut self, literal: AtomBytes, raw: AtomBytes) {
275        self.kind = TokenKind::string_literal;
276        self.string_literal = Some(literal);
277        self.raw_string = Some(raw);
278        self.string_literal_contains_escapes = false;
279    }
280    pub(crate) fn set_regexp_literal(&mut self, literal: RegExpLiteral) {
281        self.kind = TokenKind::regexp_literal;
282        self.regexp = Some(literal);
283    }
284    pub(crate) fn set_res_word(&mut self, kind: TokenKind, ident: AtomBytes) {
285        debug_assert!(kind.is_res_word());
286        self.kind = kind;
287        self.ident = Some(ident);
288    }
289    pub(crate) fn set_template_literal(
290        &mut self,
291        kind: TokenKind,
292        cooked: Option<AtomBytes>,
293        raw: AtomBytes,
294    ) {
295        debug_assert!(matches!(
296            kind,
297            TokenKind::no_substitution_template
298                | TokenKind::template_head
299                | TokenKind::template_middle
300                | TokenKind::template_tail
301        ));
302        self.kind = kind;
303        self.string_literal = cooked;
304        self.raw_string = Some(raw);
305    }
306    pub(crate) fn set_jsx_text(&mut self, value: AtomBytes, raw: AtomBytes) {
307        self.kind = TokenKind::jsx_text;
308        self.string_literal = Some(value);
309        self.raw_string = Some(raw);
310    }
311}
312
313/// The kind of a stored comment. Port of `StoredComment::Kind`.
314#[derive(Copy, Clone, Eq, PartialEq, Debug)]
315pub enum CommentKind {
316    /// Comment that begins with "//".
317    Line,
318    /// Comment that is delimited by "/*" and "*/".
319    Block,
320    /// Comment that begins with "#!" and starts at the first byte of the file.
321    Hashbang,
322}
323
324/// Represents a comment stored while lexing the file. Port of `StoredComment`.
325#[derive(Copy, Clone, Debug)]
326pub struct StoredComment {
327    kind: CommentKind,
328    range: SMRange,
329}
330
331impl StoredComment {
332    /// A comment of `kind` spanning `range`, delimiters included.
333    pub fn new(kind: CommentKind, range: SMRange) -> StoredComment {
334        StoredComment { kind, range }
335    }
336    /// \return whether this is a line, block, or hashbang comment.
337    pub fn kind(&self) -> CommentKind {
338        self.kind
339    }
340    /// \return the source range of the comment, delimiters included.
341    pub fn source_range(&self) -> SMRange {
342        self.range
343    }
344
345    /// \return the comment with delimiters (//, /*, */, #!) stripped. Port of
346    /// `StoredComment::getString` (JSLexer.h:339-347).
347    ///
348    /// Unlike the C++, which dereferences pointers into the source buffer, our
349    /// offset-based comment can't deref a pointer, so the caller passes the
350    /// source `buffer` bytes and we slice into it.
351    pub fn get_string<'a>(&self, buffer: &'a [u8]) -> &'a [u8] {
352        // Ignore opening delimiter.
353        let start = self.range.start.offset as usize + 2;
354        // Conditionally ignore closing delimiter.
355        let end = if self.kind == CommentKind::Block {
356            self.range.end.offset as usize - 2
357        } else {
358            self.range.end.offset as usize
359        };
360        debug_assert!(end >= start, "invalid comment range");
361        &buffer[start..end]
362    }
363
364    /// \return the comment with delimiters (//, /*, */, #!) included. Port of
365    /// `StoredComment::getFullString` (JSLexer.h:349-355).
366    ///
367    /// Unlike the C++, which dereferences pointers into the source buffer, our
368    /// offset-based comment can't deref a pointer, so the caller passes the
369    /// source `buffer` bytes and we slice into it.
370    pub fn get_full_string<'a>(&self, buffer: &'a [u8]) -> &'a [u8] {
371        &buffer[self.range.start.offset as usize..self.range.end.offset as usize]
372    }
373}
374
375/// Stored token when lexing. Port of `StoredToken`.
376#[derive(Copy, Clone, Debug)]
377pub struct StoredToken {
378    kind: TokenKind,
379    range: SMRange,
380}
381
382impl StoredToken {
383    /// A stored token of `kind` spanning `range`.
384    pub fn new(kind: TokenKind, range: SMRange) -> StoredToken {
385        StoredToken { kind, range }
386    }
387    /// \return the kind of the stored token.
388    pub fn kind(&self) -> TokenKind {
389        self.kind
390    }
391    /// \return the source range of the stored token.
392    pub fn source_range(&self) -> SMRange {
393        self.range
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400    use crate::token_kinds::TokenKind;
401    use hermes_support::location::{SMLoc, SMRange, SourceId};
402
403    #[test]
404    fn punctuator_token() {
405        let id = SourceId::from_index(0);
406        let mut t = Token::new(id);
407        t.set_punctuator(TokenKind::l_brace);
408        t.set_range(SMRange {
409            start: SMLoc {
410                source: id,
411                offset: 0,
412            },
413            end: SMLoc {
414                source: id,
415                offset: 1,
416            },
417        });
418        assert_eq!(t.kind(), TokenKind::l_brace);
419        assert_eq!(t.start_loc().offset, 0);
420        assert_eq!(t.end_loc().offset, 1);
421    }
422
423    #[test]
424    fn jsx_string_literal_value_and_raw() {
425        // set_jsx_string_literal sets value + raw + escapes=false. Exercises
426        // set_jsx_string_literal and get_string_literal_raw_value (a faithful
427        // Token surface the parser consumes; otherwise unexercised in-crate).
428        let tab = hermes_atom_table::AtomTable::new();
429        let value = tab.atom_bytes(b"a<b");
430        let raw = tab.atom_bytes(b"a&lt;b");
431        let mut t = Token::new(SourceId::from_index(0));
432        t.set_jsx_string_literal(value, raw);
433        assert_eq!(t.kind(), TokenKind::string_literal);
434        assert_eq!(t.get_string_literal(), value);
435        assert_eq!(t.get_string_literal_raw_value(), raw);
436        assert!(!t.get_string_literal_contains_escapes());
437    }
438
439    #[test]
440    fn template_literal_contains_not_escapes() {
441        // get_template_literal_contains_not_escapes() == (cooked is None).
442        let tab = hermes_atom_table::AtomTable::new();
443        let raw = tab.atom_bytes(b"\\9");
444        let mut t = Token::new(SourceId::from_index(0));
445        t.set_template_literal(TokenKind::no_substitution_template, None, raw);
446        assert!(t.get_template_literal_contains_not_escapes());
447        let cooked = tab.atom_bytes(b"ok");
448        t.set_template_literal(TokenKind::template_head, Some(cooked), raw);
449        assert!(!t.get_template_literal_contains_not_escapes());
450        assert_eq!(t.get_template_value(), Some(cooked));
451    }
452
453    #[test]
454    fn stored_comment_get_string() {
455        let id = SourceId::from_index(0);
456        let loc = |off| SMLoc {
457            source: id,
458            offset: off,
459        };
460        // Buffer: a line comment then a block comment.
461        //          0         1         2
462        //          0123456789012345678901234
463        let buffer = b"// hello /* world */ rest";
464        // Line comment "// hello" spans [0, 8).
465        let line = StoredComment::new(
466            CommentKind::Line,
467            SMRange {
468                start: loc(0),
469                end: loc(8),
470            },
471        );
472        assert_eq!(line.get_string(buffer), b" hello");
473        assert_eq!(line.get_full_string(buffer), b"// hello");
474        // Block comment "/* world */" spans [9, 20).
475        let block = StoredComment::new(
476            CommentKind::Block,
477            SMRange {
478                start: loc(9),
479                end: loc(20),
480            },
481        );
482        assert_eq!(block.get_string(buffer), b" world ");
483        assert_eq!(block.get_full_string(buffer), b"/* world */");
484    }
485}