use hermes_ast::context::{GCLock, NodeRc};
use hermes_ast::node::{Node, NodeField};
use hermes_ast::visitor::{Path, TransformResult};
use hermes_support::diag::{Subsystem, Warning};
use crate::ids::DeclId;
use crate::sem_context::{
private_name_identifier, Atom, Binding, DeclKind, DeclSpecial,
};
use super::SemanticResolver;
impl<'bt, 'sc, 'sm, 'ad> SemanticResolver<'bt, 'sc, 'sm, 'ad> {
pub(super) fn declare_arguments(&mut self) {
let func = self.cur_function_info();
let arguments_name = self.kw().ident_arguments;
let args_decl = self.sem_ctx.func_arguments_decl(func, arguments_name);
self.binding_table
.try_emplace(arguments_name, Binding::new(args_decl, None));
}
fn check_identifier_resolved(&mut self, node: &Node) -> Option<DeclId> {
let identifier = node
.as_identifier()
.expect("check_identifier_resolved: not an Identifier node");
if identifier.unresolvable.get() {
return None;
}
if let Some(decl) = self.sem_ctx.get_expression_decl(identifier) {
return Some(decl);
}
if let Some(binding) = self.binding_table.find(&identifier.name.get()) {
self.sem_ctx.set_expression_decl(
node.node_id(),
identifier,
Some(binding.decl),
);
return Some(binding.decl);
}
None
}
fn function_like_identifier<'gc>(
node: &'gc Node<'gc>,
) -> Option<&'gc Node<'gc>> {
match node {
Node::FunctionExpression(n) => n.id,
Node::FunctionDeclaration(n) => n.id,
Node::ArrowFunctionExpression(_) => None,
_ => {
debug_assert!(
matches!(node, Node::Program(_)),
"invalid FunctionLikeNode"
);
None
}
}
}
fn function_name(&self, gc: &GCLock) -> Option<Atom> {
let node_rc = self.function_context().node.as_ref()?;
let id_node = Self::function_like_identifier(node_rc.node(gc))?;
match id_node {
Node::Identifier(id) => Some(id.name.get()),
_ => None,
}
}
pub(super) fn declare_private_name<'gc>(
&mut self,
gc: &'gc GCLock,
ident_node: &'gc Node<'gc>,
kind: DeclKind,
is_static: bool,
) -> DeclId {
let identifier = ident_node
.as_identifier()
.expect("declare_private_name: not an Identifier node");
let private_name_str =
private_name_identifier(gc, identifier.name.get());
let cur_scope = self
.cur_scope
.expect("a private name is always declared in the class scope");
let decl = self.sem_ctx.new_decl_in_scope(
private_name_str,
kind,
cur_scope,
if is_static {
DeclSpecial::PrivateStatic
} else {
DeclSpecial::NotSpecial
},
);
let res = self.binding_table.try_emplace(
private_name_str,
Binding::new(decl, Some(NodeRc::from_node(gc, ident_node))),
);
debug_assert!(
res,
"cannot re-declare a private name in the same scope."
);
self.sem_ctx
.set_both_decl(ident_node.node_id(), identifier, Some(decl));
decl
}
pub(super) fn resolve_private_name(
&mut self,
gc: &GCLock,
ident_node: &Node,
) -> Option<DeclId> {
let identifier = ident_node
.as_identifier()
.expect("resolve_private_name: not an Identifier node");
if let Some(decl) = self.sem_ctx.get_expression_decl(identifier) {
return Some(decl);
}
let private_name_str =
private_name_identifier(gc, identifier.name.get());
if let Some(binding) = self.binding_table.find(&private_name_str) {
self.sem_ctx.set_expression_decl(
ident_node.node_id(),
identifier,
Some(binding.decl),
);
return Some(binding.decl);
}
None
}
pub(super) fn resolve_identifier<'gc>(
&mut self,
gc: &'gc GCLock,
node: &'gc Node<'gc>,
in_typeof: bool,
) -> DeclId {
let identifier = node
.as_identifier()
.expect("resolve_identifier: not an Identifier node");
let decl = self.check_identifier_resolved(node);
if let Some(d) = decl {
if self.sem_ctx.decl(d).special == DeclSpecial::Arguments {
if self.forbid_special_arguments_reference {
self.sm.error_range(
identifier.metadata.range.get(),
"invalid use of 'arguments'",
);
}
let f = self.cur_function_info();
self.sem_ctx.function_mut(f).uses_arguments = true;
}
}
if identifier.name.get() == self.kw().ident_await
&& self.forbid_await_as_identifier
{
self.sm.error_range(
identifier.metadata.range.get(),
"await is not a valid identifier name in an async function",
);
}
if identifier.name.get() == self.kw().ident_arguments
&& self.forbid_arguments_as_identifier
{
self.sm.error_range(
identifier.metadata.range.get(),
"invalid use of 'arguments' as an identifier",
);
}
if let Some(d) = decl {
return d;
}
if !in_typeof
&& self.sem_ctx.function(self.cur_function_info()).strict
&& identifier.name.get() != self.kw().ident_sh_builtin
{
let func_name_atom = self.function_name(gc);
let mut func_name = match func_name_atom {
Some(a) => String::from_utf8_lossy(gc.bytes(a)).into_owned(),
None => String::new(),
};
if self.in_global_scope_context() && func_name.is_empty() {
func_name = "global".to_string();
}
let func_type =
if self.sem_ctx.function(self.cur_function_info()).arrow {
"arrow function"
} else {
"function"
};
let disp_name = if !func_name.is_empty() {
format!("{func_type} \"{func_name}\"")
} else {
format!("anonymous {func_type}")
};
let ident_name =
String::from_utf8_lossy(gc.bytes(identifier.name.get()))
.into_owned();
self.sm.warning_range(
Warning::UndefinedVariable,
identifier.metadata.range.get(),
format!(
"the variable \"{ident_name}\" was not declared in \
{disp_name}"
),
Subsystem::Unspecified,
);
}
let name = identifier.name.get();
let new_decl = self
.sem_ctx
.new_global(name, DeclKind::UndeclaredGlobalProperty);
self.sem_ctx.set_expression_decl(
node.node_id(),
identifier,
Some(new_decl),
);
self.binding_table.try_emplace_into_scope(
&self.global_scope,
name,
Binding::new(new_decl, Some(NodeRc::from_node(gc, node))),
);
new_decl
}
pub(super) fn visit_identifier<'gc>(
&mut self,
gc: &'gc GCLock,
node: &'gc Node<'gc>,
path: Option<Path<'gc>>,
) -> TransformResult<&'gc Node<'gc>> {
let identifier = node
.as_identifier()
.expect("visit_identifier: not an Identifier node");
if let Some(p) = path {
if let Node::Property(prop) = p.parent {
if !prop.computed.get() && p.field == NodeField::key {
return TransformResult::Unchanged;
}
}
let member_like_skip = match p.parent {
Node::MemberExpression(m) => {
!m.computed.get() && p.field == NodeField::property
}
Node::OptionalMemberExpression(m) => {
!m.computed.get() && p.field == NodeField::property
}
_ => false,
};
if member_like_skip {
return TransformResult::Unchanged;
}
if matches!(
p.parent,
Node::MetaProperty(_)
| Node::BreakStatement(_)
| Node::ContinueStatement(_)
| Node::LabeledStatement(_)
) {
return TransformResult::Unchanged;
}
if let Node::UnaryExpression(unary) = p.parent {
if unary.operator.get() == self.kw().ident_typeof {
self.resolve_identifier(gc, node, true);
}
}
}
if identifier.name.get() == self.kw().ident_sh_builtin {
self.sm.error_range(
identifier.metadata.range.get(),
"invalid use of $SHBuiltin",
);
}
if let Some(p) = path {
if matches!(p.parent, Node::PrivateName(_)) {
return TransformResult::Unchanged;
}
}
self.resolve_identifier(gc, node, false);
TransformResult::Unchanged
}
}
#[cfg(test)]
mod tests {
use hermes_ast::context::Context;
use hermes_ast::node::Identifier;
use hermes_ast::node_child::NodeMetadata;
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, SemContext};
fn alloc_identifier<'gc>(
gc: &'gc hermes_ast::context::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,
)))
}
#[test]
fn expression_decl_is_cached_on_the_node_not_relooked_up() {
let mut ctx = Context::new();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("ident.js", b"x");
let loc = SMLoc {
source: buf,
offset: 0,
};
let gc = ctx.lock();
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let func = sem_ctx.new_function(
crate::sem_context::FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
CustomDirectives::default(),
);
let scope = sem_ctx.new_scope(func, None);
let name = gc.atom_bytes("x");
let decl = sem_ctx.new_decl_in_scope_default(
name,
crate::sem_context::DeclKind::Let,
scope,
);
let node = alloc_identifier(&gc, "x", loc);
let binding_table = sem_ctx.binding_table_rc();
let d1 = {
let bscope = Scope::new(&binding_table);
binding_table.try_emplace(name, Binding::new(decl, None));
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
let d = resolver.resolve_identifier(&gc, node, false);
drop(bscope); d
};
assert_eq!(d1, decl);
let d2 = {
let mut resolver2 = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
resolver2.resolve_identifier(&gc, node, false)
};
assert_eq!(d2, decl, "second resolution must hit the cached decl");
assert_eq!(sm.error_count(), 0);
assert_eq!(sm.warning_count(), 0);
}
#[test]
fn forbid_special_arguments_reference_reports_invalid_use() {
let mut ctx = Context::new();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("ident.js", b"x");
let loc = SMLoc {
source: buf,
offset: 0,
};
let gc = ctx.lock();
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let func = sem_ctx.new_function(
crate::sem_context::FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
CustomDirectives::default(),
);
let scope = sem_ctx.new_scope(func, None);
let name = gc.atom_bytes("arguments");
let decl = sem_ctx.new_decl_in_scope(
name,
crate::sem_context::DeclKind::Var,
scope,
DeclSpecial::Arguments,
);
let node = alloc_identifier(&gc, "arguments", loc);
let binding_table = sem_ctx.binding_table_rc();
let _bscope = Scope::new(&binding_table);
binding_table.try_emplace(name, Binding::new(decl, None));
let d = {
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.forbid_special_arguments_reference = true;
resolver.resolve_identifier(&gc, node, false)
};
assert_eq!(d, decl);
assert_eq!(sm.error_count(), 1);
assert_eq!(sm.warning_count(), 0);
assert!(sem_ctx.function(func).uses_arguments);
}
#[test]
fn forbid_await_as_identifier_reports_error() {
let mut ctx = Context::new();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("ident.js", b"x");
let loc = SMLoc {
source: buf,
offset: 0,
};
let gc = ctx.lock();
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let func = sem_ctx.new_function(
crate::sem_context::FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
CustomDirectives::default(),
);
let scope = sem_ctx.new_scope(func, None);
let name = sem_ctx.kw.ident_await;
let decl = sem_ctx.new_decl_in_scope_default(
name,
crate::sem_context::DeclKind::Let,
scope,
);
let node = alloc_identifier(&gc, "await", loc);
let binding_table = sem_ctx.binding_table_rc();
let _bscope = Scope::new(&binding_table);
binding_table.try_emplace(name, Binding::new(decl, None));
let d = {
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
resolver.forbid_await_as_identifier = true;
resolver.resolve_identifier(&gc, node, false)
};
assert_eq!(d, decl, "the forbid check must not prevent resolution");
assert_eq!(sm.error_count(), 1);
}
#[test]
fn forbid_arguments_as_identifier_reports_error() {
let mut ctx = Context::new();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("ident.js", b"x");
let loc = SMLoc {
source: buf,
offset: 0,
};
let gc = ctx.lock();
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let func = sem_ctx.new_function(
crate::sem_context::FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
CustomDirectives::default(),
);
let scope = sem_ctx.new_scope(func, None);
let name = sem_ctx.kw.ident_arguments;
let decl = sem_ctx.new_decl_in_scope_default(
name,
crate::sem_context::DeclKind::Let,
scope,
);
let node = alloc_identifier(&gc, "arguments", loc);
let binding_table = sem_ctx.binding_table_rc();
let _bscope = Scope::new(&binding_table);
binding_table.try_emplace(name, Binding::new(decl, None));
let d = {
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
resolver.forbid_arguments_as_identifier = true;
resolver.resolve_identifier(&gc, node, false)
};
assert_eq!(d, decl);
assert_eq!(sm.error_count(), 1);
}
#[test]
fn resolve_identifier_typeof_creates_ambient_global_without_warning() {
let mut ctx = Context::new();
let mut sm = SourceErrorManager::new();
let buf = sm.add_buffer_bytes("ident.js", b"x");
let loc = SMLoc {
source: buf,
offset: 0,
};
let gc = ctx.lock();
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let binding_table = sem_ctx.binding_table_rc();
let node = alloc_identifier(&gc, "missing", loc);
let program_placeholder =
gc.alloc(Node::Program(hermes_ast::node::Program::new(
NodeMetadata::new(SMRange {
start: loc,
end: loc,
}),
hermes_ast::node_child::NodeList::from_iter(&gc, []),
)));
let (d1, d2) = {
let mut resolver = SemanticResolver::new(
&binding_table,
&mut sem_ctx,
&mut sm,
&[],
true,
);
let func_state = resolver.enter_function(
&gc,
program_placeholder,
None,
true,
ConstructorKind::None,
CustomDirectives::default(),
true,
);
let scope_state = resolver.enter_scope(None, true);
resolver.global_scope = resolver.cur_binding_scope().ptr();
resolver
.sem_ctx
.set_binding_table_global_scope(resolver.global_scope.clone());
let d1 = resolver.resolve_identifier(&gc, node, true);
let d2 = resolver.resolve_identifier(&gc, node, false);
resolver.exit_scope(scope_state);
resolver.exit_function(func_state);
(d1, d2)
};
assert_eq!(d1, d2);
assert_eq!(sm.warning_count(), 0);
assert_eq!(sm.error_count(), 0);
assert_eq!(
sem_ctx.decl(d1).kind,
crate::sem_context::DeclKind::UndeclaredGlobalProperty
);
}
}