use hermes_ast::context::{Context, GCLock, NodeRc};
use hermes_ast::node::{ExpressionStatement, Node, NumericLiteral, Program};
use hermes_ast::node_child::{NodeList, NodeMetadata, Strictness};
use hermes_atom_table::INVALID_ATOM_BYTES;
use hermes_parser::js::JSParserImpl;
use hermes_parser::lexer::{GrammarContext, JSLexer};
use hermes_support::diag::{DiagHandler, ResolvedDiagnostic};
use hermes_support::manager::SourceErrorManager;
use hermes_sema::dump::sem_dump;
use hermes_sema::ids::FunctionInfoId;
use hermes_sema::keywords::Keywords;
use hermes_sema::resolve::{resolve_ast, resolve_ast_for_parser};
use hermes_sema::sem_context::{DeclKind, SemContext};
use std::cell::RefCell;
use std::rc::Rc;
fn parse<'gc>(
gc: &'gc GCLock,
sm: &mut SourceErrorManager,
src: &str,
) -> &'gc Node<'gc> {
let buf_id = sm.add_buffer_bytes("input", src.as_bytes());
let result: Option<&Node> = {
let atoms = &gc.ctx().atom_table;
let lexer =
JSLexer::new(buf_id, sm, atoms, GrammarContext::AllowRegExp);
let mut parser = JSParserImpl::new(gc, lexer);
parser.parse()
};
assert_eq!(sm.error_count(), 0, "unexpected parse errors in: {src}");
result.expect("parser returned no Program")
}
fn resolve(src: &str) -> (SemContext, Strictness) {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let root = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.unwrap_or_else(|| panic!("resolution failed for: {src}"));
let strictness = match root {
Node::Program(p) => p.strictness.get(),
_ => unreachable!(),
};
(sem_ctx, strictness)
}
fn atom_string(gc: &GCLock, atom: hermes_atom_table::AtomBytes) -> String {
String::from_utf8(gc.bytes(atom).to_vec()).expect("atom is not UTF-8")
}
#[test]
fn empty_program_creates_global_function_and_scope() {
let (sem_ctx, strictness) = resolve("");
sem_ctx.assert_global_function_and_scope();
let global_fn = sem_ctx.get_global_function();
let global_scope = sem_ctx.get_global_scope();
let info = sem_ctx.function(global_fn);
assert!(info.is_program_node);
assert!(!info.strict);
assert_eq!(info.get_scopes(), &[global_scope]);
assert_eq!(info.get_function_body_scope(), global_scope);
assert_eq!(sem_ctx.scope(global_scope).parent_function, global_fn);
assert_eq!(sem_ctx.scope(global_scope).parent_scope, None);
assert_eq!(sem_ctx.scope(global_scope).depth, 0);
assert_eq!(strictness, Strictness::NonStrictMode);
assert!(sem_ctx.scope(global_scope).decls.is_empty());
assert!(!sem_ctx.get_binding_table_global_scope().is_null());
}
#[test]
fn use_strict_directive_sets_strictness() {
let (sem_ctx, strictness) = resolve("\"use strict\";\n");
assert!(sem_ctx.function(sem_ctx.get_global_function()).strict);
assert_eq!(strictness, Strictness::StrictMode);
}
#[test]
fn non_prologue_string_is_not_a_directive() {
let (sem_ctx, strictness) = resolve("1;\n\"use strict\";\n");
assert!(!sem_ctx.function(sem_ctx.get_global_function()).strict);
assert_eq!(strictness, Strictness::NonStrictMode);
}
#[test]
fn ambient_decls_become_undeclared_global_properties() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let ambient = vec![NodeRc::from_node(
&gc,
parse(
&gc,
&mut sm,
"var a; var b; function b() {} function c() {} var a;",
),
)];
let root = parse(&gc, &mut sm, "");
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
assert!(resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &ambient).is_some());
let global_scope = sem_ctx.get_global_scope();
let names: Vec<String> = sem_ctx
.scope(global_scope)
.decls
.iter()
.map(|d| {
let decl = sem_ctx.decl(*d);
assert_eq!(decl.kind, DeclKind::UndeclaredGlobalProperty);
assert_eq!(decl.scope, Some(global_scope));
atom_string(&gc, decl.name)
})
.collect();
assert_eq!(names, vec!["a", "b", "c"]);
}
#[test]
fn resolver_diagnostics_are_buffered_then_flushed() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let log: Rc<RefCell<Vec<String>>> = Rc::default();
sm.set_handler(Box::new(SharedHandler(Rc::clone(&log))));
let root = parse(&gc, &mut sm, "\"inline\";\n\"noinline\";\n");
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
{
let binding_table = sem_ctx.binding_table_rc();
let mut resolver = hermes_sema::resolver::SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
assert!(resolver.run(&gc, root).is_some());
assert_eq!(
log.borrow().len(),
0,
"message escaped the buffer before the resolver was dropped"
);
}
assert_eq!(
log.borrow().as_slice(),
["Should not declare both 'inline' and 'noinline'.".to_string()],
"buffered message was never flushed"
);
assert_eq!(sm.warning_count(), 1);
}
#[test]
fn unrewritten_resolution_returns_the_same_root() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(
&gc,
&mut sm,
"\"use strict\";\n1;\n;\n\"s\";\ntrue;\nnull;\n",
);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed");
assert!(
std::ptr::eq(root, resolved),
"an unrewritten tree must be returned as-is, not rebuilt"
);
}
#[test]
fn too_deeply_nested_ast_reports_the_recursion_limit() {
std::thread::Builder::new()
.stack_size(32 * 1024 * 1024)
.spawn(too_deeply_nested_ast_reports_the_recursion_limit_impl)
.expect("failed to spawn the deep-recursion test thread")
.join()
.expect("the deep-recursion test thread panicked");
}
fn too_deeply_nested_ast_reports_the_recursion_limit_impl() {
const DEPTH: usize = 1100;
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let log: Rc<RefCell<Vec<String>>> = Rc::default();
sm.set_handler(Box::new(SharedHandler(Rc::clone(&log))));
let range = parse(&gc, &mut sm, "1;\n").range();
let mut inner: &Node = gc.alloc(Node::NumericLiteral(NumericLiteral::new(
NodeMetadata::new(range),
1.0,
)));
for _ in 0..DEPTH {
inner = gc.alloc(Node::ExpressionStatement(ExpressionStatement::new(
NodeMetadata::new(range),
inner,
INVALID_ATOM_BYTES,
)));
}
let deep_root = gc.alloc(Node::Program(Program::new(
NodeMetadata::new(range),
NodeList::from_iter(&gc, [inner]),
)));
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, deep_root, &[]);
assert!(resolved.is_none(), "over-deep resolution must fail");
assert_eq!(
log.borrow().as_slice(),
["Too many nested expressions/statements/declarations".to_string()],
"the depth error must be reported exactly once"
);
assert_eq!(sm.error_count(), 1);
}
struct SharedHandler(Rc<RefCell<Vec<String>>>);
impl DiagHandler for SharedHandler {
fn handle(&mut self, diag: &ResolvedDiagnostic) {
self.0.borrow_mut().push(diag.message.clone());
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[test]
#[should_panic(expected = "$SHBuiltin.moduleFactory needs visitModuleFactory")]
fn shbuiltin_module_factory_is_not_modeled() {
resolve("$SHBuiltin.moduleFactory(1, function (g, r) {});");
}
#[test]
#[should_panic(expected = "$SHBuiltin.export needs visitModuleExport")]
fn shbuiltin_export_is_not_modeled() {
resolve("$SHBuiltin.export('x', 1);");
}
#[test]
#[should_panic(expected = "$SHBuiltin.import needs visitModuleImport")]
fn shbuiltin_import_is_not_modeled() {
resolve("$SHBuiltin.import(1, 'x');");
}
#[test]
fn a_direct_eval_marks_its_whole_scope_chain() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(
&gc,
&mut sm,
"function f() { { eval('1'); } }\nfunction g() { { 1; } }\n",
);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed");
assert_eq!(sem_ctx.functions_len(), 3);
let global = sem_ctx.get_global_function();
let f = FunctionInfoId::from_sema_id(hermes_ast::SemaId(1));
let g = FunctionInfoId::from_sema_id(hermes_ast::SemaId(2));
let marked = |func| -> Vec<bool> {
sem_ctx
.function(func)
.get_scopes()
.iter()
.map(|s| sem_ctx.scope(*s).local_eval)
.collect()
};
assert_eq!(marked(global), vec![true], "the global scope is an ancestor");
assert_eq!(marked(f), vec![true, true], "the call's scope and its parent");
assert_eq!(marked(g), vec![false, false], "an unrelated function");
}
#[test]
fn disabled_eval_warns_differently_and_marks_no_scope() {
let mut ctx = Context::new();
ctx.set_enable_eval(false);
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let log: Rc<RefCell<Vec<String>>> = Rc::default();
sm.set_handler(Box::new(SharedHandler(Rc::clone(&log))));
let root = parse(&gc, &mut sm, "eval('1');\n");
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed");
assert_eq!(
log.borrow().as_slice(),
["eval() is disabled at runtime".to_string()]
);
assert!(
!sem_ctx.scope(sem_ctx.get_global_scope()).local_eval,
"registerLocalEval must not run when eval is disabled"
);
}
#[test]
fn var_declaration_at_global_scope_is_a_global_property() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "var x;\n");
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed for: var x;");
let global_scope = sem_ctx.get_global_scope();
assert_eq!(sem_ctx.scope(global_scope).decls.len(), 1);
let decl = sem_ctx.decl(sem_ctx.scope(global_scope).decls[0]);
assert_eq!(decl.kind, DeclKind::GlobalProperty);
}
#[test]
fn loose_identifier_reference_becomes_undeclared_global_property() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "x;\n");
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed for: x;");
let global_scope = sem_ctx.get_global_scope();
assert_eq!(sem_ctx.scope(global_scope).decls.len(), 1);
let decl = sem_ctx.decl(sem_ctx.scope(global_scope).decls[0]);
assert_eq!(decl.kind, DeclKind::UndeclaredGlobalProperty);
}
fn with_first_expression<R>(src: &str, f: impl FnOnce(&Node) -> R) -> R {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.unwrap_or_else(|| panic!("resolution failed for: {src}"));
let Node::Program(p) = resolved else {
unreachable!("resolve_ast returned a non-Program root")
};
let stmt = p.body.iter().next().expect("empty program body");
let Node::ExpressionStatement(es) = stmt else {
panic!("first statement is not an ExpressionStatement")
};
f(es.expression)
}
#[test]
fn constant_binary_chain_folds_to_a_single_literal() {
with_first_expression("1 + 2 - 3;\n", |e| match e {
Node::NumericLiteral(n) => assert_eq!(n.value.get(), 0.0),
other => {
panic!("expected a folded literal, got {}", other.node_type_str())
}
});
}
#[test]
fn partially_constant_binary_chain_folds_its_prefix() {
with_first_expression("1 + 2 + x;\n", |e| {
let be = e
.as_binary_expression()
.expect("the outer link must survive as a BinaryExpression");
match be.left {
Node::NumericLiteral(n) => assert_eq!(n.value.get(), 3.0),
other => panic!(
"left should be the folded 3, got {}",
other.node_type_str()
),
}
assert!(matches!(be.right, Node::Identifier(_)));
});
}
#[test]
fn binary_chain_stops_folding_at_the_first_failure() {
with_first_expression("x + 1 + 2;\n", |e| {
let be = e.as_binary_expression().expect("nothing may fold here");
let inner = be
.left
.as_binary_expression()
.expect("the inner link must survive too");
assert!(matches!(inner.left, Node::Identifier(_)));
assert!(matches!(inner.right, Node::NumericLiteral(_)));
assert!(matches!(be.right, Node::NumericLiteral(_)));
});
}
#[test]
fn non_linearized_binary_still_folds() {
with_first_expression("6 * 7;\n", |e| match e {
Node::NumericLiteral(n) => assert_eq!(n.value.get(), 42.0),
other => {
panic!("expected a folded literal, got {}", other.node_type_str())
}
});
}
#[test]
fn unary_minus_on_a_literal_folds() {
with_first_expression("-5;\n", |e| match e {
Node::NumericLiteral(n) => assert_eq!(n.value.get(), -5.0),
other => {
panic!("expected a folded literal, got {}", other.node_type_str())
}
});
}
#[test]
fn a_fold_rebuilds_the_root() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "1 + 2;\n");
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed");
assert!(
!std::ptr::eq(root, resolved),
"a fold must rebuild every ancestor, including the Program"
);
}
#[test]
fn a_long_binary_chain_is_folded_without_recursing() {
const LINKS: usize = 2000;
let src = (0..=LINKS)
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(" + ")
+ ";\n";
let expected = (LINKS * (LINKS + 1) / 2) as f64;
with_first_expression(&src, |e| match e {
Node::NumericLiteral(n) => assert_eq!(n.value.get(), expected),
other => panic!(
"a constant chain must fold whole, got {}",
other.node_type_str()
),
});
}
#[test]
fn a_long_assignment_chain_does_not_recurse() {
const LINKS: usize = 2000;
let src = "var a;\n".to_string() + &"a = ".repeat(LINKS) + "1;\n";
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, &src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
assert!(
resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[]).is_some(),
"a linearized `=` chain must not exhaust the recursion budget"
);
assert_eq!(sm.error_count(), 0);
}
fn sem_info_of(node: &Node) -> FunctionInfoId {
let id = match node {
Node::FunctionDeclaration(n) => n.sem_info.get(),
Node::FunctionExpression(n) => n.sem_info.get(),
_ => panic!("not a function-like node"),
};
FunctionInfoId::from_sema_id(id.expect("visitFunctionLike sets semInfo"))
}
fn first_statement<'gc>(root: &'gc Node<'gc>) -> &'gc Node<'gc> {
let Node::Program(p) = root else {
unreachable!("not a Program root")
};
p.body.iter().next().expect("empty program body")
}
#[test]
fn hoisted_function_backref_follows_a_rebuilt_node() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let src = "function f() {\n var x = 1 + 2;\n}\n";
let root = parse(&gc, &mut sm, src);
let original_id = first_statement(root).node_id();
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed");
let rebuilt = first_statement(resolved);
assert!(
matches!(rebuilt, Node::FunctionDeclaration(_)),
"the first statement must be the function declaration"
);
assert_ne!(
rebuilt.node_id(),
original_id,
"non-degeneracy: the fold must have REBUILT the FunctionDeclaration, \
otherwise this test proves nothing"
);
let global_scope = sem_ctx.get_global_scope();
let hoisted = &sem_ctx.scope(global_scope).hoisted_functions;
assert_eq!(hoisted.len(), 1, "one hoisted function declaration");
assert_eq!(
hoisted[0].node(&gc).node_id(),
rebuilt.node_id(),
"the hoistedFunctions entry is stale: it points at the \
pre-rebuild FunctionDeclaration"
);
}
#[test]
fn hoisted_function_backref_is_untouched_without_a_rebuild() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let src = "function f(a) {\n return a;\n}\n";
let root = parse(&gc, &mut sm, src);
let original_id = first_statement(root).node_id();
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed");
assert_eq!(first_statement(resolved).node_id(), original_id);
let global_scope = sem_ctx.get_global_scope();
let hoisted = &sem_ctx.scope(global_scope).hoisted_functions;
assert_eq!(hoisted.len(), 1);
assert_eq!(hoisted[0].node(&gc).node_id(), original_id);
}
#[test]
fn duplicate_loose_parameters_rebind_to_the_last_declaration() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let src = "function f(a, a) {\n return a;\n}\n";
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed");
assert_eq!(sm.error_count(), 0, "loose duplicate params are allowed");
let func_decl = first_statement(resolved);
let info = sem_info_of(func_decl);
let body_scope = sem_ctx.function(info).get_function_body_scope();
let decls = &sem_ctx.scope(body_scope).decls;
let params: Vec<_> = decls
.iter()
.copied()
.filter(|&d| sem_ctx.decl(d).kind == DeclKind::Parameter)
.collect();
assert_eq!(params.len(), 2, "each 'a' gets its own Decl");
assert_ne!(params[0], params[1]);
let Node::FunctionDeclaration(fd) = func_decl else {
unreachable!()
};
let Node::BlockStatement(block) = fd.body else {
unreachable!("function body is a BlockStatement")
};
let Some(Node::ReturnStatement(ret)) = block.body.iter().next() else {
unreachable!("body starts with a ReturnStatement")
};
let Some(Node::Identifier(ident)) = ret.argument else {
unreachable!("`return a;` returns an Identifier")
};
assert_eq!(
sem_ctx.get_expression_decl(ident),
Some(params[1]),
"the body reference must see the second parameter"
);
}
#[test]
fn duplicate_strict_parameters_are_an_error() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(
&gc,
&mut sm,
"\"use strict\";\nfunction f(a, a) {\n return a;\n}\n",
);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
assert!(
resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[]).is_none(),
"a duplicate strict parameter must fail resolution"
);
assert_eq!(sm.error_count(), 1);
}
#[test]
fn parameter_expressions_split_the_parameter_and_body_scopes() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let src = "function f(a, b = a) {\n var c;\n return c;\n}\n";
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed");
let info = sem_info_of(first_statement(resolved));
assert!(sem_ctx.function(info).has_parameter_expressions);
assert!(!sem_ctx.function(info).simple_parameter_list);
let scopes = sem_ctx.function(info).get_scopes().to_vec();
assert_eq!(scopes.len(), 3, "param scope, temp arguments scope, body");
let param_scope = sem_ctx.function(info).get_parameter_scope();
let body_scope = sem_ctx.function(info).get_function_body_scope();
assert_eq!(param_scope, scopes[0]);
assert_eq!(body_scope, scopes[2]);
assert_ne!(param_scope, body_scope);
assert!(sem_ctx.scope(scopes[1]).decls.is_empty());
let kinds = |s| {
sem_ctx
.scope(s)
.decls
.iter()
.map(|&d| sem_ctx.decl(d).kind)
.collect::<Vec<_>>()
};
assert_eq!(
kinds(param_scope),
vec![DeclKind::Parameter, DeclKind::Parameter, DeclKind::Var],
"both parameters and the implicit 'arguments' live in scopes[0]"
);
assert_eq!(kinds(body_scope), vec![DeclKind::Var], "just `var c`");
}
#[test]
fn simple_parameters_share_one_scope_with_the_body() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let src = "function f(a) {\n var c;\n return c;\n}\n";
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed");
let info = sem_info_of(first_statement(resolved));
assert!(!sem_ctx.function(info).has_parameter_expressions);
assert_eq!(sem_ctx.function(info).get_scopes().len(), 1);
assert_eq!(
sem_ctx.function(info).get_parameter_scope(),
sem_ctx.function(info).get_function_body_scope()
);
}
#[test]
fn function_expression_name_scope_belongs_to_the_enclosing_function() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let src = "var g = function me() {\n return me;\n};\n";
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed");
let global_fn = sem_ctx.get_global_function();
let scopes = sem_ctx.function(global_fn).get_scopes().to_vec();
assert_eq!(scopes.len(), 2);
let name_scope = scopes[1];
assert_eq!(sem_ctx.scope(name_scope).parent_function, global_fn);
let decls = &sem_ctx.scope(name_scope).decls;
assert_eq!(decls.len(), 1);
assert_eq!(sem_ctx.decl(decls[0]).kind, DeclKind::FunctionExprName);
let Node::VariableDeclaration(vd) = first_statement(resolved) else {
unreachable!()
};
let Some(Node::VariableDeclarator(decl)) = vd.declarations.iter().next()
else {
unreachable!()
};
let Some(Node::FunctionExpression(fe)) = decl.init else {
unreachable!("initializer is a FunctionExpression")
};
assert_eq!(fe.scope.get(), Some(name_scope.sema_id()));
}
#[test]
#[should_panic(expected = "$SHBuiltin.export needs visitModuleExport")]
fn a_panic_deep_inside_nested_scopes_unwinds_cleanly() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(
&gc,
&mut sm,
"function f() {\n {\n $SHBuiltin.export('x', 1);\n }\n}\n",
);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let _ = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[]);
}
fn label_index(node: &Node) -> u32 {
match node {
Node::WhileStatement(n) => n.label_index.get(),
Node::DoWhileStatement(n) => n.label_index.get(),
Node::ForInStatement(n) => n.label_index.get(),
Node::ForOfStatement(n) => n.label_index.get(),
Node::ForStatement(n) => n.label_index.get(),
Node::SwitchStatement(n) => n.label_index.get(),
Node::BreakStatement(n) => n.label_index.get(),
Node::ContinueStatement(n) => n.label_index.get(),
Node::LabeledStatement(n) => n.label_index.get(),
_ => panic!("no label decoration on {}", node.node_type_str()),
}
}
fn with_resolved<R>(
src: &str,
f: impl for<'gc> FnOnce(&SemContext, &'gc Node<'gc>) -> R,
) -> R {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.unwrap_or_else(|| panic!("resolution failed for: {src}"));
f(&sem_ctx, resolved)
}
#[test]
fn every_loop_kind_allocates_one_label_in_visit_order() {
let src = "while (a) ;\ndo ; while (a);\nfor (;;) ;\n\
for (x in a) ;\nfor (x of a) ;\n";
with_resolved(src, |sem_ctx, resolved| {
let Node::Program(p) = resolved else {
unreachable!("not a Program")
};
let kinds: Vec<(u32, &str)> = p
.body
.iter()
.map(|n| (label_index(n), n.node_type_str()))
.collect();
assert_eq!(
kinds,
vec![
(0, "WhileStatement"),
(1, "DoWhileStatement"),
(2, "ForStatement"),
(3, "ForInStatement"),
(4, "ForOfStatement"),
]
);
assert_eq!(
sem_ctx.function(sem_ctx.get_global_function()).num_labels,
5
);
});
}
#[test]
fn labeled_break_and_continue_target_the_labeled_loop() {
let src = "l1: while (a) {\n l2: for (;;) {\n break l1;\n\
\x20 continue l2;\n }\n}\n";
with_resolved(src, |sem_ctx, resolved| {
let outer_labeled = first_statement(resolved);
assert_eq!(label_index(outer_labeled), 0, "l1: itself");
let Node::LabeledStatement(l1) = outer_labeled else {
unreachable!("not a LabeledStatement")
};
assert_eq!(label_index(l1.body), 1, "the while");
let Node::WhileStatement(w) = l1.body else {
unreachable!("not a WhileStatement")
};
let Node::BlockStatement(outer_block) = w.body else {
unreachable!("not a BlockStatement")
};
let inner_labeled =
outer_block.body.iter().next().expect("empty while body");
assert_eq!(label_index(inner_labeled), 2, "l2: itself");
let Node::LabeledStatement(l2) = inner_labeled else {
unreachable!("not a LabeledStatement")
};
assert_eq!(label_index(l2.body), 3, "the for");
let Node::ForStatement(f) = l2.body else {
unreachable!("not a ForStatement")
};
let Node::BlockStatement(inner_block) = f.body else {
unreachable!("not a BlockStatement")
};
let mut it = inner_block.body.iter();
let brk = it.next().expect("no break");
let cont = it.next().expect("no continue");
assert_eq!(
label_index(brk),
1,
"`break l1` targets the WHILE (label 1), not the label (0)"
);
assert_eq!(label_index(cont), 3, "`continue l2` targets the for");
assert_eq!(
sem_ctx.function(sem_ctx.get_global_function()).num_labels,
4
);
});
}
#[test]
fn unlabeled_break_and_continue_use_their_own_innermost_target() {
let src = "while (a) {\n switch (b) {\n case 0:\n break;\n\
\x20 continue;\n }\n}\n";
with_resolved(src, |_sem_ctx, resolved| {
let while_stmt = first_statement(resolved);
assert_eq!(label_index(while_stmt), 0);
let Node::WhileStatement(w) = while_stmt else {
unreachable!("not a WhileStatement")
};
let Node::BlockStatement(block) = w.body else {
unreachable!("not a BlockStatement")
};
let switch_stmt = block.body.iter().next().expect("empty body");
assert_eq!(label_index(switch_stmt), 1);
let Node::SwitchStatement(sw) = switch_stmt else {
unreachable!("not a SwitchStatement")
};
let Some(Node::SwitchCase(case)) = sw.cases.iter().next() else {
unreachable!("no SwitchCase")
};
let mut it = case.consequent.iter();
let brk = it.next().expect("no break");
let cont = it.next().expect("no continue");
assert_eq!(label_index(brk), 1, "`break` targets the switch");
assert_eq!(label_index(cont), 0, "`continue` targets the while");
});
}
#[test]
fn a_rebuilt_switch_keeps_its_label_index_and_scope() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let src = "switch (1 + 2) {\ncase 0:\n break;\n}\n";
let root = parse(&gc, &mut sm, src);
let original_id = first_statement(root).node_id();
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed");
let switch_stmt = first_statement(resolved);
assert_ne!(
switch_stmt.node_id(),
original_id,
"non-degeneracy: the fold must have REBUILT the SwitchStatement, \
otherwise this test proves nothing"
);
let Node::SwitchStatement(sw) = switch_stmt else {
unreachable!("not a SwitchStatement")
};
assert!(
matches!(sw.discriminant, Node::NumericLiteral(_)),
"non-degeneracy: the discriminant must have folded"
);
assert_eq!(
sw.label_index.get(),
0,
"the REBUILT switch lost its label index"
);
assert!(
sw.scope.get().is_some(),
"the REBUILT switch lost its scope decoration"
);
let Some(Node::SwitchCase(case)) = sw.cases.iter().next() else {
unreachable!("no SwitchCase")
};
let brk = case.consequent.iter().next().expect("no break");
assert_eq!(
label_index(brk),
0,
"the `break` must target the switch's label"
);
}
#[test]
fn an_unrewritten_loop_is_returned_as_is() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let src = "for (var i = 0; i < 10; ++i) {\n break;\n}\n";
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed");
assert!(
std::ptr::eq(root, resolved),
"an unrewritten loop must be returned as-is, not rebuilt"
);
}
#[test]
fn a_label_is_erased_on_leaving_its_statement() {
let (sem_ctx, _) = resolve("l: ;\nl: ;\n");
assert_eq!(sem_ctx.function(sem_ctx.get_global_function()).num_labels, 2);
}
#[test]
fn a_label_enclosing_a_label_enclosing_a_loop_targets_the_loop() {
let src = "l1: l2: while (a) {\n continue l1;\n}\n";
with_resolved(src, |_sem_ctx, resolved| {
let l1_node = first_statement(resolved);
assert_eq!(label_index(l1_node), 0, "l1: itself");
let Node::LabeledStatement(l1) = l1_node else {
unreachable!("not a LabeledStatement")
};
assert_eq!(label_index(l1.body), 1, "l2: itself");
let Node::LabeledStatement(l2) = l1.body else {
unreachable!("not a LabeledStatement")
};
assert_eq!(label_index(l2.body), 2, "the while");
let Node::WhileStatement(w) = l2.body else {
unreachable!("not a WhileStatement")
};
let Node::BlockStatement(block) = w.body else {
unreachable!("not a BlockStatement")
};
let cont = block.body.iter().next().expect("empty body");
assert_eq!(
label_index(cont),
2,
"`continue l1` must reach the while (label 2) through l2"
);
});
}
#[test]
fn a_rebuilt_for_loop_keeps_its_label_index_and_scope() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let src = "for (var i = 1 + 2; ; ) {\n break;\n}\n";
let root = parse(&gc, &mut sm, src);
let original_id = first_statement(root).node_id();
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed");
let for_node = first_statement(resolved);
assert_ne!(
for_node.node_id(),
original_id,
"non-degeneracy: the fold must have REBUILT the ForStatement"
);
let Node::ForStatement(f) = for_node else {
unreachable!("not a ForStatement")
};
assert_eq!(f.label_index.get(), 0, "the REBUILT for lost its label");
assert!(f.scope.get().is_some(), "the REBUILT for lost its scope");
let Node::BlockStatement(block) = f.body else {
unreachable!("not a BlockStatement")
};
let brk = block.body.iter().next().expect("no break");
assert_eq!(label_index(brk), 0);
}
fn arrow_of_var<'gc>(stmt: &'gc Node<'gc>) -> &'gc Node<'gc> {
let Node::VariableDeclaration(vd) = stmt else {
panic!("not a VariableDeclaration: {}", stmt.node_type_str())
};
let Some(Node::VariableDeclarator(d)) = vd.declarations.iter().next() else {
panic!("no VariableDeclarator")
};
d.init.expect("declarator has no initializer")
}
#[test]
fn an_expression_bodied_arrow_is_rewritten_to_a_block_with_return() {
with_resolved("var f = (x) => x;\n", |_sem_ctx, resolved| {
let arrow_node = arrow_of_var(first_statement(resolved));
let Node::ArrowFunctionExpression(arrow) = arrow_node else {
panic!("not an arrow: {}", arrow_node.node_type_str())
};
assert!(
!arrow.expression.get(),
"the RETURNED arrow must carry expression = false"
);
let Node::BlockStatement(block) = arrow.body else {
panic!(
"body is not a BlockStatement: {}",
arrow.body.node_type_str()
)
};
assert!(block.implicit.get(), "the synthesized block is implicit");
let mut stmts = block.body.iter();
let Some(Node::ReturnStatement(ret)) = stmts.next() else {
panic!("the block's only statement is not a ReturnStatement")
};
assert!(stmts.next().is_none(), "the block has exactly one statement");
let arg = ret.argument.expect("the return has no argument");
assert!(
matches!(arg, Node::Identifier(_)),
"the returned expression is the original body"
);
assert_eq!(block.metadata.range.get(), arg.range());
assert_eq!(ret.metadata.range.get(), arg.range());
assert_eq!(
block.metadata.debug_loc.get(),
arg.metadata().debug_loc.get()
);
assert_eq!(
ret.metadata.debug_loc.get(),
arg.metadata().debug_loc.get()
);
});
}
#[test]
fn a_block_bodied_arrow_is_not_rewritten() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "var f = (x) => { return x; };\n");
let original_id = arrow_of_var(first_statement(root)).node_id();
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed");
let arrow_node = arrow_of_var(first_statement(resolved));
assert_eq!(
arrow_node.node_id(),
original_id,
"a block-bodied arrow must not be rebuilt"
);
let Node::ArrowFunctionExpression(arrow) = arrow_node else {
panic!("not an arrow")
};
assert!(!arrow.expression.get());
}
#[test]
fn a_rewritten_arrow_whose_body_folds_keeps_its_decorations() {
with_resolved("var f = () => 1 + 2;\n", |sem_ctx, resolved| {
let arrow_node = arrow_of_var(first_statement(resolved));
let Node::ArrowFunctionExpression(arrow) = arrow_node else {
panic!("not an arrow")
};
assert!(
!arrow.expression.get(),
"the twice-rebuilt arrow lost expression = false"
);
let sem_info = arrow
.sem_info
.get()
.expect("the twice-rebuilt arrow lost its sem_info");
assert!(sem_ctx.function(FunctionInfoId::from_sema_id(sem_info)).arrow);
let Node::BlockStatement(block) = arrow.body else {
panic!("body is not a BlockStatement")
};
let Some(Node::ReturnStatement(ret)) = block.body.iter().next() else {
panic!("no ReturnStatement")
};
assert!(
matches!(ret.argument, Some(Node::NumericLiteral(_))),
"1 + 2 did not fold, so the arrow was never rebuilt twice"
);
});
}
#[test]
fn an_arrow_using_arguments_propagates_to_the_enclosing_function() {
let src = "function f() { var g = () => arguments; }\n";
with_resolved(src, |sem_ctx, resolved| {
let f = sem_ctx.function(sem_info_of(first_statement(resolved)));
assert!(f.contains_arrow_functions);
assert!(
f.contains_arrow_functions_using_arguments,
"the arrow's usesArguments must reach f"
);
assert!(
!f.uses_arguments,
"f itself does not reference 'arguments' (the arrow does)"
);
let global = sem_ctx.function(sem_ctx.get_global_function());
assert!(!global.contains_arrow_functions);
assert!(!global.contains_arrow_functions_using_arguments);
});
}
#[test]
fn nested_arrows_propagate_arguments_use_outward() {
let src = "function f() { var g = () => { var h = () => arguments; }; }\n";
with_resolved(src, |sem_ctx, resolved| {
let f = sem_ctx.function(sem_info_of(first_statement(resolved)));
assert!(f.contains_arrow_functions);
assert!(f.contains_arrow_functions_using_arguments);
});
}
#[test]
fn an_arrow_not_using_arguments_leaves_the_propagation_flag_clear() {
let src = "function f() { arguments; var g = () => 1; }\n";
with_resolved(src, |sem_ctx, resolved| {
let f = sem_ctx.function(sem_info_of(first_statement(resolved)));
assert!(f.uses_arguments, "f references 'arguments' directly");
assert!(f.contains_arrow_functions);
assert!(
!f.contains_arrow_functions_using_arguments,
"f's own usesArguments must not leak into the arrow flag"
);
});
}
fn with_failed_resolution<R>(
src: &str,
errors: u32,
f: impl for<'gc> FnOnce(&GCLock, &SemContext, &'gc Node<'gc>) -> R,
) -> R {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
sm.set_handler(Box::new(SharedHandler(Rc::default())));
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
assert!(
resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[]).is_none(),
"expected resolution to fail for: {src}"
);
assert_eq!(sm.error_count(), errors, "unexpected error count for: {src}");
f(&gc, &sem_ctx, root)
}
fn identifier_states<'gc>(
gc: &GCLock,
node: &'gc Node<'gc>,
) -> Vec<(String, bool)> {
struct Collect<'a, 'b, 'c> {
gc: &'a GCLock<'b, 'c>,
out: Vec<(String, bool)>,
}
impl<'gc> hermes_ast::visitor::Visitor<'gc> for Collect<'_, '_, '_> {
fn visit_node(&mut self, node: &'gc Node<'gc>) {
if let Node::Identifier(id) = node {
let name = atom_string(self.gc, id.name.get());
self.out.push((name, id.unresolvable.get()));
}
node.visit_children(self);
}
}
let mut c = Collect { gc, out: Vec::new() };
hermes_ast::visitor::Visitor::visit_node(&mut c, node);
c.out
}
fn third_with_statement<'gc>(
root: &'gc Node<'gc>,
) -> &'gc hermes_ast::node::WithStatement<'gc> {
let Node::Program(p) = root else {
unreachable!("not a Program root")
};
p.body
.iter()
.nth(2)
.expect("no third statement")
.as_with_statement()
.expect("the third statement is not a WithStatement")
}
#[test]
fn with_statement_unresolves_identifiers_above_its_depth() {
let src = "var o = {a: 1};\nvar outer = 1;\n\
with (o) {\n let inner;\n outer;\n inner;\n o;\n}\n";
with_failed_resolution(src, 1, |gc, sem_ctx, root| {
let with = third_with_statement(root);
assert_eq!(
identifier_states(gc, with.object),
vec![("o".to_string(), false)]
);
assert_eq!(
identifier_states(gc, with.body),
vec![
("inner".to_string(), false),
("outer".to_string(), true),
("inner".to_string(), false),
("o".to_string(), true),
]
);
let mut out = Vec::new();
sem_dump(&mut out, gc, sem_ctx, with.body);
let dumped = String::from_utf8(out).expect("dump is not UTF-8");
assert!(dumped.contains("Id 'outer' UNR\n"), "{dumped}");
assert!(dumped.contains("Id 'o' UNR\n"), "{dumped}");
assert!(dumped.contains("Id 'inner' [D:E:"), "{dumped}");
});
}
#[test]
fn nested_with_statements_do_not_re_unresolve() {
let src = "var o = {};\nvar outer = 1;\n\
with (o) { with (o) { outer; } }\n";
with_failed_resolution(src, 2, |gc, _sem_ctx, root| {
let with = third_with_statement(root);
assert_eq!(
identifier_states(gc, with.body),
vec![("o".to_string(), true), ("outer".to_string(), true)]
);
});
}
#[test]
fn try_with_catch_and_finally_is_rewritten_into_nested_trys() {
let src = "try { 1; } catch (e) { 2; } finally { 3; }\n";
with_resolved(src, |_sem_ctx, resolved| {
let outer = first_statement(resolved)
.as_try_statement()
.expect("not a TryStatement");
assert!(
outer.handler.is_none(),
"the outer statement must have given its handler away"
);
assert!(outer.finalizer.is_some());
let wrapper = outer
.block
.as_block_statement()
.expect("the new block is not a BlockStatement");
assert!(!wrapper.implicit.get(), "cpp:803 passes `false`");
let mut wrapper_body = wrapper.body.iter();
let nested = wrapper_body
.next()
.expect("the wrapper block is empty")
.as_try_statement()
.expect("the wrapper's only statement is not a TryStatement");
assert!(
wrapper_body.next().is_none(),
"the wrapper block holds exactly one statement"
);
assert!(nested.handler.is_some());
assert!(
nested.finalizer.is_none(),
"the nested try must have no finalizer, or it would rewrite again"
);
let whole = first_statement(resolved).range();
let handler_range = nested.handler.unwrap().range();
let nested_range = nested.metadata.range.get();
assert_eq!(nested_range.start, whole.start);
assert_eq!(nested_range.end, handler_range.end);
assert_ne!(
nested_range.end, whole.end,
"a `finally` follows the handler, so the ranges must differ"
);
assert_eq!(wrapper.metadata.range.get(), nested_range);
});
}
#[test]
fn try_without_both_handler_and_finalizer_is_not_rewritten() {
let sources =
["try { 1; } catch (e) { 2; }\n", "try { 1; } finally { 2; }\n"];
for src in sources {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.unwrap_or_else(|| panic!("resolution failed for: {src}"));
assert!(
std::ptr::eq(resolved, root),
"{src} must not rebuild anything"
);
}
}
#[test]
fn the_try_rewrite_is_reported_even_when_no_child_changes() {
let src = "try { } catch (e) { } finally { }\n";
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution failed");
assert!(
!std::ptr::eq(resolved, root),
"the rewrite must have rebuilt the Program"
);
let outer = first_statement(resolved)
.as_try_statement()
.expect("not a TryStatement");
assert!(outer.handler.is_none());
}
#[test]
fn a_rebuilt_catch_clause_keeps_its_scope() {
let src = "try { } catch (e) { 1 + 2; }\n";
with_resolved(src, |sem_ctx, resolved| {
let outer = first_statement(resolved)
.as_try_statement()
.expect("not a TryStatement");
let catch = outer
.handler
.expect("no handler")
.as_catch_clause()
.expect("handler is not a CatchClause");
let scope = catch.scope.get().expect("the rebuilt clause lost `scope`");
let scope_id = hermes_sema::ids::ScopeId::from_sema_id(scope);
let decls = &sem_ctx.scope(scope_id).decls;
assert_eq!(decls.len(), 1, "the catch param must be the only decl");
assert_eq!(sem_ctx.decl(decls[0]).kind, DeclKind::ES5Catch);
let body = catch
.body
.as_block_statement()
.expect("catch body is not a block");
let Some(Node::ExpressionStatement(es)) = body.body.iter().next() else {
panic!("no ExpressionStatement in the catch body")
};
assert!(
matches!(es.expression, Node::NumericLiteral(_)),
"1 + 2 did not fold, so the CatchClause was never rebuilt"
);
});
}
fn class_function_infos(
node: &Node,
) -> (Option<hermes_ast::SemaId>, Option<hermes_ast::SemaId>, Option<hermes_ast::SemaId>) {
match node {
Node::ClassDeclaration(n) => (
n.implicit_ctor_function_info.get(),
n.instance_elements_init_function_info.get(),
n.static_elements_init_function_info.get(),
),
Node::ClassExpression(n) => (
n.implicit_ctor_function_info.get(),
n.instance_elements_init_function_info.get(),
n.static_elements_init_function_info.get(),
),
_ => panic!("no class decoration on {}", node.node_type_str()),
}
}
#[test]
fn an_implicit_constructor_function_info_is_created_for_a_class_without_one() {
with_resolved("class A {}\n", |sem_ctx, resolved| {
let class = first_statement(resolved);
let (ctor, inst, stat) = class_function_infos(class);
assert!(inst.is_none() && stat.is_none());
let ctor = FunctionInfoId::from_sema_id(
ctor.expect("no implicit constructor FunctionInfo"),
);
let info = sem_ctx.function(ctor);
assert_eq!(
info.constructor_kind,
hermes_sema::sem_context::ConstructorKind::Base
);
assert!(info.strict, "an implicit constructor is always strict");
assert_eq!(info.get_scopes().len(), 1);
assert_eq!(info.get_function_body_scope(), info.get_scopes()[0]);
});
with_resolved("class A {}\nclass B extends A {}\n", |sem_ctx, resolved| {
let Node::Program(p) = resolved else {
unreachable!("not a Program")
};
let b = p.body.iter().nth(1).expect("no class B");
let ctor = FunctionInfoId::from_sema_id(
class_function_infos(b)
.0
.expect("B has no implicit constructor FunctionInfo"),
);
assert_eq!(
sem_ctx.function(ctor).constructor_kind,
hermes_sema::sem_context::ConstructorKind::Derived
);
});
}
#[test]
fn an_explicit_constructor_suppresses_the_implicit_one() {
let src = "class A {}\nclass B extends A { constructor() {} }\n";
with_resolved(src, |sem_ctx, resolved| {
let Node::Program(p) = resolved else {
unreachable!("not a Program")
};
let mut it = p.body.iter();
let a = it.next().expect("no class A");
let b = it.next().expect("no class B");
assert!(
class_function_infos(a).0.is_some(),
"A has no explicit constructor"
);
assert!(
class_function_infos(b).0.is_none(),
"B's explicit constructor must suppress the implicit one"
);
let body = b
.as_class_declaration()
.expect("not a ClassDeclaration")
.body
.as_class_body()
.expect("not a ClassBody");
let method = body
.body
.iter()
.next()
.expect("empty class body")
.as_method_definition()
.expect("not a MethodDefinition");
let func = method
.value
.as_function_expression()
.expect("method value is not a FunctionExpression");
let id = FunctionInfoId::from_sema_id(
func.sem_info.get().expect("no semInfo on the constructor"),
);
assert_eq!(
sem_ctx.function(id).constructor_kind,
hermes_sema::sem_context::ConstructorKind::Derived
);
});
}
#[test]
fn a_rebuilt_class_keeps_its_synthetic_function_infos() {
let src = "class C { x = 1 + 2; static y; }\n";
with_resolved(src, |sem_ctx, resolved| {
let class = first_statement(resolved);
let (ctor, inst, stat) = class_function_infos(class);
assert!(ctor.is_some(), "the rebuilt class lost implicitCtor");
let inst = FunctionInfoId::from_sema_id(
inst.expect("the rebuilt class lost instanceElementsInit"),
);
let stat = FunctionInfoId::from_sema_id(
stat.expect("the rebuilt class lost staticElementsInit"),
);
assert_ne!(inst, stat);
assert!(
class
.as_class_declaration()
.expect("not a ClassDeclaration")
.scope
.get()
.is_some(),
"the rebuilt class lost `scope`"
);
assert!(sem_ctx.function(inst).arguments_decl.is_some());
assert!(sem_ctx.function(stat).arguments_decl.is_none());
let body = class
.as_class_declaration()
.expect("not a ClassDeclaration")
.body
.as_class_body()
.expect("not a ClassBody");
let prop = body
.body
.iter()
.next()
.expect("empty class body")
.as_class_property()
.expect("not a ClassProperty");
assert!(
matches!(prop.value, Some(Node::NumericLiteral(_))),
"1 + 2 did not fold, so the class was never rebuilt"
);
});
}
#[test]
fn a_class_declaration_name_carries_both_a_class_and_a_class_expr_name_decl() {
with_resolved("class C {}\n", |sem_ctx, resolved| {
let class = first_statement(resolved)
.as_class_declaration()
.expect("not a ClassDeclaration");
let id = class
.id
.expect("no class id")
.as_identifier()
.expect("class id is not an Identifier");
let decl =
sem_ctx.get_declaration_decl(id).expect("no declaration decl");
let expr = sem_ctx.get_expression_decl(id).expect("no expression decl");
assert_ne!(decl, expr);
assert_eq!(sem_ctx.decl(decl).kind, DeclKind::Class);
assert_eq!(sem_ctx.decl(expr).kind, DeclKind::ClassExprName);
let scope = hermes_sema::ids::ScopeId::from_sema_id(
class.scope.get().expect("no scope on the class"),
);
assert_eq!(sem_ctx.decl(expr).scope, Some(scope));
});
}
#[test]
fn a_class_forces_strict_mode_only_inside_itself() {
with_resolved("class C { m() {} }\n", |sem_ctx, resolved| {
assert!(
!sem_ctx.function(sem_ctx.get_global_function()).strict,
"the global function must be loose again after the class"
);
let Node::Program(p) = resolved else {
unreachable!("not a Program")
};
assert_eq!(p.strictness.get(), Strictness::NonStrictMode);
let class = first_statement(resolved)
.as_class_declaration()
.expect("not a ClassDeclaration");
let method = class
.body
.as_class_body()
.expect("not a ClassBody")
.body
.iter()
.next()
.expect("empty class body")
.as_method_definition()
.expect("not a MethodDefinition");
let func = method
.value
.as_function_expression()
.expect("method value is not a FunctionExpression");
let id = FunctionInfoId::from_sema_id(
func.sem_info.get().expect("no semInfo on the method"),
);
assert!(sem_ctx.function(id).strict, "a method must be strict");
assert_eq!(func.strictness.get(), Strictness::StrictMode);
});
}
#[test]
fn a_nested_class_gets_its_own_class_context() {
let src = "class Outer {\n m() {\n class Inner extends Outer {\n\
\x20 constructor() {}\n }\n return Inner;\n }\n}\n";
with_resolved(src, |sem_ctx, resolved| {
let outer = first_statement(resolved);
assert!(
class_function_infos(outer).0.is_some(),
"Outer has no explicit constructor of its own"
);
let method = outer
.as_class_declaration()
.expect("not a ClassDeclaration")
.body
.as_class_body()
.expect("not a ClassBody")
.body
.iter()
.next()
.expect("empty class body")
.as_method_definition()
.expect("not a MethodDefinition");
let block = method
.value
.as_function_expression()
.expect("method value is not a FunctionExpression")
.body
.as_block_statement()
.expect("method body is not a block");
let inner = block.body.iter().next().expect("empty method body");
assert!(
class_function_infos(inner).0.is_none(),
"Inner's explicit constructor must suppress ITS implicit one"
);
let ctor = inner
.as_class_declaration()
.expect("Inner is not a ClassDeclaration")
.body
.as_class_body()
.expect("not a ClassBody")
.body
.iter()
.next()
.expect("empty inner class body")
.as_method_definition()
.expect("not a MethodDefinition");
let id = FunctionInfoId::from_sema_id(
ctor.value
.as_function_expression()
.expect("not a FunctionExpression")
.sem_info
.get()
.expect("no semInfo"),
);
assert_eq!(
sem_ctx.function(id).constructor_kind,
hermes_sema::sem_context::ConstructorKind::Derived
);
});
}
#[test]
fn a_private_field_decl_is_not_the_same_as_a_same_named_variable() {
let src = "class C { #x; m() { var x; return this.#x; } }\n";
with_resolved(src, |sem_ctx, resolved| {
let class = first_statement(resolved);
let class_decl =
class.as_class_declaration().expect("not a ClassDeclaration");
let scope = hermes_sema::ids::ScopeId::from_sema_id(
class_decl.scope.get().expect("no scope on the class"),
);
let body = class_decl.body.as_class_body().expect("not a ClassBody");
let mut elms = body.body.iter();
let key = elms
.next()
.expect("empty class body")
.as_class_private_property()
.expect("not a ClassPrivateProperty")
.key
.as_identifier()
.expect("a ClassPrivateProperty key is an Identifier");
let private_decl =
sem_ctx.get_expression_decl(key).expect("unresolved private name");
assert_eq!(sem_ctx.get_declaration_decl(key), Some(private_decl));
assert_eq!(sem_ctx.decl(private_decl).kind, DeclKind::PrivateField);
assert_eq!(
sem_ctx.decl(private_decl).special,
hermes_sema::sem_context::DeclSpecial::NotSpecial,
"a FIELD never gets PrivateStatic (cpp:2212 passes isStatic=false)"
);
assert_eq!(sem_ctx.decl(private_decl).scope, Some(scope));
assert_eq!(sem_ctx.scope(scope).decls.len(), 2);
assert_eq!(sem_ctx.scope(scope).decls[1], private_decl);
assert_ne!(sem_ctx.decl(private_decl).name, key.name.get());
});
}
#[test]
fn a_private_getter_setter_pair_shares_one_upgraded_decl() {
let src = "class C { static get #x() {} static set #x(v) {} }\n";
with_resolved(src, |sem_ctx, resolved| {
let class = first_statement(resolved);
let body = class
.as_class_declaration()
.expect("not a ClassDeclaration")
.body
.as_class_body()
.expect("not a ClassBody");
let decls: Vec<hermes_sema::ids::DeclId> = body
.body
.iter()
.map(|elm| {
let key = elm
.as_method_definition()
.expect("not a MethodDefinition")
.key
.as_private_name()
.expect("key is not a PrivateName")
.id
.as_identifier()
.expect("a PrivateName's id is an Identifier");
let decl =
sem_ctx.get_expression_decl(key).expect("unresolved");
assert_eq!(
sem_ctx.get_declaration_decl(key),
Some(decl),
"setBothDecl must set both to the same decl"
);
decl
})
.collect();
assert_eq!(decls.len(), 2);
assert_eq!(decls[0], decls[1], "the pair must share one decl");
assert_eq!(
sem_ctx.decl(decls[0]).kind,
DeclKind::PrivateGetterSetter,
"the second accessor must upgrade the first's decl in place"
);
assert_eq!(
sem_ctx.decl(decls[0]).special,
hermes_sema::sem_context::DeclSpecial::PrivateStatic
);
});
}
#[test]
fn a_static_block_hoists_its_vars_into_its_own_function_scope() {
let src = "function f() { class C { static { var x; } } }\n";
with_resolved(src, |sem_ctx, resolved| {
let func = first_statement(resolved);
let outer = FunctionInfoId::from_sema_id(
func.as_function_declaration()
.expect("not a FunctionDeclaration")
.sem_info
.get()
.expect("no semInfo"),
);
let block = static_block_of_class_in_function_body(func);
let info = FunctionInfoId::from_sema_id(
block.function_info.get().expect("no functionInfo on the block"),
);
assert!(sem_ctx.function(info).is_static_block);
assert_ne!(info, outer);
let scope = hermes_sema::ids::ScopeId::from_sema_id(
block.scope.get().expect("no scope on the block"),
);
assert_eq!(sem_ctx.function(info).get_function_body_scope(), scope);
let x_decl = *sem_ctx
.scope(scope)
.decls
.first()
.expect("nothing hoisted into the static block");
assert_eq!(sem_ctx.scope(scope).decls.len(), 1);
assert_eq!(sem_ctx.decl(x_decl).kind, DeclKind::Var);
for s in sem_ctx.function(outer).get_scopes() {
assert!(
!sem_ctx.scope(*s).decls.contains(&x_decl),
"a static block's `var` must not hoist to the function"
);
}
});
}
#[test]
fn a_rebuilt_static_block_keeps_its_scope_and_function_info() {
let src = "function f() { class C { static { var x = 1 + 2; } } }\n";
with_resolved(src, |sem_ctx, resolved| {
let func = first_statement(resolved);
let block = static_block_of_class_in_function_body(func);
assert!(
block.scope.get().is_some(),
"the rebuilt static block lost `scope`"
);
let info = FunctionInfoId::from_sema_id(
block
.function_info
.get()
.expect("the rebuilt static block lost `function_info`"),
);
assert!(sem_ctx.function(info).is_static_block);
let decl = block
.body
.iter()
.next()
.expect("empty static block")
.as_variable_declaration()
.expect("not a VariableDeclaration")
.declarations
.iter()
.next()
.expect("no declarators")
.as_variable_declarator()
.expect("not a VariableDeclarator");
assert!(
matches!(decl.init, Some(Node::NumericLiteral(_))),
"1 + 2 did not fold, so the static block was never rebuilt"
);
});
}
fn static_block_of_class_in_function_body<'gc>(
func: &'gc Node<'gc>,
) -> &'gc hermes_ast::node::StaticBlock<'gc> {
let class = func
.as_function_declaration()
.expect("not a FunctionDeclaration")
.body
.as_block_statement()
.expect("function body is not a block")
.body
.iter()
.next()
.expect("empty function body");
class
.as_class_declaration()
.expect("not a ClassDeclaration")
.body
.as_class_body()
.expect("not a ClassBody")
.body
.iter()
.next()
.expect("empty class body")
.as_static_block()
.expect("not a StaticBlock")
}
#[test]
fn a_block_nested_function_is_promoted_to_global_scope() {
with_resolved("{\n function f() {}\n}\n", |sem_ctx, resolved| {
let block = first_statement(resolved)
.as_block_statement()
.expect("not a BlockStatement");
let id_node = block
.body
.iter()
.next()
.expect("empty block")
.as_function_declaration()
.expect("not a FunctionDeclaration")
.id
.expect("no function id");
let id = id_node.as_identifier().expect("id is not an Identifier");
let declared =
sem_ctx.get_declaration_decl(id).expect("no declaration decl");
assert_eq!(sem_ctx.decl(declared).kind, DeclKind::GlobalProperty);
assert_eq!(
sem_ctx.decl(declared).scope,
Some(sem_ctx.get_global_scope())
);
assert_eq!(sem_ctx.get_expression_decl(id), Some(declared));
let promoted = sem_ctx
.get_promoted_decl(id_node.node_id())
.expect("no promoted decl recorded");
assert_ne!(promoted, declared);
assert_eq!(sem_ctx.decl(promoted).kind, DeclKind::ScopedFunction);
let block_scope = hermes_sema::ids::ScopeId::from_sema_id(
block.scope.get().expect("no scope on the block"),
);
assert_eq!(sem_ctx.decl(promoted).scope, Some(block_scope));
});
}
#[test]
fn a_block_nested_function_inside_a_function_is_promoted_as_var() {
let src = "function outer() {\n {\n function g() {}\n }\n}\n";
with_resolved(src, |sem_ctx, resolved| {
let outer = first_statement(resolved)
.as_function_declaration()
.expect("not a FunctionDeclaration");
let block = outer
.body
.as_block_statement()
.expect("function body is not a block")
.body
.iter()
.next()
.expect("empty function body")
.as_block_statement()
.expect("not a BlockStatement");
let id_node = block
.body
.iter()
.next()
.expect("empty block")
.as_function_declaration()
.expect("not a FunctionDeclaration")
.id
.expect("no function id");
let id = id_node.as_identifier().expect("id is not an Identifier");
let declared =
sem_ctx.get_declaration_decl(id).expect("no declaration decl");
assert_eq!(sem_ctx.decl(declared).kind, DeclKind::Var);
let outer_info = FunctionInfoId::from_sema_id(
outer.sem_info.get().expect("no sem_info on `outer`"),
);
assert_eq!(
sem_ctx.decl(declared).scope,
Some(sem_ctx.function(outer_info).get_function_body_scope())
);
let promoted = sem_ctx
.get_promoted_decl(id_node.node_id())
.expect("no promoted decl recorded");
assert_eq!(sem_ctx.decl(promoted).kind, DeclKind::ScopedFunction);
});
}
#[test]
fn a_visible_let_blocks_promotion() {
with_resolved("let f;\n{\n function f() {}\n}\n", |sem_ctx, resolved| {
let Node::Program(p) = resolved else {
unreachable!("not a Program")
};
let block = p
.body
.iter()
.nth(1)
.expect("program has no second statement")
.as_block_statement()
.expect("not a BlockStatement");
let id_node = block
.body
.iter()
.next()
.expect("empty block")
.as_function_declaration()
.expect("not a FunctionDeclaration")
.id
.expect("no function id");
let id = id_node.as_identifier().expect("id is not an Identifier");
let declared =
sem_ctx.get_declaration_decl(id).expect("no declaration decl");
assert_eq!(sem_ctx.decl(declared).kind, DeclKind::ScopedFunction);
let block_scope = hermes_sema::ids::ScopeId::from_sema_id(
block.scope.get().expect("no scope on the block"),
);
assert_eq!(sem_ctx.decl(declared).scope, Some(block_scope));
assert_eq!(sem_ctx.get_expression_decl(id), Some(declared));
assert_eq!(sem_ctx.get_promoted_decl(id_node.node_id()), None);
let let_decl = *sem_ctx
.scope(sem_ctx.get_global_scope())
.decls
.first()
.expect("global scope has no decls");
assert_eq!(sem_ctx.decl(let_decl).kind, DeclKind::Let);
});
}
fn resolve_always<'gc>(
gc: &'gc GCLock,
sem_ctx: &mut SemContext,
sm: &mut SourceErrorManager,
root: &'gc Node<'gc>,
compile: bool,
) -> &'gc Node<'gc> {
let binding_table = sem_ctx.binding_table_rc();
let mut resolver = hermes_sema::resolver::SemanticResolver::new(
&binding_table,
sem_ctx,
sm,
&[],
compile,
);
resolver.run_always(gc, root)
}
fn global_function(sem_ctx: &SemContext) -> FunctionInfoId {
sem_ctx.scope(sem_ctx.get_global_scope()).parent_function
}
#[test]
fn import_declarations_are_recorded_on_the_function_info() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let src = "import d, {a as b} from 'm';\nimport * as ns from 'n';\n\
var x = 1 + 2;\n";
let root = parse(&gc, &mut sm, src);
let original_root_id = root.node_id();
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved =
resolve_always(&gc, &mut sem_ctx, &mut sm, root, true);
assert_eq!(sm.error_count(), 2, "one 'import' error per declaration");
assert_ne!(
resolved.node_id(),
original_root_id,
"the `1 + 2` fold must have rebuilt the Program"
);
let Node::Program(p) = resolved else {
unreachable!("not a Program")
};
let in_tree: Vec<&Node> = p
.body
.iter()
.filter(|n| matches!(n, Node::ImportDeclaration(_)))
.collect();
assert_eq!(in_tree.len(), 2, "two ImportDeclarations in the tree");
let imports = &sem_ctx.function(global_function(&sem_ctx)).imports;
assert_eq!(imports.len(), 2, "one entry per ImportDeclaration");
assert_eq!(
imports[0].node(&gc).node_id(),
in_tree[0].node_id(),
"imports[0] is not the first ImportDeclaration in the tree"
);
assert_eq!(
imports[1].node(&gc).node_id(),
in_tree[1].node_id(),
"imports[1] is not the second ImportDeclaration in the tree"
);
let kinds: Vec<DeclKind> = sem_ctx
.scope(sem_ctx.get_global_scope())
.decls
.iter()
.map(|&d| sem_ctx.decl(d).kind)
.collect();
assert_eq!(
kinds.iter().filter(|&&k| k == DeclKind::Import).count(),
3,
"`d`, `b` and `ns` are Import decls"
);
}
#[test]
fn import_backref_is_untouched_without_a_rebuild() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "import {a} from 'm';\n");
let original_id = first_statement(root).node_id();
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved =
resolve_always(&gc, &mut sem_ctx, &mut sm, root, true);
assert_eq!(first_statement(resolved).node_id(), original_id);
let imports = &sem_ctx.function(global_function(&sem_ctx)).imports;
assert_eq!(imports.len(), 1);
assert_eq!(imports[0].node(&gc).node_id(), original_id);
}
#[test]
fn export_default_anonymous_function_is_rewritten_to_an_expression() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let src = "export default async function (a) { return a; }\n";
let root = parse(&gc, &mut sm, src);
let decl_before = root
.as_program()
.expect("not a Program")
.body
.iter()
.next()
.expect("empty program")
.as_export_default_declaration()
.expect("not an ExportDefaultDeclaration")
.declaration;
let decl_range_before = decl_before.range();
assert!(
decl_before
.as_function_declaration()
.expect("parsed as a FunctionDeclaration")
.r#async
.get(),
"non-degeneracy: the source really is `async`"
);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved =
resolve_always(&gc, &mut sem_ctx, &mut sm, root, true);
assert_eq!(sm.error_count(), 1, "the 'export' module-mode error");
let export = first_statement(resolved)
.as_export_default_declaration()
.expect("the rewrite must keep an ExportDefaultDeclaration");
let func = export
.declaration
.as_function_expression()
.expect("rewrite #4 must replace the FunctionDeclaration");
assert!(func.id.is_none(), "the anonymous `_id` is carried over");
assert_eq!(func.params.iter().count(), 1, "`_params` carried over");
assert!(
matches!(func.body, Node::BlockStatement(_)),
"`_body` carried over"
);
assert!(func.type_parameters.is_none());
assert!(func.return_type.is_none());
assert!(func.predicate.is_none());
assert!(!func.generator.get(), "`_generator` carried over");
assert!(
func.r#async.get(),
"rewrite #4 must carry `_async` over (cpp:1538) — an anonymous \
`export default async function` stays async"
);
let range = func.metadata.range.get();
assert_eq!(range.start, decl_range_before.start);
assert_eq!(range.end, decl_range_before.end);
assert!(func.sem_info.get().is_some(), "the rewritten node was visited");
assert_ne!(func.strictness.get(), Strictness::NotSet);
}
#[test]
fn export_default_anonymous_non_async_function_stays_non_async() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let src = "export default function (a) { return a; }\n";
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved =
resolve_always(&gc, &mut sem_ctx, &mut sm, root, true);
let func = first_statement(resolved)
.as_export_default_declaration()
.expect("the rewrite must keep an ExportDefaultDeclaration")
.declaration
.as_function_expression()
.expect("rewrite #4 must replace the FunctionDeclaration");
assert!(!func.r#async.get(), "`_async` was false and stays false");
}
#[test]
fn export_default_named_function_is_not_rewritten() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "export default function f() {}\n");
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved =
resolve_always(&gc, &mut sem_ctx, &mut sm, root, true);
assert!(
matches!(
first_statement(resolved)
.as_export_default_declaration()
.expect("not an ExportDefaultDeclaration")
.declaration,
Node::FunctionDeclaration(_)
),
"a NAMED default export keeps its FunctionDeclaration"
);
}
#[test]
fn export_default_non_function_is_not_rewritten() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "export default 1 + 2;\n");
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved =
resolve_always(&gc, &mut sem_ctx, &mut sm, root, true);
assert!(
matches!(
first_statement(resolved)
.as_export_default_declaration()
.expect("not an ExportDefaultDeclaration")
.declaration,
Node::NumericLiteral(_)
),
"a non-function default export is untouched by rewrite #4"
);
}
#[test]
fn compile_false_skips_the_export_error_and_the_rewrite() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "export default function () {}\n");
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved =
resolve_always(&gc, &mut sem_ctx, &mut sm, root, false);
assert_eq!(sm.error_count(), 0, "no 'export' error at compile = false");
assert!(
matches!(
first_statement(resolved)
.as_export_default_declaration()
.expect("not an ExportDefaultDeclaration")
.declaration,
Node::FunctionDeclaration(_)
),
"rewrite #4 is compile_-gated and must not have fired"
);
}
#[test]
fn compile_false_still_errors_on_imports() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "import {a} from 'm';\n");
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
resolve_always(&gc, &mut sem_ctx, &mut sm, root, false);
assert_eq!(
sm.error_count(),
1,
"the import error is NOT compile_-gated (cpp:876-879)"
);
}
#[test]
fn import_attributes_add_a_second_error_only_when_present() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let log: Rc<RefCell<Vec<String>>> = Rc::default();
sm.set_handler(Box::new(SharedHandler(Rc::clone(&log))));
let src = "import 'a.js';\nimport 'b.js' with {type: 'json'};\n";
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
resolve_always(&gc, &mut sem_ctx, &mut sm, root, true);
assert_eq!(
*log.borrow(),
vec![
"'import' statement requires module mode".to_string(),
"'import' statement requires module mode".to_string(),
"import assertions are not supported".to_string(),
]
);
}
#[test]
fn shbuiltin_private_name_is_rejected_not_asserted() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let log: Rc<RefCell<Vec<String>>> = Rc::default();
sm.set_handler(Box::new(SharedHandler(Rc::clone(&log))));
let src = "class C {\n #x;\n m() {\n $SHBuiltin.#x();\n }\n}\n";
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
assert!(
resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[]).is_none(),
"resolution must fail, not panic"
);
assert_eq!(
*log.borrow(),
vec!["invalid use of $SHBuiltin".to_string()],
"exactly one ordinary invalid-use error"
);
}
#[test]
fn shbuiltin_identifier_property_still_rewrites() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "$SHBuiltin.foo(1);\n");
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[])
.expect("resolution must succeed");
assert_eq!(sm.error_count(), 0, "no invalid-use error: it was rewritten");
let callee = first_statement(resolved)
.as_expression_statement()
.expect("not an ExpressionStatement")
.expression
.as_call_expression()
.expect("not a CallExpression")
.callee;
assert!(
matches!(
callee
.as_member_expression()
.expect("not a MemberExpression")
.object,
Node::SHBuiltin(_)
),
"rewrite #3 must still fire for an identifier property"
);
}
#[test]
fn field_initializer_scopes_are_parented_in_the_initializer_function() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let src = "class C {\n x = class {};\n static y = class {};\n}\n";
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
assert!(resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[]).is_some());
let global = global_function(&sem_ctx);
let global_scopes = sem_ctx.function(global).get_scopes().to_vec();
assert_eq!(
global_scopes.len(),
2,
"the global program scope + class C's own (private-names) scope"
);
let class_c_scope = global_scopes[1];
let multi: Vec<FunctionInfoId> = (0..sem_ctx.functions_len())
.map(|i| FunctionInfoId::from_sema_id(hermes_ast::SemaId(i as u32)))
.filter(|&f| f != global && sem_ctx.function(f).get_scopes().len() > 1)
.collect();
assert_eq!(
multi.len(),
2,
"the instance and static elements-init functions"
);
for f in multi {
let info = sem_ctx.function(f);
let body = info.get_function_body_scope();
let scopes = info.get_scopes().to_vec();
assert_eq!(scopes.len(), 2, "body scope + the class expression's");
assert_eq!(scopes[0], body, "scopes[0] is the body scope");
assert_eq!(
sem_ctx.scope(scopes[1]).parent_scope,
Some(body),
"the class expression's scope must hang off the initializer \
function's body scope, not the enclosing class scope"
);
assert_eq!(
sem_ctx.scope(body).parent_scope,
Some(class_c_scope),
"the elements-init function's body scope must hang off class \
C's own scope, not the global scope"
);
}
}
fn parser_entry_dump(src: &str) -> String {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let resolved = resolve_ast_for_parser(&gc, &mut sem_ctx, &mut sm, root);
let mut out = Vec::new();
sem_dump(&mut out, &gc, &sem_ctx, resolved);
String::from_utf8(out).expect("dump is not UTF-8")
}
#[test]
fn parser_entry_dumps_with_body_identifiers_as_unr() {
let dumped = parser_entry_dump("with (o) { x; }\n");
assert!(dumped.contains("Id 'x' UNR\n"), "{dumped}");
assert!(dumped.contains("Id 'o' [D:E:"), "{dumped}");
}
#[test]
fn parser_entry_dumps_anonymous_export_default_as_default() {
let dumped = parser_entry_dump("export default function () {}\n");
assert!(dumped.contains("hoistedFunction *default*\n"), "{dumped}");
}
#[test]
fn parser_entry_dumps_named_export_default_by_name() {
let dumped = parser_entry_dump("export default function f() {}\n");
assert!(dumped.contains("hoistedFunction f\n"), "{dumped}");
}