Skip to content

eval: SyntaxError for new.target in direct eval outside non-arrow function (#5579)#5766

Merged
proggeramlug merged 1 commit into
mainfrom
fix/eval-new-target-syntax-error
Jun 28, 2026
Merged

eval: SyntaxError for new.target in direct eval outside non-arrow function (#5579)#5766
proggeramlug merged 1 commit into
mainfrom
fix/eval-new-target-syntax-error

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Root cause

ES2025 §15.2.1.1 states that new.target in a direct eval body is a
SyntaxError unless the eval is contained in function code that is not an
ArrowFunction. Perry's const-fold path (try_const_fold_eval) folded
eval("new.target") into a completion IIFE without checking this rule, so at
global scope or inside an arrow the folded code returned undefined instead of
throwing.

check_indirect_eval_super_private already rejected new.target unconditionally
(indirect eval is always global code). check_direct_eval_super_private had no
corresponding check — this PR closes that gap.

What changes

New field in_nonarrow_fn: bool on LoweringContext — tracks whether the
current lowering site is lexically inside a non-arrow function (ordinary function
declaration/expression, method, constructor, getter, setter, or static block).
ArrowFunction bodies and module/script top-level leave the field false.

Save/restore at every non-arrow function entry point:

  • lower_fn_decl (fn_decl.rs)
  • lower_fn_expr_anon (expr_function.rs)
  • lower_nested_fn_decl (body_stmt/nested_fn_decl.rs)
  • lower_constructor, lower_class_method_with_name, lower_getter_method_with_name, lower_setter_method_with_name (class_members.rs)
  • lower_private_method, lower_private_getter, lower_private_setter (private_members.rs)
  • StaticBlock arms in class_decl.rs (both lowering paths)
  • lower_method_prop, lower_accessor_prop (expr_object.rs)

New check in check_direct_eval_super_private (eval_super_scan.rs):

if scan.new_target && !ctx.in_nonarrow_fn {
    return Some(throw_eval_syntax_error_expr(
        "new.target expression is not allowed here",
    ));
}

Before / after (test262 language/eval-code/direct, tracked in #5579)

Test Before After
direct/new.target.js runtime-fail (returns undefined, expected SyntaxError) pass
direct/new.target-arrow.js runtime-fail pass

Net: −2 failures from the language/eval-code cluster in issue #5579.

Verification

  • cargo build --release -p perry-hir — clean (no new errors)
  • cargo fmt --all -- --check — clean
  • bash scripts/check_file_size.sh — clean (all files ≤2000 lines)

Closes two cases from the eval-code failure cluster tracked in #5579.


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of new.target in evaluated code, preventing invalid usage in places where it isn’t allowed.
    • Fixed context tracking during lowering so function-like bodies and class members use the correct execution context consistently.
    • Restored the previous context after lowering, avoiding state leaking between nested declarations or class members.

…ction

ES2025 §15.2.1.1 makes `new.target` in a direct eval body a SyntaxError
unless the eval is contained in function code that is not an ArrowFunction.
Perry's const-fold path folded eval("new.target") into a completion IIFE
without checking this rule, so at global scope or inside an arrow it returned
undefined instead of throwing.

Fix: add `in_nonarrow_fn: bool` to `LoweringContext`, set it to `true` at
every non-arrow function boundary (fn declarations, fn expressions, methods,
constructors, getters, setters, private members, static blocks, object-literal
methods/accessors), and check it in `check_direct_eval_super_private` — the
same pattern already used by `check_indirect_eval_super_private` for its
unconditional new.target rejection.

Fixes two test262 cases tracked in #5579:
  language/eval-code/direct/new.target.js
  language/eval-code/direct/new.target-arrow.js
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a in_nonarrow_fn: bool field to LoweringContext to track whether lowering is inside a non-arrow function. The flag is saved and set to true on entry, then restored on exit, across all non-arrow function lowering paths. Direct eval then uses this flag to emit a runtime SyntaxError when new.target is detected inside eval outside a non-arrow function context.

in_nonarrow_fn tracking and new.target eval check

Layer / File(s) Summary
LoweringContext field and initialization
crates/perry-hir/src/lower/lowering_context.rs, crates/perry-hir/src/lower/context.rs
in_nonarrow_fn: bool field added to LoweringContext with doc comments; initialized to false in with_class_id_start.
new.target legality check in direct eval
crates/perry-hir/src/lower/eval_super_scan.rs
check_direct_eval_super_private returns a js_throw_eval_syntax_error for new.target when ctx.in_nonarrow_fn is false.
Scoping in_nonarrow_fn across all non-arrow function lowering
crates/perry-hir/src/lower/expr_function.rs, crates/perry-hir/src/lower/expr_object.rs, crates/perry-hir/src/lower_decl/fn_decl.rs, crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs, crates/perry-hir/src/lower_decl/class_members.rs, crates/perry-hir/src/lower_decl/class_decl.rs, crates/perry-hir/src/lower_decl/private_members.rs
Each non-arrow function lowering path (function decls, nested fn decls, anonymous fn expressions, object method/accessor props, class constructors, class methods/getters/setters, private members, static blocks) saves, sets to true, and restores ctx.in_nonarrow_fn.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • PerryTS/perry#5110: Directly related — introduces __perry_static_init_ static block synthesis during class lowering, the same class static block lowering paths where ctx.in_nonarrow_fn scoping is now added.

Poem

🐇 A flag hops in, then hops back out,
in_nonarrow_fn knows what it's about.
new.target in eval? Not allowed here!
The context is tracked with precision and care.
Each function scope saved, restored with a bound —
The rabbit ensures spec rules are sound! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main fix and the affected eval/new.target behavior.
Description check ✅ Passed The description covers root cause, concrete changes, testing, and linked issue, so it is mostly complete despite not following the template headings exactly.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/eval-new-target-syntax-error

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with 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.

Inline comments:
In `@crates/perry-hir/src/lower/eval_super_scan.rs`:
- Around line 186-193: The setter bailout in expr_object lowering is leaving
ctx.in_nonarrow_fn in the wrong state because the early Ok(None) path skips
restoring the saved value. Update the setter-handling flow in expr_object::lower
to always restore saved_in_nonarrow_fn before returning, including the malformed
setter-parameter bailout, so later eval/new.target checks in eval_super_scan are
not misclassified.

In `@crates/perry-hir/src/lower/expr_object.rs`:
- Around line 471-472: The early bailout in the object-expression lowering path
leaves ctx.in_nonarrow_fn set to true, which leaks the non-arrow function
context into the caller. Update the logic around the saved_in_nonarrow_fn
assignment and the Ok(None) return path so ctx.in_nonarrow_fn is always restored
before exiting, just as strict mode and scope are unwound. Make sure the
restoration happens in the expr_object lowering flow regardless of whether the
function exits normally or via the bailout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 824cee42-6eba-4829-a615-924029d1e4de

📥 Commits

Reviewing files that changed from the base of the PR and between 69eaa23 and d48116e.

📒 Files selected for processing (10)
  • crates/perry-hir/src/lower/context.rs
  • crates/perry-hir/src/lower/eval_super_scan.rs
  • crates/perry-hir/src/lower/expr_function.rs
  • crates/perry-hir/src/lower/expr_object.rs
  • crates/perry-hir/src/lower/lowering_context.rs
  • crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs
  • crates/perry-hir/src/lower_decl/class_decl.rs
  • crates/perry-hir/src/lower_decl/class_members.rs
  • crates/perry-hir/src/lower_decl/fn_decl.rs
  • crates/perry-hir/src/lower_decl/private_members.rs

Comment on lines +186 to +193
// ES2025 §15.2.1.1: `new.target` in a direct eval body is only legal when
// the eval is contained in function code that is NOT an ArrowFunction.
// At module/script top-level and inside arrow functions, it is a SyntaxError.
if scan.new_target && !ctx.in_nonarrow_fn {
return Some(throw_eval_syntax_error_expr(
"new.target expression is not allowed here",
));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect every in_nonarrow_fn save site and confirm a matching restore exists on
# all exit paths, with focus on non-error early returns (return Ok(None) / return Ok(_)).
rg -nP --type=rust -C8 'ctx\.in_nonarrow_fn\s*=\s*true' crates/perry-hir/src

echo '--- early returns between save and restore in expr_object.rs ---'
rg -nP --type=rust -C3 'return Ok\(None\)|exit_scope\(scope_mark\)|in_nonarrow_fn' crates/perry-hir/src/lower/expr_object.rs

Repository: PerryTS/perry

Length of output: 24930


🏁 Script executed:

#!/bin/bash
sed -n '450,525p' crates/perry-hir/src/lower/expr_object.rs
printf '\n--- caller context ---\n'
rg -n -C4 'lower_accessor_prop\(' crates/perry-hir/src

Repository: PerryTS/perry

Length of output: 5820


Restore in_nonarrow_fn on the early Ok(None) return In crates/perry-hir/src/lower/expr_object.rs:485-490, the setter bailout skips ctx.in_nonarrow_fn = saved_in_nonarrow_fn;, so a malformed setter param can leak true into later lowering and misclassify subsequent new.target/eval checks.

🤖 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/eval_super_scan.rs` around lines 186 - 193, The
setter bailout in expr_object lowering is leaving ctx.in_nonarrow_fn in the
wrong state because the early Ok(None) path skips restoring the saved value.
Update the setter-handling flow in expr_object::lower to always restore
saved_in_nonarrow_fn before returning, including the malformed setter-parameter
bailout, so later eval/new.target checks in eval_super_scan are not
misclassified.

Comment on lines +471 to +472
let saved_in_nonarrow_fn = ctx.in_nonarrow_fn;
ctx.in_nonarrow_fn = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore ctx.in_nonarrow_fn on the early Ok(None) path.

The bailout at Line 486 unwinds strict mode and scope, but leaves ctx.in_nonarrow_fn = true. That leaks non-arrow context into the caller, so a later direct eval("new.target") in the enclosing scope can be accepted when it should throw.

Suggested fix
             Err(_) => {
                 ctx.exit_strict_mode();
                 ctx.exit_scope(scope_mark);
+                ctx.in_nonarrow_fn = saved_in_nonarrow_fn;
                 return Ok(None);
             }

Also applies to: 486-489

🤖 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/expr_object.rs` around lines 471 - 472, The early
bailout in the object-expression lowering path leaves ctx.in_nonarrow_fn set to
true, which leaks the non-arrow function context into the caller. Update the
logic around the saved_in_nonarrow_fn assignment and the Ok(None) return path so
ctx.in_nonarrow_fn is always restored before exiting, just as strict mode and
scope are unwound. Make sure the restoration happens in the expr_object lowering
flow regardless of whether the function exits normally or via the bailout.

@proggeramlug proggeramlug merged commit 1d83578 into main Jun 28, 2026
15 checks passed
@proggeramlug proggeramlug deleted the fix/eval-new-target-syntax-error branch June 28, 2026 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants