use deno_ast::MediaType;
use deno_ast::TokenOrComment;
use deno_ast::swc::parser::token::Token;
use deno_ast::swc::parser::token::Word;
use deno_core::op2;
use deno_core::v8;
use deno_error::JsErrorBox;
#[op2]
pub fn op_node_get_first_expression<'s>(
scope: &mut v8::PinScope<'s, '_>,
arg: v8::Local<v8::Value>,
) -> Result<v8::Local<'s, v8::Value>, JsErrorBox> {
if !arg.is_object() {
return Err(JsErrorBox::type_error("Argument must be an object"));
}
let msg = v8::Exception::create_message(scope, arg);
let source_line: String;
if let Some(inner_source_line) = msg.get_source_line(scope) {
source_line = inner_source_line.to_rust_string_lossy(scope);
} else {
return Ok(v8::undefined(scope).into());
}
let start_column = msg.get_start_column();
let result = get_first_expression(&source_line, start_column);
Ok(v8::String::new(scope, result).unwrap().into())
}
fn is_member_access_token(token: &Token) -> bool {
matches!(token, Token::Dot | Token::LBracket | Token::RBracket)
}
fn is_member_name_token(token: &Token) -> bool {
matches!(
token,
Token::Word(..) | Token::Str { .. } | Token::Num { .. }
)
}
fn is_ident_word(token: &Token) -> bool {
matches!(token, Token::Word(Word::Ident(..)))
}
fn token_text<'a>(
code: &'a str,
range: &std::ops::Range<usize>,
) -> Option<&'a str> {
if range.start <= range.end
&& range.end <= code.len()
&& code.is_char_boundary(range.start)
&& code.is_char_boundary(range.end)
{
Some(&code[range.start..range.end])
} else {
None
}
}
fn is_question_token(code: &str, range: &std::ops::Range<usize>) -> bool {
token_text(code, range) == Some("?")
}
fn utf16_to_utf8_offset(code: &str, offset_utf16: usize) -> Option<usize> {
let mut current_utf16 = 0;
for (offset_utf8, c) in code.char_indices() {
if current_utf16 == offset_utf16 {
return Some(offset_utf8);
}
current_utf16 += c.len_utf16();
if current_utf16 > offset_utf16 {
return None;
}
}
(current_utf16 == offset_utf16).then_some(code.len())
}
fn get_first_expression(code: &str, original_start_col_index: usize) -> &str {
let Some(start_index) = utf16_to_utf8_offset(code, original_start_col_index)
else {
return code;
};
let items = deno_ast::lex(code, MediaType::JavaScript);
let tokens: Vec<(Token, std::ops::Range<usize>)> = items
.into_iter()
.filter_map(|item| match item.inner {
TokenOrComment::Token(token) => Some((token, item.range)),
TokenOrComment::Comment { .. } => None,
})
.collect();
let mut last_token = None;
let mut second_last_token = None;
let mut first_member_access_name_token = None; let mut terminating_col = None;
let mut paren_lvl = 0;
for (token, range) in &tokens {
if range.start < start_index {
if matches!(token, Token::Semi) {
first_member_access_name_token = None;
second_last_token = last_token;
last_token = Some((token, range));
continue;
}
let prev_is_question = last_token
.map(|(_, last_range)| is_question_token(code, last_range))
.unwrap_or(false);
let is_optional_chain_dot =
matches!(token, Token::Dot) && prev_is_question;
let is_member_access =
is_member_access_token(token) || is_optional_chain_dot;
let member_access_base_token = if is_optional_chain_dot {
second_last_token
} else {
last_token
};
if is_member_access
&& first_member_access_name_token.is_none()
&& let Some((last_tok, last_range)) = member_access_base_token
&& is_ident_word(last_tok)
{
first_member_access_name_token = Some(last_range.start);
} else if !is_member_access
&& !is_member_name_token(token)
&& !is_question_token(code, range)
{
first_member_access_name_token = None;
}
second_last_token = last_token;
last_token = Some((token, range));
continue;
}
if matches!(token, Token::LParen) {
paren_lvl += 1;
continue;
}
if matches!(token, Token::RParen) {
paren_lvl -= 1;
if paren_lvl == 0 {
terminating_col = Some(range.start + 1);
break;
}
continue;
}
if matches!(token, Token::Semi) {
terminating_col = Some(range.start);
break;
}
}
let start = first_member_access_name_token.unwrap_or(start_index);
let end = terminating_col.unwrap_or(code.len());
code.get(start..end).unwrap_or(code)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_utf16_to_utf8_offset() {
let text = "a😀汉b";
assert_eq!(utf16_to_utf8_offset(text, 0), Some(0));
assert_eq!(utf16_to_utf8_offset(text, 1), Some(1));
assert_eq!(utf16_to_utf8_offset(text, 2), None);
assert_eq!(utf16_to_utf8_offset(text, 3), Some(5));
assert_eq!(utf16_to_utf8_offset(text, 4), Some(8));
assert_eq!(utf16_to_utf8_offset(text, 5), Some(9));
assert_eq!(utf16_to_utf8_offset(text, 6), None);
}
#[test]
fn test_get_first_expression_with_multibyte_identifier() {
assert_eq!(get_first_expression("汉汉.ok(false);", 3), "汉汉.ok(false)");
}
#[test]
fn test_get_first_expression_with_invalid_utf16_offset() {
let code = "😀; assert.ok(false);";
assert_eq!(get_first_expression(code, 1), code);
}
}