use std::collections::{HashMap, HashSet};
use crate::parser::ast::{
ArrowBody, AssignTarget, BlockStmt, CatchClause, ClassBody, ClassDecl, ClassExpr, ClassMember,
Expr, FnDecl, FnExpr, ForInOfLeft, ForInit, Ident, ModuleDecl, ObjectPatProp, Param, Pat,
Program, ProgramItem, SourceLocation, Stmt, VarDecl, VarKind,
};
pub type ScopeId = usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScopeKind {
Global,
Function,
Block,
Module,
Eval,
With,
Catch,
}
impl ScopeKind {
pub fn is_function_boundary(self) -> bool {
matches!(
self,
ScopeKind::Global | ScopeKind::Function | ScopeKind::Module | ScopeKind::Eval
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindingKind {
Var,
Let,
Const,
Function,
Class,
Param,
Import,
}
impl BindingKind {
pub fn has_tdz(self) -> bool {
matches!(
self,
BindingKind::Let | BindingKind::Const | BindingKind::Class
)
}
pub fn is_function_scoped(self) -> bool {
matches!(self, BindingKind::Var | BindingKind::Function)
}
}
#[derive(Debug, Clone)]
pub struct Binding {
pub name: String,
pub kind: BindingKind,
pub loc: SourceLocation,
pub scope_id: ScopeId,
}
#[derive(Debug, Clone)]
pub struct Scope {
pub id: ScopeId,
pub kind: ScopeKind,
pub parent: Option<ScopeId>,
pub children: Vec<ScopeId>,
pub bindings: HashMap<String, Binding>,
pub uses_arguments: bool,
pub uses_eval: bool,
pub uses_this: bool,
pub uses_super: bool,
pub captures: HashSet<String>,
pub is_strict: bool,
}
impl Scope {
fn new(id: ScopeId, kind: ScopeKind, parent: Option<ScopeId>) -> Self {
Self {
id,
kind,
parent,
children: Vec::new(),
bindings: HashMap::new(),
uses_arguments: false,
uses_eval: false,
uses_this: false,
uses_super: false,
captures: HashSet::new(),
is_strict: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScopeErrorKind {
DuplicateBinding {
previous: SourceLocation,
},
TdzViolation,
}
#[derive(Debug, Clone)]
pub struct ScopeError {
pub kind: ScopeErrorKind,
pub name: String,
pub loc: SourceLocation,
}
#[derive(Debug)]
pub struct ScopeTree {
pub scopes: Vec<Scope>,
pub root: ScopeId,
pub errors: Vec<ScopeError>,
}
impl ScopeTree {
pub fn scope(&self, id: ScopeId) -> &Scope {
&self.scopes[id]
}
}
pub fn analyze(program: &Program) -> ScopeTree {
let mut analyzer = Analyzer::new();
analyzer.analyze_program(program);
ScopeTree {
scopes: analyzer.scopes,
root: 0,
errors: analyzer.errors,
}
}
type TdzSet = HashSet<String>;
struct Analyzer {
scopes: Vec<Scope>,
scope_stack: Vec<ScopeId>,
errors: Vec<ScopeError>,
tdz: HashMap<ScopeId, TdzSet>,
}
impl Analyzer {
fn new() -> Self {
Self {
scopes: Vec::new(),
scope_stack: Vec::new(),
errors: Vec::new(),
tdz: HashMap::new(),
}
}
fn push_scope(&mut self, kind: ScopeKind) -> ScopeId {
let parent = self.scope_stack.last().copied();
let id = self.scopes.len();
self.scopes.push(Scope::new(id, kind, parent));
if let Some(p) = parent {
self.scopes[p].children.push(id);
}
self.scope_stack.push(id);
id
}
fn pop_scope(&mut self) {
self.scope_stack.pop();
}
fn current_scope(&self) -> ScopeId {
*self
.scope_stack
.last()
.expect("scope stack is never empty during analysis")
}
fn nearest_fn_scope(&self) -> ScopeId {
for &id in self.scope_stack.iter().rev() {
if self.scopes[id].kind.is_function_boundary() {
return id;
}
}
self.scope_stack[0]
}
fn declare(&mut self, name: &str, kind: BindingKind, loc: SourceLocation, scope_id: ScopeId) {
if let Some(existing) = self.scopes[scope_id].bindings.get(name) {
let prev_loc = existing.loc;
let prev_kind = existing.kind;
if kind == BindingKind::Var
&& matches!(prev_kind, BindingKind::Var | BindingKind::Function)
{
return;
}
if kind == BindingKind::Function && prev_kind == BindingKind::Function {
let b = self.scopes[scope_id].bindings.get_mut(name).unwrap();
b.loc = loc;
return;
}
self.errors.push(ScopeError {
kind: ScopeErrorKind::DuplicateBinding { previous: prev_loc },
name: name.to_owned(),
loc,
});
return;
}
let binding = Binding {
name: name.to_owned(),
kind,
loc,
scope_id,
};
self.scopes[scope_id]
.bindings
.insert(name.to_owned(), binding);
if kind.has_tdz() {
self.tdz
.entry(scope_id)
.or_default()
.insert(name.to_owned());
}
}
fn declare_auto(&mut self, name: &str, kind: BindingKind, loc: SourceLocation) {
let target = if kind.is_function_scoped() {
self.nearest_fn_scope()
} else {
self.current_scope()
};
self.declare(name, kind, loc, target);
}
fn exit_tdz(&mut self, name: &str, scope_id: ScopeId) {
if let Some(set) = self.tdz.get_mut(&scope_id) {
set.remove(name);
}
}
fn resolve_ref(&mut self, name: &str, loc: SourceLocation) {
if name == "undefined" || name == "Infinity" || name == "NaN" {
return;
}
let stack_len = self.scope_stack.len();
for i in (0..stack_len).rev() {
let sid = self.scope_stack[i];
if self.scopes[sid].kind == ScopeKind::With {
return;
}
if self.scopes[sid].bindings.contains_key(name) {
let in_tdz = self
.tdz
.get(&sid)
.map(|s| s.contains(name))
.unwrap_or(false);
if in_tdz {
self.errors.push(ScopeError {
kind: ScopeErrorKind::TdzViolation,
name: name.to_owned(),
loc,
});
return;
}
let decl_fn = self.nearest_fn_scope_from(i);
let ref_fn = self.nearest_fn_scope();
if decl_fn != ref_fn {
self.scopes[decl_fn].captures.insert(name.to_owned());
}
return;
}
}
}
fn nearest_fn_scope_from(&self, from: usize) -> ScopeId {
for i in (0..=from).rev() {
let id = self.scope_stack[i];
if self.scopes[id].kind.is_function_boundary() {
return id;
}
}
self.scope_stack[0]
}
fn mark_uses_arguments(&mut self) {
let fn_scope = self.nearest_fn_scope();
self.scopes[fn_scope].uses_arguments = true;
}
fn mark_uses_eval(&mut self) {
let sid = self.current_scope();
self.scopes[sid].uses_eval = true;
}
fn mark_uses_this(&mut self) {
let sid = self.current_scope();
self.scopes[sid].uses_this = true;
}
fn mark_uses_super(&mut self) {
let sid = self.current_scope();
self.scopes[sid].uses_super = true;
}
fn hoist_stmts(&mut self, stmts: &[Stmt]) {
for stmt in stmts {
self.hoist_stmt(stmt);
}
}
fn hoist_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::VarDecl(v) if v.kind == VarKind::Var => {
for decl in &v.declarators {
self.hoist_pat_bindings(&decl.id, BindingKind::Var);
}
}
Stmt::FnDecl(f) => {
if let Some(id) = &f.id {
self.declare_auto(&id.name, BindingKind::Function, id.loc);
}
}
Stmt::Block(b) => self.hoist_stmts(&b.body),
Stmt::If(s) => {
self.hoist_stmt(&s.consequent);
if let Some(alt) = &s.alternate {
self.hoist_stmt(alt);
}
}
Stmt::For(s) => {
if let Some(ForInit::VarDecl(v)) = &s.init
&& v.kind == VarKind::Var
{
for decl in &v.declarators {
self.hoist_pat_bindings(&decl.id, BindingKind::Var);
}
}
self.hoist_stmt(&s.body);
}
Stmt::ForIn(s) => {
if let ForInOfLeft::VarDecl(v) = &s.left
&& v.kind == VarKind::Var
{
for decl in &v.declarators {
self.hoist_pat_bindings(&decl.id, BindingKind::Var);
}
}
self.hoist_stmt(&s.body);
}
Stmt::ForOf(s) => {
if let ForInOfLeft::VarDecl(v) = &s.left
&& v.kind == VarKind::Var
{
for decl in &v.declarators {
self.hoist_pat_bindings(&decl.id, BindingKind::Var);
}
}
self.hoist_stmt(&s.body);
}
Stmt::While(s) => self.hoist_stmt(&s.body),
Stmt::DoWhile(s) => self.hoist_stmt(&s.body),
Stmt::Switch(s) => {
for case in &s.cases {
self.hoist_stmts(&case.consequent);
}
}
Stmt::Try(s) => {
self.hoist_stmts(&s.block.body);
if let Some(handler) = &s.handler {
self.hoist_stmts(&handler.body.body);
}
if let Some(fin) = &s.finalizer {
self.hoist_stmts(&fin.body);
}
}
Stmt::Labeled(s) => self.hoist_stmt(&s.body),
Stmt::With(s) => self.hoist_stmt(&s.body),
_ => {}
}
}
fn hoist_pat_bindings(&mut self, pat: &Pat, kind: BindingKind) {
match pat {
Pat::Ident(id) => self.declare_auto(&id.name, kind, id.loc),
Pat::Array(a) => {
for el in a.elements.iter().flatten() {
self.hoist_pat_bindings(el, kind);
}
}
Pat::Object(o) => {
for prop in &o.properties {
match prop {
ObjectPatProp::KeyValue(kv) => self.hoist_pat_bindings(&kv.value, kind),
ObjectPatProp::Assign(ap) => {
self.declare_auto(&ap.key.name, kind, ap.key.loc)
}
ObjectPatProp::Rest(r) => self.hoist_pat_bindings(&r.argument, kind),
}
}
}
Pat::Rest(r) => self.hoist_pat_bindings(&r.argument, kind),
Pat::Assign(a) => self.hoist_pat_bindings(&a.left, kind),
Pat::Expr(_) => {} }
}
fn analyze_program(&mut self, program: &crate::parser::ast::Program) {
use crate::parser::ast::SourceType;
let kind = if program.source_type == SourceType::Module {
ScopeKind::Module
} else {
ScopeKind::Global
};
let scope_id = self.push_scope(kind);
if program.is_strict || program.source_type == SourceType::Module {
self.scopes[scope_id].is_strict = true;
}
let stmts: Vec<Stmt> = program
.body
.iter()
.filter_map(|item| {
if let ProgramItem::Stmt(s) = item {
Some(s.clone())
} else {
None
}
})
.collect();
self.hoist_stmts(&stmts);
for item in &program.body {
match item {
ProgramItem::Stmt(s) => self.visit_stmt(s),
ProgramItem::ModuleDecl(m) => self.visit_module_decl(m),
}
}
self.pop_scope();
}
fn visit_module_decl(&mut self, decl: &ModuleDecl) {
match decl {
ModuleDecl::Import(imp) => {
use crate::parser::ast::ImportSpecifier;
for spec in &imp.specifiers {
let local: &Ident = match spec {
ImportSpecifier::Named(s) => &s.local,
ImportSpecifier::Default(s) => &s.local,
ImportSpecifier::Namespace(s) => &s.local,
};
self.declare_auto(&local.name, BindingKind::Import, local.loc);
}
}
ModuleDecl::ExportNamed(e) => {
if let Some(decl) = &e.declaration {
self.visit_stmt(decl);
}
for spec in &e.specifiers {
use crate::parser::ast::ModuleExportName;
if let ModuleExportName::Ident(id) = &spec.local {
self.resolve_ref(&id.name, id.loc);
}
}
}
ModuleDecl::ExportDefault(e) => {
use crate::parser::ast::ExportDefaultExpr;
match &e.declaration {
ExportDefaultExpr::Fn(f) => self.visit_fn_decl(f),
ExportDefaultExpr::Class(c) => self.visit_class_decl(c),
ExportDefaultExpr::Expr(ex) => self.visit_expr(ex),
}
}
ModuleDecl::ExportAll(_) => {
}
}
}
fn visit_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::Block(b) => self.visit_block_stmt(b, false),
Stmt::VarDecl(v) => self.visit_var_decl(v),
Stmt::FnDecl(f) => self.visit_fn_decl(f),
Stmt::ClassDecl(c) => self.visit_class_decl(c),
Stmt::Expr(e) => self.visit_expr(&e.expr),
Stmt::If(s) => {
self.visit_expr(&s.test);
self.visit_stmt(&s.consequent);
if let Some(alt) = &s.alternate {
self.visit_stmt(alt);
}
}
Stmt::For(s) => {
let needs_scope = matches!(
&s.init,
Some(ForInit::VarDecl(v)) if v.kind != VarKind::Var
);
if needs_scope {
self.push_scope(ScopeKind::Block);
}
if let Some(init) = &s.init {
match init {
ForInit::VarDecl(v) => self.visit_var_decl(v),
ForInit::Expr(e) => self.visit_expr(e),
}
}
if let Some(test) = &s.test {
self.visit_expr(test);
}
if let Some(update) = &s.update {
self.visit_expr(update);
}
self.visit_stmt(&s.body);
if needs_scope {
self.pop_scope();
}
}
Stmt::ForIn(s) => {
let needs_scope =
matches!(&s.left, ForInOfLeft::VarDecl(v) if v.kind != VarKind::Var);
if needs_scope {
self.push_scope(ScopeKind::Block);
}
match &s.left {
ForInOfLeft::VarDecl(v) => self.visit_var_decl(v),
ForInOfLeft::Pat(p) => self.visit_pat_ref(p),
ForInOfLeft::Expr(e) => self.visit_expr(e),
}
self.visit_expr(&s.right);
self.visit_stmt(&s.body);
if needs_scope {
self.pop_scope();
}
}
Stmt::ForOf(s) => {
let needs_scope =
matches!(&s.left, ForInOfLeft::VarDecl(v) if v.kind != VarKind::Var);
if needs_scope {
self.push_scope(ScopeKind::Block);
}
match &s.left {
ForInOfLeft::VarDecl(v) => self.visit_var_decl(v),
ForInOfLeft::Pat(p) => self.visit_pat_ref(p),
ForInOfLeft::Expr(e) => self.visit_expr(e),
}
self.visit_expr(&s.right);
self.visit_stmt(&s.body);
if needs_scope {
self.pop_scope();
}
}
Stmt::While(s) => {
self.visit_expr(&s.test);
self.visit_stmt(&s.body);
}
Stmt::DoWhile(s) => {
self.visit_stmt(&s.body);
self.visit_expr(&s.test);
}
Stmt::Switch(s) => {
self.visit_expr(&s.discriminant);
self.push_scope(ScopeKind::Block);
for case in &s.cases {
if let Some(test) = &case.test {
self.visit_expr(test);
}
for stmt in &case.consequent {
self.visit_stmt(stmt);
}
}
self.pop_scope();
}
Stmt::Try(s) => self.visit_try_stmt(s),
Stmt::Return(s) => {
if let Some(arg) = &s.argument {
self.visit_expr(arg);
}
}
Stmt::Throw(s) => self.visit_expr(&s.argument),
Stmt::Break(_) | Stmt::Continue(_) | Stmt::Debugger(_) | Stmt::Empty(_) => {}
Stmt::Labeled(s) => self.visit_stmt(&s.body),
Stmt::With(s) => self.visit_with_stmt(s),
}
}
fn visit_block_stmt(&mut self, block: &BlockStmt, is_fn_body: bool) {
if !is_fn_body {
self.push_scope(ScopeKind::Block);
self.hoist_stmts(&block.body);
}
for stmt in &block.body {
self.visit_stmt(stmt);
}
if !is_fn_body {
self.pop_scope();
}
}
fn visit_var_decl(&mut self, v: &VarDecl) {
for decl in &v.declarators {
match v.kind {
VarKind::Var => {
}
VarKind::Let | VarKind::Using | VarKind::AwaitUsing => {
self.declare_pat_bindings(&decl.id, BindingKind::Let);
}
VarKind::Const => {
self.declare_pat_bindings(&decl.id, BindingKind::Const);
}
}
if let Some(init) = &decl.init {
self.visit_expr(init);
}
if matches!(
v.kind,
VarKind::Let | VarKind::Const | VarKind::Using | VarKind::AwaitUsing
) {
self.exit_tdz_pat(&decl.id);
}
}
}
fn declare_pat_bindings(&mut self, pat: &Pat, kind: BindingKind) {
match pat {
Pat::Ident(id) => self.declare_auto(&id.name, kind, id.loc),
Pat::Array(a) => {
for el in a.elements.iter().flatten() {
self.declare_pat_bindings(el, kind);
}
}
Pat::Object(o) => {
for prop in &o.properties {
match prop {
ObjectPatProp::KeyValue(kv) => self.declare_pat_bindings(&kv.value, kind),
ObjectPatProp::Assign(ap) => {
self.declare_auto(&ap.key.name, kind, ap.key.loc)
}
ObjectPatProp::Rest(r) => self.declare_pat_bindings(&r.argument, kind),
}
}
}
Pat::Rest(r) => self.declare_pat_bindings(&r.argument, kind),
Pat::Assign(a) => self.declare_pat_bindings(&a.left, kind),
Pat::Expr(_) => {} }
}
fn exit_tdz_pat(&mut self, pat: &Pat) {
let sid = self.current_scope();
match pat {
Pat::Ident(id) => self.exit_tdz(&id.name, sid),
Pat::Array(a) => {
for el in a.elements.iter().flatten() {
self.exit_tdz_pat(el);
}
}
Pat::Object(o) => {
for prop in &o.properties {
match prop {
ObjectPatProp::KeyValue(kv) => self.exit_tdz_pat(&kv.value),
ObjectPatProp::Assign(ap) => self.exit_tdz(&ap.key.name, sid),
ObjectPatProp::Rest(r) => self.exit_tdz_pat(&r.argument),
}
}
}
Pat::Rest(r) => self.exit_tdz_pat(&r.argument),
Pat::Assign(a) => self.exit_tdz_pat(&a.left),
Pat::Expr(_) => {} }
}
fn visit_fn_decl(&mut self, f: &FnDecl) {
let parent_strict = self.scopes[self.current_scope()].is_strict;
let scope_id = self.push_scope(ScopeKind::Function);
self.scopes[scope_id].is_strict = f.is_strict || parent_strict;
for param in &f.params {
self.declare_param(param);
}
self.hoist_stmts(&f.body.body);
for stmt in &f.body.body {
self.visit_stmt(stmt);
}
self.pop_scope();
}
fn visit_fn_expr(&mut self, f: &FnExpr) {
let parent_strict = self.scopes[self.current_scope()].is_strict;
let scope_id = self.push_scope(ScopeKind::Function);
self.scopes[scope_id].is_strict = f.is_strict || parent_strict;
if let Some(id) = &f.id {
self.declare(
&id.name,
BindingKind::Function,
id.loc,
self.current_scope(),
);
}
for param in &f.params {
self.declare_param(param);
}
self.hoist_stmts(&f.body.body);
for stmt in &f.body.body {
self.visit_stmt(stmt);
}
self.pop_scope();
}
fn visit_arrow_expr(&mut self, a: &crate::parser::ast::ArrowExpr) {
let parent_strict = self.scopes[self.current_scope()].is_strict;
let scope_id = self.push_scope(ScopeKind::Function);
self.scopes[scope_id].is_strict = a.is_strict || parent_strict;
for param in &a.params {
self.declare_param(param);
}
match &a.body {
ArrowBody::Block(b) => {
self.hoist_stmts(&b.body);
for stmt in &b.body {
self.visit_stmt(stmt);
}
}
ArrowBody::Expr(e) => self.visit_expr(e),
}
self.pop_scope();
}
fn visit_class_decl(&mut self, c: &ClassDecl) {
if let Some(id) = &c.id {
self.declare_auto(&id.name, BindingKind::Class, id.loc);
self.exit_tdz(&id.name, self.current_scope());
}
if let Some(super_class) = &c.super_class {
self.visit_expr(super_class);
}
self.visit_class_body(&c.body);
}
fn visit_class_expr(&mut self, c: &ClassExpr) {
self.push_scope(ScopeKind::Block);
if let Some(id) = &c.id {
let sid = self.current_scope();
self.declare(&id.name, BindingKind::Class, id.loc, sid);
self.exit_tdz(&id.name, sid);
}
if let Some(super_class) = &c.super_class {
self.visit_expr(super_class);
}
self.visit_class_body(&c.body);
self.pop_scope();
}
fn visit_class_body(&mut self, body: &ClassBody) {
for member in &body.body {
match member {
ClassMember::Method(m) => self.visit_fn_expr(&m.value),
ClassMember::Property(p) => {
if let Some(val) = &p.value {
self.visit_expr(val);
}
}
ClassMember::StaticBlock(s) => {
self.push_scope(ScopeKind::Block);
self.hoist_stmts(&s.body);
for stmt in &s.body {
self.visit_stmt(stmt);
}
self.pop_scope();
}
}
}
}
fn visit_try_stmt(&mut self, s: &crate::parser::ast::TryStmt) {
self.visit_block_stmt(&s.block, false);
if let Some(handler) = &s.handler {
self.visit_catch_clause(handler);
}
if let Some(fin) = &s.finalizer {
self.visit_block_stmt(fin, false);
}
}
fn visit_catch_clause(&mut self, clause: &CatchClause) {
self.push_scope(ScopeKind::Catch);
if let Some(param) = &clause.param {
self.declare_pat_bindings(param, BindingKind::Param);
}
self.hoist_stmts(&clause.body.body);
for stmt in &clause.body.body {
self.visit_stmt(stmt);
}
self.pop_scope();
}
fn visit_with_stmt(&mut self, s: &crate::parser::ast::WithStmt) {
self.visit_expr(&s.object);
self.push_scope(ScopeKind::With);
self.visit_stmt(&s.body);
self.pop_scope();
}
fn visit_expr(&mut self, expr: &Expr) {
match expr {
Expr::Ident(id) => {
if id.name == "arguments" {
self.mark_uses_arguments();
}
self.resolve_ref(&id.name, id.loc);
}
Expr::This(_) => self.mark_uses_this(),
Expr::MetaProp(m) => {
if m.meta.name == "super" {
self.mark_uses_super();
}
}
Expr::Null(_)
| Expr::Bool(_)
| Expr::Num(_)
| Expr::Str(_)
| Expr::BigInt(_)
| Expr::Regexp(_) => {}
Expr::Template(t) => {
for e in &t.expressions {
self.visit_expr(e);
}
}
Expr::Array(a) => {
for el in a.elements.iter().flatten() {
self.visit_expr(el);
}
}
Expr::Object(o) => {
use crate::parser::ast::{ObjectProp, PropValue};
for prop in &o.properties {
match prop {
ObjectProp::Prop(p) => match &p.value {
PropValue::Value(v) => self.visit_expr(v),
PropValue::Shorthand => {
if let crate::parser::ast::PropKey::Ident(id) = &p.key {
self.resolve_ref(&id.name, id.loc);
}
}
PropValue::Get(f) | PropValue::Set(f) | PropValue::Method(f) => {
self.visit_fn_expr(f)
}
},
ObjectProp::Spread(s) => self.visit_expr(&s.argument),
}
}
}
Expr::Fn(f) => self.visit_fn_expr(f),
Expr::Arrow(a) => self.visit_arrow_expr(a),
Expr::Class(c) => self.visit_class_expr(c),
Expr::Unary(u) => self.visit_expr(&u.argument),
Expr::Update(u) => self.visit_expr(&u.argument),
Expr::Binary(b) => {
self.visit_expr(&b.left);
self.visit_expr(&b.right);
}
Expr::Logical(l) => {
self.visit_expr(&l.left);
self.visit_expr(&l.right);
}
Expr::Conditional(c) => {
self.visit_expr(&c.test);
self.visit_expr(&c.consequent);
self.visit_expr(&c.alternate);
}
Expr::Assign(a) => {
match &a.left {
AssignTarget::Pat(p) => self.visit_pat_ref(p),
AssignTarget::Expr(e) => self.visit_expr(e),
}
self.visit_expr(&a.right);
}
Expr::Sequence(s) => {
for e in &s.expressions {
self.visit_expr(e);
}
}
Expr::Member(m) => {
self.visit_expr(&m.object);
if m.is_computed
&& let crate::parser::ast::MemberProp::Computed(e) = &m.property
{
self.visit_expr(e);
}
}
Expr::OptionalMember(m) => {
self.visit_expr(&m.object);
if m.is_computed
&& let crate::parser::ast::MemberProp::Computed(e) = &m.property
{
self.visit_expr(e);
}
}
Expr::Call(c) => {
if let Expr::Ident(id) = c.callee.as_ref()
&& id.name == "eval"
{
self.mark_uses_eval();
}
self.visit_expr(&c.callee);
for arg in &c.arguments {
self.visit_expr(arg);
}
}
Expr::OptionalCall(c) => {
self.visit_expr(&c.callee);
for arg in &c.arguments {
self.visit_expr(arg);
}
}
Expr::OptionalChain(inner) => {
self.visit_expr(inner);
}
Expr::New(n) => {
self.visit_expr(&n.callee);
for arg in &n.arguments {
self.visit_expr(arg);
}
}
Expr::TaggedTemplate(t) => {
self.visit_expr(&t.tag);
for e in &t.quasi.expressions {
self.visit_expr(e);
}
}
Expr::Spread(s) => self.visit_expr(&s.argument),
Expr::Yield(y) => {
if let Some(arg) = &y.argument {
self.visit_expr(arg);
}
}
Expr::Await(a) => self.visit_expr(&a.argument),
Expr::Import(i) => {
self.visit_expr(&i.source);
}
Expr::PrivateName(_) => {}
}
}
fn visit_pat_ref(&mut self, pat: &Pat) {
match pat {
Pat::Ident(id) => self.resolve_ref(&id.name, id.loc),
Pat::Array(a) => {
for el in a.elements.iter().flatten() {
self.visit_pat_ref(el);
}
}
Pat::Object(o) => {
for prop in &o.properties {
match prop {
ObjectPatProp::KeyValue(kv) => self.visit_pat_ref(&kv.value),
ObjectPatProp::Assign(ap) => self.resolve_ref(&ap.key.name, ap.key.loc),
ObjectPatProp::Rest(r) => self.visit_pat_ref(&r.argument),
}
}
}
Pat::Rest(r) => self.visit_pat_ref(&r.argument),
Pat::Assign(a) => {
self.visit_pat_ref(&a.left);
self.visit_expr(&a.right);
}
Pat::Expr(e) => self.visit_expr(e),
}
}
fn declare_param(&mut self, param: &Param) {
let sid = self.current_scope();
self.declare_param_pat(¶m.pat, sid);
if let Some(default) = ¶m.default {
self.visit_expr(default);
}
}
fn declare_param_pat(&mut self, pat: &Pat, scope_id: ScopeId) {
match pat {
Pat::Ident(id) => self.declare(&id.name, BindingKind::Param, id.loc, scope_id),
Pat::Array(a) => {
for el in a.elements.iter().flatten() {
self.declare_param_pat(el, scope_id);
}
}
Pat::Object(o) => {
for prop in &o.properties {
match prop {
ObjectPatProp::KeyValue(kv) => self.declare_param_pat(&kv.value, scope_id),
ObjectPatProp::Assign(ap) => {
self.declare(&ap.key.name, BindingKind::Param, ap.key.loc, scope_id)
}
ObjectPatProp::Rest(r) => self.declare_param_pat(&r.argument, scope_id),
}
}
}
Pat::Rest(r) => self.declare_param_pat(&r.argument, scope_id),
Pat::Assign(a) => self.declare_param_pat(&a.left, scope_id),
Pat::Expr(_) => {} }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::ast::{
BlockStmt, EmptyStmt, FnDecl, Ident, Param, Program, ProgramItem, SourceType, Stmt,
VarDecl, VarDeclarator, VarKind,
};
use crate::parser::scanner::{Position, Span};
fn loc() -> SourceLocation {
Span {
start: Position {
offset: 0,
line: 1,
column: 1,
},
end: Position {
offset: 0,
line: 1,
column: 1,
},
}
}
fn ident(name: &str) -> Ident {
Ident {
loc: loc(),
name: name.to_owned(),
}
}
fn empty_program() -> Program {
Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![],
is_strict: false,
}
}
fn ident_expr(name: &str) -> Expr {
Expr::Ident(ident(name))
}
fn var_decl(kind: VarKind, name: &str) -> VarDecl {
VarDecl {
loc: loc(),
kind,
declarators: vec![VarDeclarator {
loc: loc(),
id: Pat::Ident(ident(name)),
init: None,
}],
}
}
fn var_decl_init(kind: VarKind, name: &str, init: Expr) -> VarDecl {
VarDecl {
loc: loc(),
kind,
declarators: vec![VarDeclarator {
loc: loc(),
id: Pat::Ident(ident(name)),
init: Some(Box::new(init)),
}],
}
}
#[test]
fn test_analyze_empty_script_creates_global_scope() {
let prog = empty_program();
let tree = analyze(&prog);
assert_eq!(tree.scopes.len(), 1);
assert_eq!(tree.scopes[0].kind, ScopeKind::Global);
assert!(tree.errors.is_empty());
}
#[test]
fn test_analyze_module_creates_module_scope() {
let prog = Program {
loc: loc(),
source_type: SourceType::Module,
body: vec![],
is_strict: true,
};
let tree = analyze(&prog);
assert_eq!(tree.scopes[0].kind, ScopeKind::Module);
}
#[test]
fn test_var_hoisted_to_global() {
let prog = Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![ProgramItem::Stmt(Stmt::VarDecl(var_decl(
VarKind::Var,
"x",
)))],
is_strict: false,
};
let tree = analyze(&prog);
assert!(tree.scopes[0].bindings.contains_key("x"));
assert_eq!(tree.scopes[0].bindings["x"].kind, BindingKind::Var);
assert!(tree.errors.is_empty());
}
#[test]
fn test_var_hoisted_out_of_block() {
let block = Stmt::Block(BlockStmt {
loc: loc(),
body: vec![Stmt::VarDecl(var_decl(VarKind::Var, "x"))],
});
let prog = Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![ProgramItem::Stmt(block)],
is_strict: false,
};
let tree = analyze(&prog);
assert!(tree.scopes[0].bindings.contains_key("x"));
assert!(!tree.scopes[1].bindings.contains_key("x"));
assert!(tree.errors.is_empty());
}
#[test]
fn test_function_declaration_hoisted() {
let fn_decl = Stmt::FnDecl(Box::new(FnDecl {
loc: loc(),
id: Some(ident("f")),
is_async: false,
is_generator: false,
params: vec![],
body: BlockStmt {
loc: loc(),
body: vec![],
},
is_strict: false,
}));
let prog = Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![ProgramItem::Stmt(fn_decl)],
is_strict: false,
};
let tree = analyze(&prog);
assert!(tree.scopes[0].bindings.contains_key("f"));
assert_eq!(tree.scopes[0].bindings["f"].kind, BindingKind::Function);
assert!(tree.errors.is_empty());
}
#[test]
fn test_let_in_block_scope() {
let block = Stmt::Block(BlockStmt {
loc: loc(),
body: vec![Stmt::VarDecl(var_decl(VarKind::Let, "y"))],
});
let prog = Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![ProgramItem::Stmt(block)],
is_strict: false,
};
let tree = analyze(&prog);
assert!(!tree.scopes[0].bindings.contains_key("y"));
let block_scope = tree
.scopes
.iter()
.find(|s| s.kind == ScopeKind::Block)
.unwrap();
assert!(block_scope.bindings.contains_key("y"));
assert!(tree.errors.is_empty());
}
#[test]
fn test_duplicate_var_is_ok() {
let prog = Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![
ProgramItem::Stmt(Stmt::VarDecl(var_decl(VarKind::Var, "x"))),
ProgramItem::Stmt(Stmt::VarDecl(var_decl(VarKind::Var, "x"))),
],
is_strict: false,
};
let tree = analyze(&prog);
assert!(
tree.errors.is_empty(),
"var redeclaration should not be an error"
);
}
#[test]
fn test_duplicate_let_is_error() {
let prog = Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![
ProgramItem::Stmt(Stmt::VarDecl(var_decl(VarKind::Let, "x"))),
ProgramItem::Stmt(Stmt::VarDecl(var_decl(VarKind::Let, "x"))),
],
is_strict: false,
};
let tree = analyze(&prog);
assert_eq!(tree.errors.len(), 1);
assert_eq!(tree.errors[0].name, "x");
assert!(matches!(
tree.errors[0].kind,
ScopeErrorKind::DuplicateBinding { .. }
));
}
#[test]
fn test_duplicate_const_is_error() {
let prog = Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![
ProgramItem::Stmt(Stmt::VarDecl(var_decl(VarKind::Const, "C"))),
ProgramItem::Stmt(Stmt::VarDecl(var_decl(VarKind::Const, "C"))),
],
is_strict: false,
};
let tree = analyze(&prog);
assert_eq!(tree.errors.len(), 1);
}
#[test]
fn test_tdz_let_use_before_decl() {
let init_expr = ident_expr("x");
let prog = Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![ProgramItem::Stmt(Stmt::VarDecl(var_decl_init(
VarKind::Let,
"x",
init_expr,
)))],
is_strict: false,
};
let tree = analyze(&prog);
assert!(
tree.errors
.iter()
.any(|e| e.name == "x" && matches!(e.kind, ScopeErrorKind::TdzViolation)),
"expected a TDZ violation for 'x'"
);
}
#[test]
fn test_tdz_no_violation_after_decl() {
use crate::parser::ast::VarDeclarator;
let x_decl = Stmt::VarDecl(VarDecl {
loc: loc(),
kind: VarKind::Let,
declarators: vec![VarDeclarator {
loc: loc(),
id: Pat::Ident(ident("x")),
init: None,
}],
});
let y_decl = Stmt::VarDecl(VarDecl {
loc: loc(),
kind: VarKind::Let,
declarators: vec![VarDeclarator {
loc: loc(),
id: Pat::Ident(ident("y")),
init: Some(Box::new(ident_expr("x"))),
}],
});
let prog = Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![ProgramItem::Stmt(x_decl), ProgramItem::Stmt(y_decl)],
is_strict: false,
};
let tree = analyze(&prog);
assert!(tree.errors.is_empty(), "no TDZ error expected");
}
#[test]
fn test_function_creates_scope_with_params() {
let fn_stmt = Stmt::FnDecl(Box::new(FnDecl {
loc: loc(),
id: Some(ident("f")),
is_async: false,
is_generator: false,
params: vec![Param {
loc: loc(),
pat: Pat::Ident(ident("a")),
default: None,
}],
body: BlockStmt {
loc: loc(),
body: vec![],
},
is_strict: false,
}));
let prog = Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![ProgramItem::Stmt(fn_stmt)],
is_strict: false,
};
let tree = analyze(&prog);
let fn_scope = tree
.scopes
.iter()
.find(|s| s.kind == ScopeKind::Function)
.unwrap();
assert!(fn_scope.bindings.contains_key("a"));
assert_eq!(fn_scope.bindings["a"].kind, BindingKind::Param);
}
#[test]
fn test_closure_captures_outer_var() {
use crate::parser::ast::ReturnStmt;
let inner_fn = Stmt::FnDecl(Box::new(FnDecl {
loc: loc(),
id: Some(ident("inner")),
is_async: false,
is_generator: false,
params: vec![],
body: BlockStmt {
loc: loc(),
body: vec![Stmt::Return(ReturnStmt {
loc: loc(),
argument: Some(Box::new(ident_expr("x"))),
})],
},
is_strict: false,
}));
let outer_fn = Stmt::FnDecl(Box::new(FnDecl {
loc: loc(),
id: Some(ident("outer")),
is_async: false,
is_generator: false,
params: vec![],
body: BlockStmt {
loc: loc(),
body: vec![Stmt::VarDecl(var_decl(VarKind::Var, "x")), inner_fn],
},
is_strict: false,
}));
let prog = Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![ProgramItem::Stmt(outer_fn)],
is_strict: false,
};
let tree = analyze(&prog);
let outer_scope = tree
.scopes
.iter()
.find(|s| s.kind == ScopeKind::Function && s.bindings.contains_key("x"))
.expect("outer function scope not found");
assert!(
outer_scope.captures.contains("x"),
"x should be captured; captures = {:?}",
outer_scope.captures
);
assert!(tree.errors.is_empty());
}
#[test]
fn test_catch_creates_catch_scope() {
use crate::parser::ast::{CatchClause, TryStmt};
let try_stmt = Stmt::Try(TryStmt {
loc: loc(),
block: BlockStmt {
loc: loc(),
body: vec![],
},
handler: Some(CatchClause {
loc: loc(),
param: Some(Pat::Ident(ident("e"))),
body: BlockStmt {
loc: loc(),
body: vec![],
},
}),
finalizer: None,
});
let prog = Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![ProgramItem::Stmt(try_stmt)],
is_strict: false,
};
let tree = analyze(&prog);
let catch_scope = tree
.scopes
.iter()
.find(|s| s.kind == ScopeKind::Catch)
.unwrap();
assert!(catch_scope.bindings.contains_key("e"));
assert!(tree.errors.is_empty());
}
#[test]
fn test_with_creates_with_scope() {
use crate::parser::ast::WithStmt;
let with_stmt = Stmt::With(WithStmt {
loc: loc(),
object: Box::new(ident_expr("obj")),
body: Box::new(Stmt::Empty(EmptyStmt { loc: loc() })),
});
let prog = Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![ProgramItem::Stmt(with_stmt)],
is_strict: false,
};
let tree = analyze(&prog);
assert!(tree.scopes.iter().any(|s| s.kind == ScopeKind::With));
assert!(tree.errors.is_empty());
}
#[test]
fn test_uses_eval_flag() {
use crate::parser::ast::{CallExpr, ExprStmt};
let call = Expr::Call(Box::new(CallExpr {
loc: loc(),
callee: Box::new(ident_expr("eval")),
arguments: vec![Expr::Str(crate::parser::ast::StringLit {
loc: loc(),
value: "1+1".to_owned(),
})],
}));
let prog = Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![ProgramItem::Stmt(Stmt::Expr(ExprStmt {
loc: loc(),
expr: Box::new(call),
}))],
is_strict: false,
};
let tree = analyze(&prog);
assert!(tree.scopes[0].uses_eval);
}
#[test]
fn test_uses_this_flag() {
use crate::parser::ast::{ExprStmt, ThisExpr};
let prog = Program {
loc: loc(),
source_type: SourceType::Script,
body: vec![ProgramItem::Stmt(Stmt::Expr(ExprStmt {
loc: loc(),
expr: Box::new(Expr::This(ThisExpr { loc: loc() })),
}))],
is_strict: false,
};
let tree = analyze(&prog);
assert!(tree.scopes[0].uses_this);
}
#[test]
fn test_scope_kind_is_function_boundary() {
assert!(ScopeKind::Global.is_function_boundary());
assert!(ScopeKind::Function.is_function_boundary());
assert!(ScopeKind::Module.is_function_boundary());
assert!(ScopeKind::Eval.is_function_boundary());
assert!(!ScopeKind::Block.is_function_boundary());
assert!(!ScopeKind::Catch.is_function_boundary());
assert!(!ScopeKind::With.is_function_boundary());
}
#[test]
fn test_binding_kind_has_tdz() {
assert!(BindingKind::Let.has_tdz());
assert!(BindingKind::Const.has_tdz());
assert!(BindingKind::Class.has_tdz());
assert!(!BindingKind::Var.has_tdz());
assert!(!BindingKind::Function.has_tdz());
assert!(!BindingKind::Param.has_tdz());
assert!(!BindingKind::Import.has_tdz());
}
}