diff --git a/core/ast/src/function/ordinary_function.rs b/core/ast/src/function/ordinary_function.rs index 76701afabe8..43add223a47 100644 --- a/core/ast/src/function/ordinary_function.rs +++ b/core/ast/src/function/ordinary_function.rs @@ -5,7 +5,9 @@ use crate::{ join_nodes, operations::{ContainsSymbol, contains}, scope::{FunctionScopes, Scope}, - scope_analyzer::{analyze_binding_escapes, collect_bindings}, + scope_analyzer::{ + analyze_binding_escapes, collect_bindings, optimize_scope_indices_with_root_function_scope, + }, visitor::{VisitWith, Visitor, VisitorMut}, }; use boa_interner::{Interner, ToIndentedString}; @@ -279,9 +281,25 @@ impl FunctionExpression { strict: bool, scope: &Scope, interner: &Interner, + ) -> Result<(), &'static str> { + self.analyze_scope_with_options(strict, scope, interner, false) + } + + /// Analyze the scope of the function expression, with extra scope analysis options. + /// + /// # Errors + /// Any scope or binding errors that happened during the analysis. + pub fn analyze_scope_with_options( + &mut self, + strict: bool, + scope: &Scope, + interner: &Interner, + force_function_scope: bool, ) -> Result<(), &'static str> { collect_bindings(self, strict, false, scope, interner)?; - analyze_binding_escapes(self, false, scope.clone(), interner) + analyze_binding_escapes(self, false, scope.clone(), interner)?; + optimize_scope_indices_with_root_function_scope(self, scope, force_function_scope); + Ok(()) } } diff --git a/core/ast/src/scope_analyzer.rs b/core/ast/src/scope_analyzer.rs index 56875874ae9..69a58beed55 100644 --- a/core/ast/src/scope_analyzer.rs +++ b/core/ast/src/scope_analyzer.rs @@ -1223,15 +1223,31 @@ impl BindingCollectorVisitor<'_> { pub(crate) fn optimize_scope_indices<'a, N>(node: &'a mut N, scope: &Scope) where &'a mut N: Into>, +{ + optimize_scope_indices_with_root_function_scope(node, scope, false); +} + +/// Optimize scope indices when scopes only contain local bindings, optionally forcing the +/// root function-like scope to be treated as requiring a function environment. +pub(crate) fn optimize_scope_indices_with_root_function_scope<'a, N>( + node: &'a mut N, + scope: &Scope, + force_root_function_scope: bool, +) where + &'a mut N: Into>, { let mut visitor = ScopeIndexVisitor { index: scope.scope_index(), + force_root_function_scope, + root_function_scope_forced: false, }; let _ = visitor.visit(node.into()); } struct ScopeIndexVisitor { index: u32, + force_root_function_scope: bool, + root_function_scope_forced: bool, } impl<'ast> VisitorMut<'ast> for ScopeIndexVisitor { @@ -1658,6 +1674,14 @@ impl ScopeIndexVisitor { scope.set_index(self.index); } + let force_function_scope = + if !arrow && self.force_root_function_scope && !self.root_function_scope_forced { + self.root_function_scope_forced = true; + true + } else { + force_function_scope + }; + if force_function_scope || !scopes.function_scope().all_bindings_local() { scopes.requires_function_scope = true; self.index += 1; diff --git a/core/engine/src/builtins/function/mod.rs b/core/engine/src/builtins/function/mod.rs index 45e9c79232c..6b6791a6de9 100644 --- a/core/engine/src/builtins/function/mod.rs +++ b/core/engine/src/builtins/function/mod.rs @@ -647,9 +647,12 @@ impl BuiltInFunctionObject { false, Span::new(function_span_start, function_span_end), ); - if let Err(reason) = - function.analyze_scope(strict, context.realm().scope(), context.interner()) - { + if let Err(reason) = function.analyze_scope_with_options( + strict, + context.realm().scope(), + context.interner(), + true, + ) { return Err(js_error!(SyntaxError: "failed to analyze function scope: {}", reason)); } diff --git a/core/engine/src/builtins/function/tests.rs b/core/engine/src/builtins/function/tests.rs index 7200a610e3c..80292e65339 100644 --- a/core/engine/src/builtins/function/tests.rs +++ b/core/engine/src/builtins/function/tests.rs @@ -194,3 +194,39 @@ fn function_constructor_early_errors_super() { ), ]); } + +#[test] +fn function_constructor_nested_scope_indices_regression() { + run_test_actions([TestAction::assert_eq( + indoc! {r#" + (() => { + const compiledCode = ` + const noop = () => {} + function Counter() { + const [v, setV] = [0, noop] + + const onClick = () => { + setV(v => v + 1) + } + + return null + } + return Counter() + `; + + const React = {}; + const f = new Function("React", compiledCode); + return f(React); + })() + "#}, + JsValue::null(), + )]); +} + +#[test] +fn function_constructor_nested_lexical_capture() { + run_test_actions([TestAction::assert_eq( + "Function(\"function f(){ const x = 1; return () => x; } return f()()\")()", + 1, + )]); +}