use std::cmp::max;
use oxc_allocator::ArenaStringBuilder;
use oxc_data_structures::branch_hints::cold_path;
use crate::{config::LexerConfig as Config, diagnostics};
use super::{
Kind, Lexer, Span, Token, cold_branch,
search::{SafeByteMatchTable, byte_search, safe_byte_match_table},
};
const fn to_bytes<const N: usize>(ch: char) -> [u8; N] {
assert!(ch.len_utf8() == N);
let mut bytes = [0u8; N];
ch.encode_utf8(&mut bytes);
bytes
}
const LOSSY_REPLACEMENT_CHAR_BYTES: [u8; 3] = to_bytes('\u{FFFD}');
const LOSSY_REPLACEMENT_CHAR_FIRST_BYTE: u8 = LOSSY_REPLACEMENT_CHAR_BYTES[0];
const _: () = assert!(LOSSY_REPLACEMENT_CHAR_FIRST_BYTE == 0xEF);
const MIN_ESCAPED_STR_LEN: usize = 16;
static DOUBLE_QUOTE_STRING_END_TABLE: SafeByteMatchTable =
safe_byte_match_table!(|b| matches!(b, b'"' | b'\r' | b'\n' | b'\\'));
static SINGLE_QUOTE_STRING_END_TABLE: SafeByteMatchTable =
safe_byte_match_table!(|b| matches!(b, b'\'' | b'\r' | b'\n' | b'\\'));
static DOUBLE_QUOTE_ESCAPED_MATCH_TABLE: SafeByteMatchTable = safe_byte_match_table!(|b| matches!(
b,
b'"' | b'\r' | b'\n' | b'\\' | LOSSY_REPLACEMENT_CHAR_FIRST_BYTE
));
static SINGLE_QUOTE_ESCAPED_MATCH_TABLE: SafeByteMatchTable = safe_byte_match_table!(|b| matches!(
b,
b'\'' | b'\r' | b'\n' | b'\\' | LOSSY_REPLACEMENT_CHAR_FIRST_BYTE
));
macro_rules! handle_string_literal {
($lexer:ident, $delimiter:literal, $table:ident, $escaped_table:ident) => {{
debug_assert!($delimiter.is_ascii());
let after_opening_quote = $lexer.source.position().add(1);
let next_byte = byte_search! {
lexer: $lexer,
table: $table,
start: after_opening_quote,
handle_eof: {
return cold_branch(|| {
$lexer.error(diagnostics::unterminated_string($lexer.unterminated_range()));
Kind::Undetermined
});
},
};
match next_byte {
$delimiter => {
$lexer.source.next_byte_unchecked();
Kind::Str
}
b'\\' => cold_branch(|| {
handle_string_literal_escape!(
$lexer,
$delimiter,
$escaped_table,
after_opening_quote
)
}),
_ => {
cold_branch(|| {
debug_assert!(matches!(next_byte, b'\r' | b'\n'));
$lexer.consume_char();
$lexer.error(diagnostics::unterminated_string($lexer.unterminated_range()));
Kind::Undetermined
})
}
}
}};
}
macro_rules! handle_string_literal_escape {
($lexer:ident, $delimiter:literal, $table:ident, $after_opening_quote:ident) => {{
let so_far = $lexer.source.str_from_pos_to_current($after_opening_quote);
let capacity = max(so_far.len() * 2, MIN_ESCAPED_STR_LEN);
let mut str = ArenaStringBuilder::with_capacity_in(capacity, $lexer.allocator);
str.push_str(so_far);
'outer: loop {
let escape_start_offset = $lexer.offset();
$lexer.consume_char();
let mut is_valid_escape_sequence = true;
$lexer.read_string_escape_sequence(&mut str, false, &mut is_valid_escape_sequence);
if !is_valid_escape_sequence {
let range = Span::new(escape_start_offset, $lexer.offset());
$lexer.error(diagnostics::invalid_escape_sequence(range));
}
let mut chunk_start = $lexer.source.position();
while let Some(b) = $lexer.peek_byte() {
match b {
b if !$table.matches(b) => {
$lexer.source.next_byte_unchecked();
}
b if b == $delimiter => {
let chunk = $lexer.source.str_from_pos_to_current(chunk_start);
str.push_str(chunk);
$lexer.source.next_byte_unchecked();
break 'outer;
}
b'\\' => {
let chunk = $lexer.source.str_from_pos_to_current(chunk_start);
str.push_str(chunk);
continue 'outer;
}
LOSSY_REPLACEMENT_CHAR_FIRST_BYTE => {
cold_path();
$lexer.source.next_byte_unchecked();
let next1 = $lexer.source.next_byte_unchecked();
let next2 = $lexer.source.next_byte_unchecked();
if $lexer.token.lone_surrogates()
&& [next1, next2] == [LOSSY_REPLACEMENT_CHAR_BYTES[1], LOSSY_REPLACEMENT_CHAR_BYTES[2]]
{
let chunk = $lexer.source.str_from_pos_to_current(chunk_start);
str.push_str(chunk);
str.push_str("fffd");
chunk_start = $lexer.source.position();
}
}
_ => {
return cold_branch(|| {
debug_assert!(matches!(b, b'\r' | b'\n'));
$lexer.consume_char();
$lexer.error(diagnostics::unterminated_string($lexer.unterminated_range()));
Kind::Undetermined
});
}
}
}
$lexer.error(diagnostics::unterminated_string($lexer.unterminated_range()));
return Kind::Undetermined;
}
$lexer.save_string(true, str.into_str());
Kind::Str
}};
}
impl<'a, C: Config> Lexer<'a, C> {
pub(super) unsafe fn read_string_literal_double_quote(&mut self) -> Kind {
unsafe {
handle_string_literal!(
self,
b'"',
DOUBLE_QUOTE_STRING_END_TABLE,
DOUBLE_QUOTE_ESCAPED_MATCH_TABLE
)
}
}
pub(super) unsafe fn read_string_literal_single_quote(&mut self) -> Kind {
unsafe {
handle_string_literal!(
self,
b'\'',
SINGLE_QUOTE_STRING_END_TABLE,
SINGLE_QUOTE_ESCAPED_MATCH_TABLE
)
}
}
pub(super) fn save_string(&mut self, has_escape: bool, s: &'a str) {
if has_escape {
self.save_escaped_string(s);
}
}
#[cold]
fn save_escaped_string(&mut self, s: &'a str) {
self.escaped_strings.insert(self.token.start(), s);
self.token.set_escaped(true);
}
pub(crate) fn get_string(&self, token: Token) -> &'a str {
if token.escaped() {
return self.escaped_strings[&token.start()];
}
let source_text = self.source.whole();
let mut start = token.start() as usize;
let mut end = token.end() as usize;
match token.kind() {
Kind::Str => {
start += 1;
end -= 1;
}
Kind::PrivateIdentifier => {
start += 1;
}
_ => {}
}
&source_text[start..end]
}
}