Skip to content
Merged
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
1 change: 1 addition & 0 deletions crates/perry-hir/src/lower/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ impl LoweringContext {
fn_ctor_env: super::fn_ctor_env::FnCtorEnv::default(),
expr_lower_depth: 0,
prelowered_member_receiver: None,
in_nonarrow_fn: false,
}
}

Expand Down
8 changes: 8 additions & 0 deletions crates/perry-hir/src/lower/eval_super_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,14 @@ pub(crate) fn check_direct_eval_super_private(
"'arguments' is not allowed in class field initializer",
));
}
// 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",
));
}
Comment on lines +186 to +193

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.

check_private_refs(&scan, |name| {
ctx.private_scopes
.iter()
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-hir/src/lower/expr_function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,8 @@ fn lower_fn_expr_anon(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> Resul
// in a class field initializer. Cleared here, restored at the end.
let saved_field_init = ctx.in_class_field_init;
ctx.in_class_field_init = false;
let saved_in_nonarrow_fn = ctx.in_nonarrow_fn;
ctx.in_nonarrow_fn = true;

// Lower parameters and collect destructuring info.
//
Expand Down Expand Up @@ -1217,6 +1219,7 @@ fn lower_fn_expr_anon(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> Resul
ctx.exit_strict_mode();
ctx.exit_scope(scope_mark);
ctx.in_class_field_init = saved_field_init;
ctx.in_nonarrow_fn = saved_in_nonarrow_fn;

// Scope popped: `ctx.locals.id_set()` is now the enclosing scope's locals.
let (captures, mutable_captures) =
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-hir/src/lower/expr_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ fn lower_method_prop(
.collect();

let scope_mark = ctx.enter_scope();
let saved_in_nonarrow_fn = ctx.in_nonarrow_fn;
ctx.in_nonarrow_fn = true;
// Object-literal methods are NOT implicitly strict (unlike class bodies):
// strictness is inherited from the enclosing code or introduced by the
// method's own directive prologue. A blanket `true` here made every
Expand Down Expand Up @@ -343,6 +345,7 @@ fn lower_method_prop(
}
ctx.exit_strict_mode();
ctx.exit_scope(scope_mark);
ctx.in_nonarrow_fn = saved_in_nonarrow_fn;

// Capture analysis (same pattern as arrow/function expressions)
let mut all_refs = Vec::new();
Expand Down Expand Up @@ -465,6 +468,8 @@ fn lower_accessor_prop(
.collect();

let scope_mark = ctx.enter_scope();
let saved_in_nonarrow_fn = ctx.in_nonarrow_fn;
ctx.in_nonarrow_fn = true;
Comment on lines +471 to +472

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.

// Accessors in object literals inherit strictness (see lower_method_prop).
let accessor_strict = ctx.current_strict_mode()
|| body
Expand Down Expand Up @@ -508,6 +513,7 @@ fn lower_accessor_prop(
};
ctx.exit_strict_mode();
ctx.exit_scope(scope_mark);
ctx.in_nonarrow_fn = saved_in_nonarrow_fn;

// Capture analysis — identical pattern to `lower_method_prop`.
let mut all_refs = Vec::new();
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-hir/src/lower/lowering_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -783,4 +783,12 @@ pub struct LoweringContext {
/// idempotent w.r.t. the value produced (the fluent-success path already
/// reuses the same lowered receiver), so reusing it is semantics-preserving.
pub(crate) prelowered_member_receiver: Option<((u32, u32), Expr)>,
/// True when the eval call site is lexically inside a non-arrow function
/// (ordinary `function` declaration/expression, method, constructor, getter,
/// setter, or static block). Used by `check_direct_eval_super_private` to
/// enforce the spec rule: `new.target` in a direct eval body is only legal
/// when the eval is contained in function code that is NOT an ArrowFunction
/// (ES2025 §15.2.1.1, early error for `new.target` in eval). ArrowFunction
/// bodies and module/script top-level both leave this false.
pub(crate) in_nonarrow_fn: bool,
}
3 changes: 3 additions & 0 deletions crates/perry-hir/src/lower_decl/body_stmt/nested_fn_decl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ pub(super) fn lower_nested_fn_decl(
};

let scope_mark = ctx.enter_scope();
let saved_in_nonarrow_fn = ctx.in_nonarrow_fn;
ctx.in_nonarrow_fn = true;

// Lower parameters. Skip the TypeScript `this:` annotation —
// it has no runtime existence (see the sibling site above for
Expand Down Expand Up @@ -194,6 +196,7 @@ pub(super) fn lower_nested_fn_decl(
}

ctx.exit_scope(scope_mark);
ctx.in_nonarrow_fn = saved_in_nonarrow_fn;

// Detect captured variables
let mut all_refs = Vec::new();
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-hir/src/lower_decl/class_decl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1045,8 +1045,11 @@ pub fn lower_class_decl(
// such method right after static field init, so they
// run once at module startup.
let scope_mark = ctx.enter_scope();
let saved_in_nonarrow_fn = ctx.in_nonarrow_fn;
ctx.in_nonarrow_fn = true;
let body = lower_block_stmt(ctx, &block.body)?;
ctx.exit_scope(scope_mark);
ctx.in_nonarrow_fn = saved_in_nonarrow_fn;

let block_idx = static_methods
.iter()
Expand Down Expand Up @@ -1814,8 +1817,11 @@ pub fn lower_class_from_ast(
}
ast::ClassMember::StaticBlock(block) => {
let scope_mark = ctx.enter_scope();
let saved_in_nonarrow_fn = ctx.in_nonarrow_fn;
ctx.in_nonarrow_fn = true;
let body = lower_block_stmt(ctx, &block.body)?;
ctx.exit_scope(scope_mark);
ctx.in_nonarrow_fn = saved_in_nonarrow_fn;

let block_idx = static_methods
.iter()
Expand Down
12 changes: 12 additions & 0 deletions crates/perry-hir/src/lower_decl/class_members.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub fn lower_constructor(
ctor: &ast::Constructor,
) -> Result<Function> {
let scope_mark = ctx.enter_scope();
let saved_in_nonarrow_fn = ctx.in_nonarrow_fn;
ctx.in_nonarrow_fn = true;
ctx.enter_strict_mode(true);

// Track that we're inside a constructor body so `new.target` can resolve
Expand Down Expand Up @@ -210,6 +212,7 @@ pub fn lower_constructor(

ctx.exit_strict_mode();
ctx.exit_scope(scope_mark);
ctx.in_nonarrow_fn = saved_in_nonarrow_fn;
ctx.in_constructor_class = saved_ctor_class;

Ok(Function {
Expand Down Expand Up @@ -469,6 +472,8 @@ pub fn lower_class_method_with_name(
ctx.enter_type_param_scope(&type_params);

let scope_mark = ctx.enter_scope();
let saved_in_nonarrow_fn = ctx.in_nonarrow_fn;
ctx.in_nonarrow_fn = true;
ctx.enter_strict_mode(true);

// Add 'this' for instance methods
Expand Down Expand Up @@ -641,6 +646,7 @@ pub fn lower_class_method_with_name(

ctx.exit_strict_mode();
ctx.exit_scope(scope_mark);
ctx.in_nonarrow_fn = saved_in_nonarrow_fn;

// Exit method's type param scope
ctx.exit_type_param_scope();
Expand Down Expand Up @@ -710,6 +716,8 @@ pub fn lower_getter_method_with_name(
name: String,
) -> Result<Function> {
let scope_mark = ctx.enter_scope();
let saved_in_nonarrow_fn = ctx.in_nonarrow_fn;
ctx.in_nonarrow_fn = true;
ctx.enter_strict_mode(true);

// Add 'this' for instance getters
Expand Down Expand Up @@ -747,6 +755,7 @@ pub fn lower_getter_method_with_name(

ctx.exit_strict_mode();
ctx.exit_scope(scope_mark);
ctx.in_nonarrow_fn = saved_in_nonarrow_fn;

Ok(Function {
id: ctx.fresh_func(),
Expand Down Expand Up @@ -788,6 +797,8 @@ pub fn lower_setter_method_with_name(
name: String,
) -> Result<Function> {
let scope_mark = ctx.enter_scope();
let saved_in_nonarrow_fn = ctx.in_nonarrow_fn;
ctx.in_nonarrow_fn = true;
ctx.enter_strict_mode(true);

// Add 'this' for instance setters
Expand Down Expand Up @@ -870,6 +881,7 @@ pub fn lower_setter_method_with_name(

ctx.exit_strict_mode();
ctx.exit_scope(scope_mark);
ctx.in_nonarrow_fn = saved_in_nonarrow_fn;

Ok(Function {
id: ctx.fresh_func(),
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-hir/src/lower_decl/fn_decl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ pub fn lower_fn_decl(ctx: &mut LoweringContext, fn_decl: &ast::FnDecl) -> Result
ctx.enter_type_param_scope(&type_params);

let scope_mark = ctx.enter_scope();
let saved_in_nonarrow_fn = ctx.in_nonarrow_fn;
ctx.in_nonarrow_fn = true;

// Pre-scan body for `arguments` references. If the function references
// `arguments`, we synthesize a hidden raw-arguments parameter so
Expand Down Expand Up @@ -372,6 +374,7 @@ pub fn lower_fn_decl(ctx: &mut LoweringContext, fn_decl: &ast::FnDecl) -> Result

ctx.exit_strict_mode();
ctx.exit_scope(scope_mark);
ctx.in_nonarrow_fn = saved_in_nonarrow_fn;

// Exit type parameter scope
ctx.exit_type_param_scope();
Expand Down
9 changes: 9 additions & 0 deletions crates/perry-hir/src/lower_decl/private_members.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ pub fn lower_private_method(
ctx.enter_type_param_scope(&type_params);

let scope_mark = ctx.enter_scope();
let saved_in_nonarrow_fn = ctx.in_nonarrow_fn;
ctx.in_nonarrow_fn = true;
ctx.enter_strict_mode(true);

// Add 'this' for instance methods
Expand Down Expand Up @@ -190,6 +192,7 @@ pub fn lower_private_method(

ctx.exit_strict_mode();
ctx.exit_scope(scope_mark);
ctx.in_nonarrow_fn = saved_in_nonarrow_fn;
ctx.exit_type_param_scope();

let func_id = ctx.fresh_func();
Expand Down Expand Up @@ -232,6 +235,8 @@ pub fn lower_private_getter(
) -> Result<Function> {
let name = format!("get_#{}", method.key.name);
let scope_mark = ctx.enter_scope();
let saved_in_nonarrow_fn = ctx.in_nonarrow_fn;
ctx.in_nonarrow_fn = true;
ctx.enter_strict_mode(true);
ctx.define_local("this".to_string(), Type::Any);

Expand All @@ -250,6 +255,7 @@ pub fn lower_private_getter(

ctx.exit_strict_mode();
ctx.exit_scope(scope_mark);
ctx.in_nonarrow_fn = saved_in_nonarrow_fn;

Ok(Function {
id: ctx.fresh_func(),
Expand All @@ -276,6 +282,8 @@ pub fn lower_private_setter(
) -> Result<Function> {
let name = format!("set_#{}", method.key.name);
let scope_mark = ctx.enter_scope();
let saved_in_nonarrow_fn = ctx.in_nonarrow_fn;
ctx.in_nonarrow_fn = true;
ctx.enter_strict_mode(true);
ctx.define_local("this".to_string(), Type::Any);

Expand Down Expand Up @@ -326,6 +334,7 @@ pub fn lower_private_setter(

ctx.exit_strict_mode();
ctx.exit_scope(scope_mark);
ctx.in_nonarrow_fn = saved_in_nonarrow_fn;

Ok(Function {
id: ctx.fresh_func(),
Expand Down
Loading