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 adjust_start_column_for_non_ascii(
code: &str,
mut start_column: usize,
) -> usize {
let utf16_code_units: Vec<u16> = code.encode_utf16().collect();
let mut index = 0;
while index < start_column {
if utf16_code_units.get(index).copied().unwrap_or_default() > 127 {
start_column += 1;
}
index += 1;
}
start_column
}
fn get_first_expression(code: &str, original_start_col_index: usize) -> &str {
let start_index =
adjust_start_column_for_non_ascii(code, original_start_col_index);
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());
if start <= end && end <= code.len() {
&code[start..end]
} else {
code
}
}