Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 81 additions & 4 deletions crates/perry-hir/src/lower_decl/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -632,9 +632,26 @@ fn annexb_nested_stmt(
ast::Stmt::DoWhile(do_while) => {
annexb_nested_stmt(&do_while.body, forbidden, all_out, var_out)
}
ast::Stmt::For(for_stmt) => annexb_nested_stmt(&for_stmt.body, forbidden, all_out, var_out),
ast::Stmt::ForIn(for_in) => annexb_nested_stmt(&for_in.body, forbidden, all_out, var_out),
ast::Stmt::ForOf(for_of) => annexb_nested_stmt(&for_of.body, forbidden, all_out, var_out),
// A `for`/`for-in`/`for-of` lexical head (`for (let f; ...)`,
// `for (let f in/of ...)`) introduces a binding whose scope encloses
// the loop body; an equivalent `var f` in the body is an early error
// (14.7.4.1 / 14.7.5.1), so the AnnexB legacy `var` for a same-named
// block function in the body must be skipped.
ast::Stmt::For(for_stmt) => {
let names = match &for_stmt.init {
Some(ast::VarDeclOrExpr::VarDecl(vd)) => var_decl_lexical_names(vd),
_ => Vec::new(),
};
annexb_nested_loop_body(&for_stmt.body, names, forbidden, all_out, var_out);
}
ast::Stmt::ForIn(for_in) => {
let names = for_head_lexical_names(&for_in.left);
annexb_nested_loop_body(&for_in.body, names, forbidden, all_out, var_out);
}
ast::Stmt::ForOf(for_of) => {
let names = for_head_lexical_names(&for_of.left);
annexb_nested_loop_body(&for_of.body, names, forbidden, all_out, var_out);
}
ast::Stmt::Labeled(labeled) => {
annexb_nested_stmt(&labeled.body, forbidden, all_out, var_out)
}
Expand All @@ -654,7 +671,25 @@ fn annexb_nested_stmt(
ast::Stmt::Try(try_stmt) => {
annexb_nested_block(&try_stmt.block.stmts, forbidden, all_out, var_out);
if let Some(handler) = &try_stmt.handler {
annexb_nested_block(&handler.body.stmts, forbidden, all_out, var_out);
// B.3.5: a `var` whose name is also a bound name of a
// *destructuring* CatchParameter is an early error, so the
// equivalent AnnexB legacy `var` for a same-named block
// function in the handler body must be skipped. The B.3.5
// exception only exempts a simple `catch (e)` BindingIdentifier
// (where the var IS allowed), so only pattern catch params
// (`catch ({ f })` / `catch ([f])`) contribute to `forbidden`.
let mut handler_forbidden;
let inner = match &handler.param {
Some(param) if !matches!(param, ast::Pat::Ident(_)) => {
handler_forbidden = forbidden.clone();
let mut names = Vec::new();
collect_var_binding_names_from_pat(param, &mut names);
handler_forbidden.extend(names);
&handler_forbidden
}
_ => forbidden,
};
annexb_nested_block(&handler.body.stmts, inner, all_out, var_out);
}
if let Some(finalizer) = &try_stmt.finalizer {
annexb_nested_block(&finalizer.stmts, forbidden, all_out, var_out);
Expand All @@ -667,6 +702,48 @@ fn annexb_nested_stmt(
}
}

/// Lexical (`let`/`const`) binding names introduced by a `VarDecl`. A `var`
/// declaration introduces no lexical names and yields an empty list.
fn var_decl_lexical_names(vd: &ast::VarDecl) -> Vec<String> {
if vd.kind == ast::VarDeclKind::Var {
return Vec::new();
}
let mut names = Vec::new();
for decl in &vd.decls {
collect_var_binding_names_from_pat(&decl.name, &mut names);
}
names
}

/// Lexical binding names of a `for-in` / `for-of` head (`for (let f in …)`).
/// A `var` head or a bare assignment-target pattern introduces no lexical
/// binding here and yields an empty list.
fn for_head_lexical_names(head: &ast::ForHead) -> Vec<String> {
match head {
ast::ForHead::VarDecl(vd) => var_decl_lexical_names(vd),
_ => Vec::new(),
}
}
Comment on lines +718 to +726

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if ForHead enum has a UsingDecl variant in swc_ecma_ast
rg -n 'enum ForHead' --type rust -A 10 | head -30

# Also check if there are any for-of/for-in tests involving `using` in the test suite
rg -n 'for.*using.*of' --type rust -C 2 | head -30

Repository: PerryTS/perry

Length of output: 999


🏁 Script executed:

# Check the imports in block.rs to identify what 'ast' refers to
head -50 crates/perry-hir/src/lower_decl/block.rs | grep -E '^use|^extern'

Repository: PerryTS/perry

Length of output: 314


🏁 Script executed:

# Find where ForHead is defined (could be swc_ecma_ast or local)
fd -t f '*.rs' | xargs rg 'enum ForHead' -B 2 -A 15

Repository: PerryTS/perry

Length of output: 1945


🏁 Script executed:

# Check if UsingDecl appears anywhere in ForHead-related code
rg 'UsingDecl' crates/perry-hir/src/ -B 2 -A 2

Repository: PerryTS/perry

Length of output: 483


🏁 Script executed:

# Read the full for_head_lexical_names function and surrounding context in block.rs
sed -n '705,745p' crates/perry-hir/src/lower_decl/block.rs

Repository: PerryTS/perry

Length of output: 1546


🏁 Script executed:

# Check how var_decl_lexical_names is defined
sed -n '685,720p' crates/perry-hir/src/lower_decl/block.rs

Repository: PerryTS/perry

Length of output: 1408


🏁 Script executed:

# Check the fn_ctor_env.rs to see full UsingDecl handling for comparison
rg -n 'UsingDecl' crates/perry-hir/src/lower/fn_ctor_env.rs -B 5 -A 10

Repository: PerryTS/perry

Length of output: 565


Handle ForHead::UsingDecl in for_head_lexical_names.

The function currently returns an empty list for any ForHead variant other than VarDecl, including UsingDecl. However, using declarations in for-of/for-in loops introduce lexically scoped bindings (per the explicit resource management proposal), similar to let and const. These bindings should be extracted and added to the forbidden set to correctly apply AnnexB legacy var restrictions.

Compare with fn_ctor_env.rs (line 957–961), which explicitly handles UsingDecl by iterating through its declarations. The fix should extract binding names from UsingDecl in the same way.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-hir/src/lower_decl/block.rs` around lines 718 - 726, The
`for_head_lexical_names` function currently only handles `ast::ForHead::VarDecl`
and returns an empty vector for all other variants, including `UsingDecl`.
However, `using` declarations in for-of/for-in loops also introduce lexically
scoped bindings that should be extracted. Add a new match arm for
`ast::ForHead::UsingDecl` that extracts the binding names from the using
declaration by iterating through its declarations (following the same pattern
used in `fn_ctor_env.rs` at lines 957-961). Return the collected binding names
instead of returning an empty vector for this variant.


/// Descend into a loop body, adding the loop head's lexical binding names to
/// the forbidden set so a same-named block function in the body skips its
/// AnnexB legacy `var` (the equivalent `var` would be an early error).
fn annexb_nested_loop_body(
body: &ast::Stmt,
lexical_names: Vec<String>,
forbidden: &std::collections::HashSet<String>,
all_out: &mut Vec<String>,
var_out: &mut Vec<String>,
) {
if lexical_names.is_empty() {
annexb_nested_stmt(body, forbidden, all_out, var_out);
} else {
let mut inner = forbidden.clone();
inner.extend(lexical_names);
annexb_nested_stmt(body, &inner, all_out, var_out);
}
}

fn annexb_nested_block(
stmts: &[ast::Stmt],
forbidden: &std::collections::HashSet<String>,
Expand Down
Loading