use hermes_ast::context::{GCLock, NodeRc};
use hermes_ast::node::{builder, Node, NodeField};
use hermes_ast::visitor::TransformResult;
use hermes_support::diag::Subsystem;
use hermes_support::manager::SourceErrorManager;
use crate::ids::{DeclId, ScopeId};
use crate::sem_context::{Atom, Binding, DeclKind};
use super::SemanticResolver;
const TYPED: bool = false;
pub(super) fn atom_str(gc: &GCLock, atom: Atom) -> String {
String::from_utf8_lossy(gc.bytes(atom)).into_owned()
}
pub(super) fn extract_declared_idents_from_id<'gc>(
sm: &mut SourceErrorManager,
node: Option<&'gc Node<'gc>>,
idents: &mut Vec<&'gc Node<'gc>>,
) -> bool {
let node = match node {
Some(n) => n,
None => return false,
};
if let Node::Identifier(_) = node {
idents.push(node);
return false;
}
if let Node::Empty(_) = node {
return false;
}
if let Node::AssignmentPattern(ap) = node {
extract_declared_idents_from_id(sm, Some(ap.left), idents);
return true;
}
if let Node::ArrayPattern(arr) = node {
let mut contains_expr = false;
for elem in arr.elements.iter() {
contains_expr |=
extract_declared_idents_from_id(sm, Some(elem), idents);
}
return contains_expr;
}
if let Node::RestElement(re) = node {
return extract_declared_idents_from_id(sm, Some(re.argument), idents);
}
if let Node::ObjectPattern(obj) = node {
let mut contains_expr = false;
for prop_node in obj.properties.iter() {
match prop_node {
Node::Property(p) => {
contains_expr |= extract_declared_idents_from_id(
sm,
Some(p.value),
idents,
);
}
Node::RestElement(re) => {
contains_expr |= extract_declared_idents_from_id(
sm,
Some(re.argument),
idents,
);
}
_ => panic!(
"cast<RestElementNode> failed: unexpected \
ObjectPattern property kind {}",
prop_node.node_type_str()
),
}
}
return contains_expr;
}
if let Node::ComponentParameter(param) = node {
return extract_declared_idents_from_id(sm, Some(param.local), idents);
}
sm.error_range(node.range(), "invalid destructuring target");
false
}
impl<'bt, 'sc, 'sm, 'ad> SemanticResolver<'bt, 'sc, 'sm, 'ad> {
fn function_body_scope_of(&self, scope: ScopeId) -> ScopeId {
let parent_function = self.sem_ctx.scope(scope).parent_function;
self.sem_ctx
.function(parent_function)
.get_function_body_scope()
}
fn cur_scope_is_function_body_scope(&self) -> bool {
let cur = self.cur_scope.expect("no active scope");
cur == self.function_body_scope_of(cur)
}
fn decl_in_cur_function(&self, decl: DeclId) -> bool {
let scope = self
.sem_ctx
.decl(decl)
.scope
.expect("declInCurFunction requires a scoped decl");
self.sem_ctx.scope(scope).parent_function == self.cur_function_info()
}
pub(super) fn extract_idents_from_decl<'gc>(
&mut self,
node: &'gc Node<'gc>,
idents: &mut Vec<&'gc Node<'gc>>,
) -> DeclKind {
match node {
Node::VariableDeclaration(vd) => {
for decl in vd.declarations.iter() {
let vdecl = decl.as_variable_declarator().expect(
"VariableDeclaration child must be a \
VariableDeclarator",
);
self.extract_declared_idents_from_id(
Some(vdecl.id),
idents,
);
}
let kind_atom = vd.kind.get();
if kind_atom == self.kw().ident_var {
if self.in_global_scope_context() {
DeclKind::GlobalProperty
} else {
DeclKind::Var
}
} else if kind_atom == self.kw().ident_let {
DeclKind::Let
} else {
DeclKind::Const
}
}
Node::FunctionDeclaration(fd) => {
self.extract_declared_idents_from_id(fd.id, idents);
if self.cur_scope_is_function_body_scope() {
if self.in_global_scope_context() {
DeclKind::GlobalProperty
} else {
DeclKind::Var
}
} else {
DeclKind::ScopedFunction
}
}
Node::ClassDeclaration(cd) => {
self.extract_declared_idents_from_id(cd.id, idents);
DeclKind::Class
}
Node::CatchClause(cc) => {
self.extract_declared_idents_from_id(cc.param, idents);
if matches!(cc.param, Some(Node::Identifier(_))) {
DeclKind::ES5Catch
} else {
DeclKind::Catch
}
}
Node::ImportDeclaration(import_decl) => {
for spec in import_decl.specifiers.iter() {
match spec {
Node::ImportSpecifier(s) => {
self.extract_declared_idents_from_id(
Some(s.local),
idents,
);
}
Node::ImportDefaultSpecifier(s) => {
self.extract_declared_idents_from_id(
Some(s.local),
idents,
);
}
Node::ImportNamespaceSpecifier(s) => {
self.extract_declared_idents_from_id(
Some(s.local),
idents,
);
}
_ => {}
}
}
DeclKind::Import
}
_ => {
self.sm
.error_range(node.range(), "unsuppported declaration kind");
DeclKind::Var
}
}
}
pub(super) fn extract_declared_idents_from_id<'gc>(
&mut self,
node: Option<&'gc Node<'gc>>,
idents: &mut Vec<&'gc Node<'gc>>,
) -> bool {
extract_declared_idents_from_id(self.sm, node, idents)
}
pub(super) fn process_declarations(
&mut self,
gc: &GCLock,
decls: &[NodeRc],
) {
for decl_rc in decls {
let decl_node = decl_rc.node(gc);
if matches!(
decl_node,
Node::TypeAlias(_) | Node::TSTypeAliasDeclaration(_)
) {
continue;
}
let mut idents: Vec<&Node> = Vec::new();
let kind = self.extract_idents_from_decl(decl_node, &mut idents);
if TYPED {
unreachable!(
"typed-mode builtin-function skip is S2 scope \
(cpp:2138-2151)"
);
}
for ident in idents {
self.validate_and_declare_identifier(gc, kind, ident);
}
}
}
pub(super) fn validate_and_declare_identifier<'gc>(
&mut self,
gc: &'gc GCLock,
kind: DeclKind,
ident_node: &'gc Node<'gc>,
) {
let identifier = ident_node
.as_identifier()
.expect("validate_and_declare_identifier: not an Identifier");
if !self.validate_declaration_name(gc, kind, ident_node) {
return;
}
let mut prev_name: Option<Binding> =
self.binding_table.lookup(&identifier.name.get());
#[allow(clippy::overly_complex_bool_expr, clippy::needless_bool)]
if false {
if !self.sem_ctx.function(self.cur_function_info()).strict
&& identifier.name.get() == self.kw().ident_arguments
&& kind == DeclKind::Var
{
return;
}
}
if let Some(pn) = &prev_name {
if !self.decl_in_cur_function(pn.decl) {
prev_name = None;
}
}
let mut decl: Option<DeclId> = None;
let mut reuse_decl_for_new_binding = false;
if let Some(pn) = &prev_name {
if self.sem_ctx.decl(pn.decl).kind
!= DeclKind::UndeclaredGlobalProperty
{
let prev_kind = self.sem_ctx.decl(pn.decl).kind;
let cur_scope = self.cur_scope.expect("no active scope");
let same_scope =
self.sem_ctx.decl(pn.decl).scope == Some(cur_scope);
let top_level = self.cur_scope_is_function_body_scope();
let prev_in_prev_scope = self.sem_ctx.decl(pn.decl).scope
== self.sem_ctx.scope(cur_scope).parent_scope;
assert!(
!(prev_kind == DeclKind::ScopedFunction
&& kind == DeclKind::Var),
"invalid state, scopedFunctions are not at top-level"
);
if (prev_kind.is_let_like() && kind.is_var_like())
|| (prev_kind.is_var_like()
&& kind.is_let_like()
&& same_scope)
|| (prev_kind.is_let_like()
&& kind.is_let_like()
&& same_scope
&& !(!self
.sem_ctx
.function(self.cur_function_info())
.strict
&& prev_kind == DeclKind::ScopedFunction
&& kind == DeclKind::ScopedFunction))
|| (prev_kind == DeclKind::Parameter
&& kind.is_let_like()
&& top_level)
|| ((prev_kind == DeclKind::Catch
|| prev_kind == DeclKind::ES5Catch)
&& kind.is_let_like()
&& prev_in_prev_scope)
{
self.sm.error_range(
ident_node.range(),
format!(
"Identifier '{}' is already declared",
atom_str(gc, identifier.name.get())
),
);
if let Some(prev_ident) = &pn.ident {
self.sm.note_range(
prev_ident.node(gc).range(),
"previous declaration",
Subsystem::Unspecified,
);
}
return;
}
if prev_kind.is_var_like() && kind.is_var_like() {
decl = Some(pn.decl);
}
else if prev_kind.is_var_like()
&& kind == DeclKind::ScopedFunction
{
decl = None;
if same_scope {
decl = Some(pn.decl);
} else if let Some(&d) = self
.function_context()
.promoted_func_decls
.get(&identifier.name.get())
{
reuse_decl_for_new_binding = true;
decl = Some(d);
}
}
else if prev_kind == DeclKind::ES5Catch
&& kind == DeclKind::ScopedFunction
{
if let Some(&d) = self
.function_context()
.promoted_func_decls
.get(&identifier.name.get())
{
reuse_decl_for_new_binding = true;
decl = Some(d);
} else {
decl = None;
}
}
else if prev_kind == DeclKind::ScopedFunction
&& kind == DeclKind::ScopedFunction
{
decl = None;
}
}
}
if self.cur_scope == Some(self.sem_ctx.get_global_scope())
&& kind.is_let_like()
&& self.is_restricted_global_property(identifier.name.get())
{
self.sm.error_range(
ident_node.range(),
format!(
"Can't create duplicate variable that shadows a global \
property: '{}'",
atom_str(gc, identifier.name.get())
),
);
}
if self.sem_ctx.get_declaration_decl(identifier).is_some()
&& self
.function_context()
.promoted_func_decls
.contains_key(&identifier.name.get())
{
let cur_scope = self.cur_scope.expect("no active scope");
let new_decl = self.sem_ctx.new_decl_in_scope_default(
identifier.name.get(),
kind,
cur_scope,
);
self.binding_table.put(
identifier.name.get(),
Binding::new(new_decl, Some(NodeRc::from_node(gc, ident_node))),
);
self.sem_ctx
.set_promoted_decl(ident_node.node_id(), new_decl);
return;
}
if let Some(d) = decl {
if reuse_decl_for_new_binding {
self.binding_table.try_emplace(
identifier.name.get(),
Binding::new(d, Some(NodeRc::from_node(gc, ident_node))),
);
}
} else {
let new_decl = if kind.is_global() {
self.sem_ctx.new_global(identifier.name.get(), kind)
} else {
let cur_scope = self.cur_scope.expect("no active scope");
self.sem_ctx.new_decl_in_scope_default(
identifier.name.get(),
kind,
cur_scope,
)
};
self.binding_table.try_emplace(
identifier.name.get(),
Binding::new(new_decl, Some(NodeRc::from_node(gc, ident_node))),
);
decl = Some(new_decl);
}
self.sem_ctx.set_declaration_decl(
ident_node.node_id(),
identifier,
decl,
);
}
pub(super) fn validate_declaration_name(
&mut self,
gc: &GCLock,
decl_kind: DeclKind,
id_node: &Node,
) -> bool {
let identifier = id_node
.as_identifier()
.expect("validate_declaration_name: not an Identifier");
if self.sem_ctx.function(self.cur_function_info()).strict {
if identifier.name.get() == self.kw().ident_arguments
|| identifier.name.get() == self.kw().ident_eval
{
self.sm.error_range(
id_node.range(),
format!(
"cannot declare '{}' in strict mode",
atom_str(gc, identifier.name.get())
),
);
return false;
}
if decl_kind == DeclKind::Parameter
&& identifier.name.get() == self.kw().ident_let
{
self.sm.error_range(
id_node.range(),
"invalid parameter name 'let' in strict mode",
);
return false;
}
}
if (decl_kind == DeclKind::Let || decl_kind == DeclKind::Const)
&& identifier.name.get() == self.kw().ident_let
{
self.sm.error_range(
id_node.range(),
"'let' is disallowed as a lexically bound name",
);
return false;
}
true
}
pub(super) fn visit_variable_declaration<'gc>(
&mut self,
gc: &'gc GCLock,
node: &'gc Node<'gc>,
) -> TransformResult<&'gc Node<'gc>> {
let vd = match node {
Node::VariableDeclaration(vd) => vd,
_ => unreachable!(
"visit_variable_declaration: not a VariableDeclaration"
),
};
if self.compile()
&& (vd.kind.get() == self.kw().ident_using
|| vd.kind.get() == self.kw().ident_await_using)
{
self.sm.error_range(
node.range(),
"using declarations are not yet supported",
);
return TransformResult::Unchanged;
}
let result = node.visit_children_mut(gc, self);
if vd.kind.get() == self.kw().ident_var
&& !self.cur_scope_is_function_body_scope()
{
let mut idents: Vec<&Node> = Vec::new();
self.extract_idents_from_decl(node, &mut idents);
for ident_node in idents {
let identifier = ident_node.as_identifier().expect(
"extract_idents_from_decl only ever pushes Identifiers",
);
let name = identifier.name.get();
let Some((prev_binding, prev_depth)) =
self.binding_table.find_with_depth(&name)
else {
continue;
};
let prev_is_lexical_binding_of_promoted_func = self
.function_context()
.promoted_func_decls
.contains_key(&name)
&& prev_depth
!= self.function_context().binding_table_scope_depth;
let prev_scope = self
.sem_ctx
.decl(prev_binding.decl)
.scope
.expect("decl must be scoped");
if prev_scope == self.function_body_scope_of(prev_scope)
&& !prev_is_lexical_binding_of_promoted_func
{
continue;
}
let prev_kind = self.sem_ctx.decl(prev_binding.decl).kind;
if (prev_kind.is_let_like() && prev_kind != DeclKind::ES5Catch)
|| prev_is_lexical_binding_of_promoted_func
{
self.sm.error_range(
ident_node.range(),
format!(
"Identifier '{}' is already declared",
atom_str(gc, name)
),
);
if let Some(prev_ident) = &prev_binding.ident {
self.sm.note_range(
prev_ident.node(gc).range(),
"previous declaration",
Subsystem::Unspecified,
);
}
}
}
}
result
}
pub(super) fn visit_block_statement<'gc>(
&mut self,
gc: &'gc GCLock,
node: &'gc Node<'gc>,
path: Option<hermes_ast::visitor::Path<'gc>>,
) -> TransformResult<&'gc Node<'gc>> {
if let Some(p) = path {
if matches!(
p.parent,
Node::FunctionDeclaration(_)
| Node::FunctionExpression(_)
| Node::ArrowFunctionExpression(_)
) {
return node.visit_children_mut(gc, self);
}
}
let scope_state = self.enter_scope(Some(node), false);
self.process_collected_declarations(gc, node);
let result = node.visit_children_mut(gc, self);
self.exit_scope(scope_state);
result
}
pub(super) fn visit_object_pattern<'gc>(
&mut self,
gc: &'gc GCLock,
node: &'gc Node<'gc>,
) -> TransformResult<&'gc Node<'gc>> {
let n = node
.as_object_pattern()
.expect("visit_object_pattern: not an ObjectPattern");
let mut b = builder::ObjectPattern::from_node(n);
if let Some(properties) =
self.visit_node_list(gc, n.properties, node, NodeField::properties)
{
b.properties(properties);
}
b.build(gc)
}
pub(super) fn visit_array_pattern<'gc>(
&mut self,
gc: &'gc GCLock,
node: &'gc Node<'gc>,
) -> TransformResult<&'gc Node<'gc>> {
let n = node
.as_array_pattern()
.expect("visit_array_pattern: not an ArrayPattern");
let mut b = builder::ArrayPattern::from_node(n);
if let Some(elements) =
self.visit_node_list(gc, n.elements, node, NodeField::elements)
{
b.elements(elements);
}
b.build(gc)
}
}
#[cfg(test)]
mod tests {
use hermes_ast::context::Context;
use hermes_ast::node::{
Identifier, NumericLiteral, VariableDeclaration, VariableDeclarator,
};
use hermes_ast::node_child::NodeList;
use hermes_ast::node_child::NodeMetadata;
use hermes_parser::js::JSParserImpl;
use hermes_parser::lexer::{GrammarContext, JSLexer};
use hermes_support::location::{SMLoc, SMRange};
use hermes_support::manager::SourceErrorManager;
use hermes_support::persistent_scoped_map::Scope;
use super::*;
use crate::keywords::Keywords;
use crate::resolver::FunctionContext;
use crate::sem_context::{
ConstructorKind, CustomDirectives, FuncIsArrow, SemContext,
};
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 first_statement<'gc>(program_node: &'gc Node<'gc>) -> &'gc Node<'gc> {
match program_node {
Node::Program(p) => p.body.iter().next().expect("empty program"),
_ => unreachable!("first_statement: not a Program"),
}
}
fn alloc_identifier<'gc>(
gc: &'gc GCLock,
name: &str,
loc: SMLoc,
) -> &'gc Node<'gc> {
let atom = gc.atom_bytes(name);
gc.alloc(Node::Identifier(Identifier::new(
NodeMetadata::new(SMRange {
start: loc,
end: loc,
}),
atom,
None,
false,
)))
}
fn alloc_var_decl<'gc>(
gc: &'gc GCLock,
kw_var: Atom,
ident_node: &'gc Node<'gc>,
range: SMRange,
) -> &'gc Node<'gc> {
let declarator = gc.alloc(Node::VariableDeclarator(
VariableDeclarator::new(NodeMetadata::new(range), None, ident_node),
));
gc.alloc(Node::VariableDeclaration(VariableDeclaration::new(
NodeMetadata::new(range),
kw_var,
NodeList::from_iter(gc, [declarator]),
)))
}
#[test]
fn function_declaration_at_global_top_level_is_global_property() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "function f() {}\n");
let func_decl = first_statement(root);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let binding_table = sem_ctx.binding_table_rc();
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
let func_state = resolver.enter_function(
&gc,
root,
None,
false,
ConstructorKind::None,
CustomDirectives::default(),
true,
);
let scope_state =
resolver.enter_scope(None, true);
let mut idents: Vec<&Node> = Vec::new();
let kind = resolver.extract_idents_from_decl(func_decl, &mut idents);
assert_eq!(kind, DeclKind::GlobalProperty);
assert_eq!(idents.len(), 1);
assert!(matches!(idents[0], Node::Identifier(_)));
resolver.exit_scope(scope_state);
resolver.exit_function(func_state);
}
#[test]
fn function_declaration_at_non_global_top_level_is_var() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "function f() {}\n");
let func_decl = first_statement(root);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let binding_table = sem_ctx.binding_table_rc();
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
let func_state = resolver.enter_function(
&gc,
root,
None,
false,
ConstructorKind::None,
CustomDirectives::default(),
false,
);
let scope_state = resolver.enter_scope(None, true);
let mut idents: Vec<&Node> = Vec::new();
let kind = resolver.extract_idents_from_decl(func_decl, &mut idents);
assert_eq!(kind, DeclKind::Var);
resolver.exit_scope(scope_state);
resolver.exit_function(func_state);
}
#[test]
fn function_declaration_in_nested_scope_is_scoped_function() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "function f() {}\n");
let func_decl = first_statement(root);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let binding_table = sem_ctx.binding_table_rc();
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
let func_state = resolver.enter_function(
&gc,
root,
None,
false,
ConstructorKind::None,
CustomDirectives::default(),
true,
);
let body_scope_state = resolver.enter_scope(None, true);
let block_scope_state = resolver.enter_scope(None, false);
let mut idents: Vec<&Node> = Vec::new();
let kind = resolver.extract_idents_from_decl(func_decl, &mut idents);
assert_eq!(kind, DeclKind::ScopedFunction);
resolver.exit_scope(block_scope_state);
resolver.exit_scope(body_scope_state);
resolver.exit_function(func_state);
}
#[test]
fn class_declaration_is_class() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "class C {}\n");
let class_decl = first_statement(root);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let binding_table = sem_ctx.binding_table_rc();
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
let mut idents: Vec<&Node> = Vec::new();
let kind = resolver.extract_idents_from_decl(class_decl, &mut idents);
assert_eq!(kind, DeclKind::Class);
assert_eq!(idents.len(), 1);
}
#[test]
fn catch_with_identifier_param_is_es5catch() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "try {} catch (e) {}\n");
let try_stmt = first_statement(root);
let handler = match try_stmt {
Node::TryStatement(t) => t.handler.expect("try has a handler"),
_ => unreachable!(),
};
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let binding_table = sem_ctx.binding_table_rc();
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
let mut idents: Vec<&Node> = Vec::new();
let kind = resolver.extract_idents_from_decl(handler, &mut idents);
assert_eq!(kind, DeclKind::ES5Catch);
assert_eq!(idents.len(), 1);
}
#[test]
fn catch_with_destructuring_param_is_catch() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "try {} catch ({a, b}) {}\n");
let try_stmt = first_statement(root);
let handler = match try_stmt {
Node::TryStatement(t) => t.handler.expect("try has a handler"),
_ => unreachable!(),
};
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let binding_table = sem_ctx.binding_table_rc();
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
let mut idents: Vec<&Node> = Vec::new();
let kind = resolver.extract_idents_from_decl(handler, &mut idents);
assert_eq!(kind, DeclKind::Catch);
assert_eq!(idents.len(), 2);
}
#[test]
fn catch_with_no_param_is_catch() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "try {} catch {}\n");
let try_stmt = first_statement(root);
let handler = match try_stmt {
Node::TryStatement(t) => t.handler.expect("try has a handler"),
_ => unreachable!(),
};
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let binding_table = sem_ctx.binding_table_rc();
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
let mut idents: Vec<&Node> = Vec::new();
let kind = resolver.extract_idents_from_decl(handler, &mut idents);
assert_eq!(kind, DeclKind::Catch);
assert_eq!(idents.len(), 0);
}
#[test]
fn import_declaration_collects_default_and_named_locals() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "import def, {a, b as c} from \"m\";\n");
let import_decl = first_statement(root);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let binding_table = sem_ctx.binding_table_rc();
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
let mut idents: Vec<&Node> = Vec::new();
let kind = resolver.extract_idents_from_decl(import_decl, &mut idents);
assert_eq!(kind, DeclKind::Import);
let names: Vec<String> = idents
.iter()
.map(|n| {
let id = n.as_identifier().unwrap();
String::from_utf8_lossy(gc.bytes(id.name.get())).into_owned()
})
.collect();
assert_eq!(names, vec!["def", "a", "c"]);
}
#[test]
fn import_declaration_collects_namespace_local() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, "import * as ns from \"m\";\n");
let import_decl = first_statement(root);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let binding_table = sem_ctx.binding_table_rc();
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
let mut idents: Vec<&Node> = Vec::new();
let kind = resolver.extract_idents_from_decl(import_decl, &mut idents);
assert_eq!(kind, DeclKind::Import);
assert_eq!(idents.len(), 1);
let id = idents[0].as_identifier().unwrap();
assert_eq!(String::from_utf8_lossy(gc.bytes(id.name.get())), "ns");
}
#[test]
fn unsupported_declaration_kind_reports_error() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("d.js", b"1");
let loc = SMLoc {
source: buf,
offset: 0,
};
let range = SMRange {
start: loc,
end: loc,
};
let node = gc.alloc(Node::NumericLiteral(NumericLiteral::new(
NodeMetadata::new(range),
1.0,
)));
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let binding_table = sem_ctx.binding_table_rc();
{
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
let mut idents: Vec<&Node> = Vec::new();
let kind = resolver.extract_idents_from_decl(node, &mut idents);
assert_eq!(kind, DeclKind::Var);
assert!(idents.is_empty());
}
assert_eq!(sm.error_count(), 1);
}
#[test]
fn invalid_destructuring_target_reports_error() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("d.js", b"1");
let loc = SMLoc {
source: buf,
offset: 0,
};
let range = SMRange {
start: loc,
end: loc,
};
let node = gc.alloc(Node::NumericLiteral(NumericLiteral::new(
NodeMetadata::new(range),
1.0,
)));
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let binding_table = sem_ctx.binding_table_rc();
{
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
let mut idents: Vec<&Node> = Vec::new();
let contains_expr = resolver
.extract_declared_idents_from_id(Some(node), &mut idents);
assert!(!contains_expr);
assert!(idents.is_empty());
}
assert_eq!(sm.error_count(), 1);
}
#[test]
fn validate_declaration_name_rejects_strict_arguments_and_eval() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("d.js", b"arguments eval");
let loc_a = SMLoc {
source: buf,
offset: 0,
};
let loc_e = SMLoc {
source: buf,
offset: 10,
};
let arguments_node = alloc_identifier(&gc, "arguments", loc_a);
let eval_node = alloc_identifier(&gc, "eval", loc_e);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let func = sem_ctx.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
true,
CustomDirectives::default(),
);
let binding_table = sem_ctx.binding_table_rc();
{
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
resolver.function_stack.push(FunctionContext {
sem_info: func,
node: None,
label_map: Default::default(),
current_loop: None,
current_loop_or_switch: None,
is_formal_params: false,
decls: None,
promoted_func_decls: Default::default(),
binding_table_scope_depth: 0,
});
assert!(!resolver.validate_declaration_name(
&gc,
DeclKind::Var,
arguments_node
));
assert!(!resolver.validate_declaration_name(
&gc,
DeclKind::Let,
eval_node
));
}
assert_eq!(sm.error_count(), 2);
}
#[test]
fn validate_declaration_name_rejects_strict_parameter_named_let() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("d.js", b"let");
let loc = SMLoc {
source: buf,
offset: 0,
};
let let_node = alloc_identifier(&gc, "let", loc);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let func = sem_ctx.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
true,
CustomDirectives::default(),
);
let binding_table = sem_ctx.binding_table_rc();
{
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
resolver.function_stack.push(FunctionContext {
sem_info: func,
node: None,
label_map: Default::default(),
current_loop: None,
current_loop_or_switch: None,
is_formal_params: false,
decls: None,
promoted_func_decls: Default::default(),
binding_table_scope_depth: 0,
});
assert!(!resolver.validate_declaration_name(
&gc,
DeclKind::Parameter,
let_node
));
}
assert_eq!(sm.error_count(), 1);
}
#[test]
fn validate_declaration_name_rejects_let_named_let_in_loose_mode() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("d.js", b"let");
let loc = SMLoc {
source: buf,
offset: 0,
};
let let_node = alloc_identifier(&gc, "let", loc);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let func = sem_ctx.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
CustomDirectives::default(),
);
let binding_table = sem_ctx.binding_table_rc();
{
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
resolver.function_stack.push(FunctionContext {
sem_info: func,
node: None,
label_map: Default::default(),
current_loop: None,
current_loop_or_switch: None,
is_formal_params: false,
decls: None,
promoted_func_decls: Default::default(),
binding_table_scope_depth: 0,
});
assert!(!resolver.validate_declaration_name(
&gc,
DeclKind::Let,
let_node
));
assert!(!resolver.validate_declaration_name(
&gc,
DeclKind::Const,
let_node
));
assert!(resolver.validate_declaration_name(
&gc,
DeclKind::Var,
let_node
));
}
assert_eq!(sm.error_count(), 2);
}
#[test]
fn parameter_then_toplevel_let_is_invalid() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("d.js", b"x x");
let loc_param = SMLoc {
source: buf,
offset: 0,
};
let loc_let = SMLoc {
source: buf,
offset: 2,
};
let param_ident = alloc_identifier(&gc, "x", loc_param);
let let_ident = alloc_identifier(&gc, "x", loc_let);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let name = gc.atom_bytes("x");
let func = sem_ctx.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
CustomDirectives::default(),
);
let param_scope = sem_ctx.new_scope(func, None);
let body_scope = sem_ctx.new_scope(func, Some(param_scope));
sem_ctx.function_mut(func).function_body_scope_idx = 1;
let param_decl = sem_ctx.new_decl_in_scope_default(
name,
DeclKind::Parameter,
param_scope,
);
let binding_table = sem_ctx.binding_table_rc();
let _bscope = Scope::new(&binding_table);
binding_table.try_emplace(
name,
Binding::new(param_decl, Some(NodeRc::from_node(&gc, param_ident))),
);
{
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
resolver.function_stack.push(FunctionContext {
sem_info: func,
node: None,
label_map: Default::default(),
current_loop: None,
current_loop_or_switch: None,
is_formal_params: false,
decls: None,
promoted_func_decls: Default::default(),
binding_table_scope_depth: 0,
});
resolver.cur_scope = Some(body_scope);
resolver.validate_and_declare_identifier(
&gc,
DeclKind::Let,
let_ident,
);
}
assert_eq!(sm.error_count(), 1);
assert_eq!(sm.note_count(), 1);
assert!(sem_ctx.scope(body_scope).decls.is_empty());
}
#[test]
fn catch_then_let_in_catch_body_is_invalid() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("d.js", b"e e");
let loc_catch = SMLoc {
source: buf,
offset: 0,
};
let loc_let = SMLoc {
source: buf,
offset: 2,
};
let catch_ident = alloc_identifier(&gc, "e", loc_catch);
let let_ident = alloc_identifier(&gc, "e", loc_let);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let name = gc.atom_bytes("e");
let func = sem_ctx.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
CustomDirectives::default(),
);
let top_scope = sem_ctx.new_scope(func, None);
sem_ctx.function_mut(func).function_body_scope_idx = 0;
let catch_param_scope = sem_ctx.new_scope(func, Some(top_scope));
let catch_body_scope = sem_ctx.new_scope(func, Some(catch_param_scope));
let catch_decl = sem_ctx.new_decl_in_scope_default(
name,
DeclKind::Catch,
catch_param_scope,
);
let binding_table = sem_ctx.binding_table_rc();
let _bscope = Scope::new(&binding_table);
binding_table.try_emplace(
name,
Binding::new(catch_decl, Some(NodeRc::from_node(&gc, catch_ident))),
);
{
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
resolver.function_stack.push(FunctionContext {
sem_info: func,
node: None,
label_map: Default::default(),
current_loop: None,
current_loop_or_switch: None,
is_formal_params: false,
decls: None,
promoted_func_decls: Default::default(),
binding_table_scope_depth: 0,
});
resolver.cur_scope = Some(catch_body_scope);
resolver.validate_and_declare_identifier(
&gc,
DeclKind::Let,
let_ident,
);
}
assert_eq!(sm.error_count(), 1);
assert_eq!(sm.note_count(), 1);
}
#[test]
fn es5catch_then_var_is_valid() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("d.js", b"e");
let loc = SMLoc {
source: buf,
offset: 0,
};
let range = SMRange {
start: loc,
end: loc,
};
let catch_ident = alloc_identifier(&gc, "e", loc);
let var_ident = alloc_identifier(&gc, "e", loc);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let name = gc.atom_bytes("e");
let kw_var = sem_ctx.kw.ident_var;
let func = sem_ctx.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
CustomDirectives::default(),
);
let body_scope = sem_ctx.new_scope(func, None);
sem_ctx.function_mut(func).function_body_scope_idx = 0;
let nested_scope = sem_ctx.new_scope(func, Some(body_scope));
let es5catch_decl = sem_ctx.new_decl_in_scope_default(
name,
DeclKind::ES5Catch,
nested_scope,
);
let binding_table = sem_ctx.binding_table_rc();
let _bscope = Scope::new(&binding_table);
binding_table.try_emplace(
name,
Binding::new(
es5catch_decl,
Some(NodeRc::from_node(&gc, catch_ident)),
),
);
{
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
resolver.function_stack.push(FunctionContext {
sem_info: func,
node: None,
label_map: Default::default(),
current_loop: None,
current_loop_or_switch: None,
is_formal_params: false,
decls: None,
promoted_func_decls: Default::default(),
binding_table_scope_depth: 0,
});
resolver.cur_scope = Some(nested_scope);
let var_decl = alloc_var_decl(&gc, kw_var, var_ident, range);
resolver.visit_variable_declaration(&gc, var_decl);
}
assert_eq!(sm.error_count(), 0);
assert_eq!(sm.note_count(), 0);
}
#[test]
fn restricted_global_property_rejects_lexical_shadow() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("d.js", b"NaN");
let loc = SMLoc {
source: buf,
offset: 0,
};
let nan_node = alloc_identifier(&gc, "NaN", loc);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let func = sem_ctx.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
CustomDirectives::default(),
);
let global_scope = sem_ctx.new_scope(func, None);
assert_eq!(global_scope, sem_ctx.get_global_scope());
let binding_table = sem_ctx.binding_table_rc();
let _bscope = Scope::new(&binding_table);
{
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
resolver.function_stack.push(FunctionContext {
sem_info: func,
node: None,
label_map: Default::default(),
current_loop: None,
current_loop_or_switch: None,
is_formal_params: false,
decls: None,
promoted_func_decls: Default::default(),
binding_table_scope_depth: 0,
});
resolver.cur_scope = Some(global_scope);
resolver.validate_and_declare_identifier(
&gc,
DeclKind::Let,
nan_node,
);
}
assert_eq!(sm.error_count(), 1);
}
#[test]
fn var_then_scoped_function_reuses_promoted_decl_in_different_scope() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("d.js", b"foo foo");
let loc_var = SMLoc {
source: buf,
offset: 0,
};
let loc_func = SMLoc {
source: buf,
offset: 4,
};
let func_ident = alloc_identifier(&gc, "foo", loc_func);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let name = gc.atom_bytes("foo");
let func = sem_ctx.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
CustomDirectives::default(),
);
let top_scope = sem_ctx.new_scope(func, None);
sem_ctx.function_mut(func).function_body_scope_idx = 0;
let var_decl =
sem_ctx.new_decl_in_scope_default(name, DeclKind::Var, top_scope);
let nested_scope = sem_ctx.new_scope(func, Some(top_scope));
let promoted_decl =
sem_ctx.new_decl_in_scope_default(name, DeclKind::Var, top_scope);
let binding_table = sem_ctx.binding_table_rc();
let _bscope_top = Scope::new(&binding_table);
binding_table.try_emplace(
name,
Binding::new(var_decl, Some(NodeRc::from_node(&gc, func_ident))),
);
let _bscope_nested = Scope::new(&binding_table);
{
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
resolver.function_stack.push(FunctionContext {
sem_info: func,
node: None,
label_map: Default::default(),
current_loop: None,
current_loop_or_switch: None,
is_formal_params: false,
decls: None,
promoted_func_decls: [(name, promoted_decl)]
.into_iter()
.collect(),
binding_table_scope_depth: 0,
});
resolver.cur_scope = Some(nested_scope);
resolver.validate_and_declare_identifier(
&gc,
DeclKind::ScopedFunction,
func_ident,
);
}
assert_eq!(sm.error_count(), 0);
let new_binding = binding_table.lookup(&name).expect("binding present");
assert_eq!(new_binding.decl, promoted_decl);
assert_ne!(new_binding.decl, var_decl);
let identifier = func_ident.as_identifier().unwrap();
assert_eq!(
sem_ctx.get_declaration_decl(identifier),
Some(promoted_decl)
);
let _ = loc_var; }
#[test]
fn promoted_decl_side_table_branch_creates_a_new_block_scoped_decl() {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("d.js", b"foo");
let loc = SMLoc {
source: buf,
offset: 0,
};
let ident_node = alloc_identifier(&gc, "foo", loc);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let name = gc.atom_bytes("foo");
let func = sem_ctx.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
CustomDirectives::default(),
);
let top_scope = sem_ctx.new_scope(func, None);
let original_decl = sem_ctx.new_decl_in_scope_default(
name,
DeclKind::GlobalProperty,
top_scope,
);
let identifier = ident_node.as_identifier().unwrap();
sem_ctx.set_declaration_decl(
ident_node.node_id(),
identifier,
Some(original_decl),
);
let block_scope = sem_ctx.new_scope(func, Some(top_scope));
let binding_table = sem_ctx.binding_table_rc();
let _bscope = Scope::new(&binding_table);
{
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
resolver.function_stack.push(FunctionContext {
sem_info: func,
node: None,
label_map: Default::default(),
current_loop: None,
current_loop_or_switch: None,
is_formal_params: false,
decls: None,
promoted_func_decls: [(name, original_decl)]
.into_iter()
.collect(),
binding_table_scope_depth: 0,
});
resolver.cur_scope = Some(block_scope);
resolver.validate_and_declare_identifier(
&gc,
DeclKind::ScopedFunction,
ident_node,
);
}
assert_eq!(sm.error_count(), 0);
let promoted = sem_ctx
.get_promoted_decl(ident_node.node_id())
.expect("promoted decl side table populated");
assert_ne!(promoted, original_decl);
assert_eq!(sem_ctx.decl(promoted).scope, Some(block_scope));
let new_binding = binding_table.lookup(&name).expect("binding present");
assert_eq!(new_binding.decl, promoted);
assert_eq!(
sem_ctx.get_declaration_decl(identifier),
Some(original_decl)
);
}
#[test]
fn scoped_function_redeclaration_same_scope_strict_vs_loose() {
for strict in [true, false] {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("d.js", b"foo foo");
let loc_first = SMLoc {
source: buf,
offset: 0,
};
let loc_second = SMLoc {
source: buf,
offset: 4,
};
let first_ident = alloc_identifier(&gc, "foo", loc_first);
let second_ident = alloc_identifier(&gc, "foo", loc_second);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let name = gc.atom_bytes("foo");
let func = sem_ctx.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
strict,
CustomDirectives::default(),
);
let scope = sem_ctx.new_scope(func, None);
sem_ctx.function_mut(func).function_body_scope_idx = 0;
let first_decl = sem_ctx.new_decl_in_scope_default(
name,
DeclKind::ScopedFunction,
scope,
);
let binding_table = sem_ctx.binding_table_rc();
let _bscope = Scope::new(&binding_table);
binding_table.try_emplace(
name,
Binding::new(
first_decl,
Some(NodeRc::from_node(&gc, first_ident)),
),
);
{
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
resolver.function_stack.push(FunctionContext {
sem_info: func,
node: None,
label_map: Default::default(),
current_loop: None,
current_loop_or_switch: None,
is_formal_params: false,
decls: None,
promoted_func_decls: Default::default(),
binding_table_scope_depth: 0,
});
resolver.cur_scope = Some(scope);
resolver.validate_and_declare_identifier(
&gc,
DeclKind::ScopedFunction,
second_ident,
);
}
if strict {
assert_eq!(sm.error_count(), 1, "strict mode must reject it");
assert_eq!(sm.note_count(), 1);
} else {
assert_eq!(sm.error_count(), 0, "loose mode must allow it");
let identifier = second_ident.as_identifier().unwrap();
let second_decl = sem_ctx
.get_declaration_decl(identifier)
.expect("a fresh decl was declared");
assert_ne!(
second_decl, first_decl,
"loose mode declares a NEW decl, it doesn't reuse the first"
);
}
}
}
}