fix(codegen): gate String-method dispatch on string receiver — user method like internals.trim(v,s) (#5271)#5272
Conversation
internals.trim(value, schema) on a non-string receiver was mis-lowered to String.prototype.trim and failed compile on arity. Thread receiver-type info so String-method fast-path only fires for string receivers; object/any route to dynamic method dispatch.
📝 WalkthroughWalkthroughFixes issue ChangesString.prototype fast-path fix for non-string receivers
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/perry-codegen/src/stmt/let_stmt.rs (1)
66-79:⚠️ Potential issue | 🟠 Major | ⚡ Quick winObject-literal receiver tracking is currently too narrow.
ctx.object_literal_localsis only updated inside the!mutablebranch and only for direct object-literal-shaped initializers. That misses common shapes likelet o = { ... }and aliasing (const b = a), so a laterb.trim()/o.split(...)can still be claimed by the String fast-path when arity matches.Suggested patch shape
- if !mutable { - if let Some(init_expr) = init { - ... - if is_object_literal_init(init_expr) { - ctx.object_literal_locals.insert(id); - } - } - } + if let Some(init_expr) = init { + if !mutable { + if let Some(props) = crate::lower_call::extract_options_fields(ctx, init_expr) { + ctx.option_object_locals.insert(id, props); + } + } + if is_object_literal_init(init_expr) { + ctx.object_literal_locals.insert(id); + } else if let perry_hir::Expr::LocalGet(src_id) = init_expr { + if ctx.object_literal_locals.contains(src_id) { + ctx.object_literal_locals.insert(id); + } + } + }🤖 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-codegen/src/stmt/let_stmt.rs` around lines 66 - 79, The object-literal receiver tracking in the let_stmt.rs file is too restrictive because it only updates ctx.object_literal_locals inside the !mutable branch. This misses cases where mutable bindings (like let o = { ... }) should also track object-literal locals, causing later method calls on those variables to be incorrectly claimed by the String fast-path. Move the object-literal tracking logic (the is_object_literal_init check and ctx.object_literal_locals.insert call) outside of the !mutable condition so that both mutable and immutable variable declarations with object-literal initializers are properly tracked, ensuring that method calls like o.trim() or b.split() resolve to the object's own method rather than the static String-method fast path.
🧹 Nitpick comments (1)
crates/perry/tests/issue_5271_string_method_nonstring_receiver.rs (1)
78-133: ⚡ Quick winAdd regressions for mutable and inline object-literal receivers.
Current tests are strong, but they don’t pin
let o = { ... }; o.trim()or inline method-bearing object literals (IIFE-lowered shape), which are the remaining collision-prone receiver forms.🤖 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/tests/issue_5271_string_method_nonstring_receiver.rs` around lines 78 - 133, Add two new regression test functions following the same pattern as the existing tests in this file. First, add a test for mutable object-literal receivers (using let o = { trim() { ... } }; o.trim() style) to verify that user-defined methods on mutable object variables resolve correctly and don't incorrectly dispatch to String builtins. Second, add a test for inline object-literal receivers passed directly or in IIFE-lowered forms (like ({ trim() { ... } }).trim()) to verify the same collision-avoidance behavior. Both tests should use compile_and_run and assert that user-defined methods are called, not the String builtins, to ensure these edge-case receiver forms don't regress.
🤖 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-codegen/src/lower_call/property_get.rs`:
- Around line 459-466: The receiver_is_object_literal check in the condition
block is incomplete because it only detects LocalGet references to object
literal locals and direct Object expressions, but misses IIFE-lowered object
literals. Extend the receiver_is_object_literal guard to also detect the
object-building IIFE pattern (a Call expression with a Function body that
returns an object). This will prevent incorrectly routing such patterns to the
String fast-path when they should not be treated as simple object literals.
---
Outside diff comments:
In `@crates/perry-codegen/src/stmt/let_stmt.rs`:
- Around line 66-79: The object-literal receiver tracking in the let_stmt.rs
file is too restrictive because it only updates ctx.object_literal_locals inside
the !mutable branch. This misses cases where mutable bindings (like let o = {
... }) should also track object-literal locals, causing later method calls on
those variables to be incorrectly claimed by the String fast-path. Move the
object-literal tracking logic (the is_object_literal_init check and
ctx.object_literal_locals.insert call) outside of the !mutable condition so that
both mutable and immutable variable declarations with object-literal
initializers are properly tracked, ensuring that method calls like o.trim() or
b.split() resolve to the object's own method rather than the static
String-method fast path.
---
Nitpick comments:
In `@crates/perry/tests/issue_5271_string_method_nonstring_receiver.rs`:
- Around line 78-133: Add two new regression test functions following the same
pattern as the existing tests in this file. First, add a test for mutable
object-literal receivers (using let o = { trim() { ... } }; o.trim() style) to
verify that user-defined methods on mutable object variables resolve correctly
and don't incorrectly dispatch to String builtins. Second, add a test for inline
object-literal receivers passed directly or in IIFE-lowered forms (like ({
trim() { ... } }).trim()) to verify the same collision-avoidance behavior. Both
tests should use compile_and_run and assert that user-defined methods are
called, not the String builtins, to ensure these edge-case receiver forms don't
regress.
🪄 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: c10c3d9a-e786-4745-a3fc-47f287b8c1db
📒 Files selected for processing (8)
crates/perry-codegen/src/codegen/closure.rscrates/perry-codegen/src/codegen/entry.rscrates/perry-codegen/src/codegen/function.rscrates/perry-codegen/src/codegen/method.rscrates/perry-codegen/src/expr/mod.rscrates/perry-codegen/src/lower_call/property_get.rscrates/perry-codegen/src/stmt/let_stmt.rscrates/perry/tests/issue_5271_string_method_nonstring_receiver.rs
| let receiver_is_object_literal = matches!( | ||
| &**object, | ||
| Expr::LocalGet(id) if ctx.object_literal_locals.contains(id) | ||
| ) || matches!(&**object, Expr::Object(_)); | ||
| if is_string_only_method | ||
| && string_only_method_arity_ok(property, args.len()) | ||
| && !receiver_is_object_literal | ||
| && !is_array_expr(ctx, object) |
There was a problem hiding this comment.
Receiver-shape guard misses IIFE-lowered object literals.
receiver_is_object_literal does not include the object-building IIFE shape, so inline method-bearing literals can still hit the String fast-path for matching-arity builtin names.
Suggested guard extension
let receiver_is_object_literal = matches!(
&**object,
Expr::LocalGet(id) if ctx.object_literal_locals.contains(id)
-) || matches!(&**object, Expr::Object(_));
+) || matches!(&**object, Expr::Object(_))
+ || matches!(
+ &**object,
+ Expr::Call { callee, args, .. }
+ if matches!(args.first(), Some(Expr::Object(_)))
+ && matches!(
+ callee.as_ref(),
+ Expr::Closure { params, .. }
+ if params.first().map_or(false, |p| p.name == "__perry_obj_iife")
+ )
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let receiver_is_object_literal = matches!( | |
| &**object, | |
| Expr::LocalGet(id) if ctx.object_literal_locals.contains(id) | |
| ) || matches!(&**object, Expr::Object(_)); | |
| if is_string_only_method | |
| && string_only_method_arity_ok(property, args.len()) | |
| && !receiver_is_object_literal | |
| && !is_array_expr(ctx, object) | |
| let receiver_is_object_literal = matches!( | |
| &**object, | |
| Expr::LocalGet(id) if ctx.object_literal_locals.contains(id) | |
| ) || matches!(&**object, Expr::Object(_)) | |
| || matches!( | |
| &**object, | |
| Expr::Call { callee, args, .. } | |
| if matches!(args.first(), Some(Expr::Object(_))) | |
| && matches!( | |
| callee.as_ref(), | |
| Expr::Closure { params, .. } | |
| if params.first().map_or(false, |p| p.name == "__perry_obj_iife") | |
| ) | |
| ); | |
| if is_string_only_method | |
| && string_only_method_arity_ok(property, args.len()) | |
| && !receiver_is_object_literal | |
| && !is_array_expr(ctx, object) |
🤖 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-codegen/src/lower_call/property_get.rs` around lines 459 - 466,
The receiver_is_object_literal check in the condition block is incomplete
because it only detects LocalGet references to object literal locals and direct
Object expressions, but misses IIFE-lowered object literals. Extend the
receiver_is_object_literal guard to also detect the object-building IIFE pattern
(a Call expression with a Function body that returns an object). This will
prevent incorrectly routing such patterns to the String fast-path when they
should not be treated as simple object literals.
Fixes #5271.
internals.trim(value, schema)(a user method on a non-string receiver) was mis-lowered toString.prototype.trimand failed the compile on arity (String.trim takes no args, got 2), blocking joi.Fix: added an arity-aware gate (
string_only_method_arity_ok) inlower_call/property_get.rs— a builtin-named method on a receiver that is NOT provably a string, whose arg count can't match the String builtin's signature, falls through to normal dynamic method dispatch instead of forcingString.prototype.<m>. Genuine string receivers still fast-path.Verified:
internals.trim("a","b")→a:b; string regressions (" hi ".trim(),split,toLowerCase,repeat,padStart) match Node; new testissue_5271_string_method_nonstring_receiver. joi's String.trim error is gone (it now advances to a separate regex-incompatibility wall — Rustregexcrate lacks lookahead).No version/CHANGELOG/Cargo.lock edits (for maintainer to fold in at merge).
Summary by CodeRabbit
String.prototypebuiltins (e.g.,trim,split,slice). User-defined methods are now correctly called instead of incorrectly attempting String method dispatch on non-string receivers.