Skip to content
Closed
Show file tree
Hide file tree
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
4 changes: 4 additions & 0 deletions changelog.d/10993-inline-class-iterator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fixed runtime iteration of an inline anonymous class expression constructed
with `new (class { ... })()`. Computed methods such as generator
`[Symbol.iterator]` are now registered before the instance is constructed,
matching ordinary class expressions.
27 changes: 18 additions & 9 deletions crates/perry-hir/src/lower/expr_new/non_ident.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use anyhow::Result;
use swc_ecma_ast as ast;

use crate::ir::Expr;
use crate::lower_decl::lower_class_from_ast;
use crate::lower_decl::{lower_class_from_ast, prepare_ordered_class_computed_names};
use crate::lower_types::extract_ts_type_with_ctx;

use super::super::{lower_expr, LoweringContext};
Expand Down Expand Up @@ -165,6 +165,12 @@ pub(crate) fn lower_new_non_ident(
// link instead and needs no registration — which is why the user-parent
// form of this shape already worked.
let parent_expr = class.extends_expr.clone();
// This arm bypasses `lower_class_expr`, which normally evaluates and
// registers computed member keys at class-definition time. Without
// that prelude, an inline `new (class { *[Symbol.iterator]() {} })()`
// constructs an instance before its iterator method is registered.
let (computed_name_evaluations, _) =
prepare_ordered_class_computed_names(&class_expr.class.body, &class, &synthetic_name);
ctx.pending_classes.push(class);
let mut args: Vec<Expr> = new_expr
.args
Expand Down Expand Up @@ -217,16 +223,19 @@ pub(crate) fn lower_new_non_ident(
// The `Sequence` yields its LAST element, so the `new` site still sees
// the constructed instance — the registration is pure side effect,
// ordered before it.
let Some(parent_expr) = parent_expr else {
return Ok(construct);
};
return Ok(Expr::Sequence(vec![
Expr::RegisterClassParentDynamic {
let mut definition_steps = Vec::new();
if let Some(parent_expr) = parent_expr {
definition_steps.push(Expr::RegisterClassParentDynamic {
class_name: synthetic_name,
parent_expr,
},
construct,
]));
});
}
definition_steps.extend(computed_name_evaluations);
if definition_steps.is_empty() {
return Ok(construct);
}
definition_steps.push(construct);
return Ok(Expr::Sequence(definition_steps));
}

let callee = Box::new(lower_expr(ctx, callee_expr)?);
Expand Down
6 changes: 3 additions & 3 deletions crates/perry/tests/issue_5128_user_symbol_iterator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ console.log(it.next().value, it.next().value, it.next().done);
);
}

/// The same fix must apply to class *expressions* — `lower_class_from_ast`
/// mirrors `lower_class_decl`, so `new (class { *[Symbol.iterator]() {…} })()`
/// and a named class-expression binding are iterable for every runtime consumer.
/// The same fix must apply to class *expressions*. A direct `new (class
/// { *[Symbol.iterator]() {…} })()` uses a separate lowering arm that must
/// register its computed method before constructing the instance (#10839).
#[test]
fn class_expression_generator_symbol_iterator_is_iterable() {
let dir = tempfile::tempdir().expect("tempdir");
Expand Down
Loading