diff --git a/crates/js/src/lexer.rs b/crates/js/src/lexer.rs index 57b1e07..845c3e6 100644 --- a/crates/js/src/lexer.rs +++ b/crates/js/src/lexer.rs @@ -673,8 +673,9 @@ impl<'a> Lexer<'a> { } Some(b'\\') => { self.advance(); // backslash - let ch = self.scan_escape_sequence()?; - value.push(ch); + if let Some(ch) = self.scan_escape_sequence()? { + value.push(ch); + } } Some(_) => { let ch = self.advance_char(); @@ -686,24 +687,27 @@ impl<'a> Lexer<'a> { Ok(TokenKind::String(value)) } - fn scan_escape_sequence(&mut self) -> Result { + /// Scan an escape sequence after the backslash has been consumed. + /// Returns `None` for line continuations (`\`), which produce no character. + fn scan_escape_sequence(&mut self) -> Result, LexError> { let pos = self.current_pos(); match self.advance() { - Some(b'n') => Ok('\n'), - Some(b'r') => Ok('\r'), - Some(b't') => Ok('\t'), - Some(b'b') => Ok('\u{0008}'), - Some(b'f') => Ok('\u{000C}'), - Some(b'v') => Ok('\u{000B}'), - Some(b'0') if !matches!(self.peek(), Some(b'0'..=b'9')) => Ok('\0'), - Some(b'\\') => Ok('\\'), - Some(b'\'') => Ok('\''), - Some(b'"') => Ok('"'), - Some(b'`') => Ok('`'), - Some(b'\n') => Ok('\n'), + Some(b'n') => Ok(Some('\n')), + Some(b'r') => Ok(Some('\r')), + Some(b't') => Ok(Some('\t')), + Some(b'b') => Ok(Some('\u{0008}')), + Some(b'f') => Ok(Some('\u{000C}')), + Some(b'v') => Ok(Some('\u{000B}')), + Some(b'0') if !matches!(self.peek(), Some(b'0'..=b'9')) => Ok(Some('\0')), + Some(b'\\') => Ok(Some('\\')), + Some(b'\'') => Ok(Some('\'')), + Some(b'"') => Ok(Some('"')), + Some(b'`') => Ok(Some('`')), + // Line continuation: \ produces no character + Some(b'\n') => Ok(None), Some(b'\r') => { self.advance_if(b'\n'); - Ok('\n') + Ok(None) } Some(b'x') => { let hi = self.advance().and_then(hex_digit_val).ok_or(LexError { @@ -715,12 +719,12 @@ impl<'a> Lexer<'a> { pos, })?; let code = (hi << 4) | lo; - Ok(code as char) + Ok(Some(code as char)) } - Some(b'u') => self.scan_unicode_escape(pos), + Some(b'u') => self.scan_unicode_escape(pos).map(Some), Some(b) => { // identity escape - Ok(b as char) + Ok(Some(b as char)) } None => Err(LexError { message: "unexpected end of input in escape sequence".into(), @@ -839,8 +843,9 @@ impl<'a> Lexer<'a> { } Some(b'\\') => { self.advance(); - let ch = self.scan_escape_sequence()?; - value.push(ch); + if let Some(ch) = self.scan_escape_sequence()? { + value.push(ch); + } } Some(_) => { let ch = self.advance_char(); @@ -887,8 +892,9 @@ impl<'a> Lexer<'a> { } Some(b'\\') => { self.advance(); - let ch = self.scan_escape_sequence()?; - value.push(ch); + if let Some(ch) = self.scan_escape_sequence()? { + value.push(ch); + } } Some(_) => { let ch = self.advance_char(); @@ -1538,6 +1544,15 @@ mod tests { ); } + #[test] + fn test_string_line_continuation() { + // \ is a line continuation producing no character + assert_eq!( + kind("\"line1\\\nline2\""), + TokenKind::String("line1line2".into()) + ); + } + #[test] fn test_empty_string() { assert_eq!(kind(r#""""#), TokenKind::String("".into()));