Skip to content

Conversation

@seefeldb
Copy link
Contributor

@seefeldb seefeldb commented Oct 24, 2025

Summary

This PR fixes two related issues with map transformations in JSX expressions:

  1. Prevents .mapWithPattern() errors when map is inside derive: Stops transforming .map() to .mapWithPattern() when the map will be wrapped in a derive(), since the array gets unwrapped inside the derive and plain .map() is needed.

  2. Fixes incorrect parameter capture in outer derives: Prevents untransformed map callback parameters from being incorrectly captured in outer derive calls.

Problem

When a map is nested in an expression with opaque refs (e.g., list.length > 0 && list.map(...)), the OpaqueRefJSXTransformer wraps the entire expression in derive(list, list => ...). This causes two issues:

  1. If ClosureTransformer had already converted .map() to .mapWithPattern(), this causes runtime errors because unwrapped arrays don't have mapWithPattern()
  2. Map callback parameters were incorrectly captured in the outer derive as dependencies (e.g., item.name from inside the map callback)

Solution

Fix 1: Skip map transformation when inside derive

  • Added shouldTransformMap() function in closures/transformer.ts that uses dataflow analysis
  • Detects when a map's containing JSX expression will be wrapped in derive
  • Checks for analysis.requiresRewrite && !skip-call-rewrite hint to distinguish derive wrapping from other transformations
  • Skips transformation in these cases, keeping plain .map() which works on unwrapped arrays

Fix 2: Exclude untransformed map parameters from outer scope

  • Modified getOpaqueCallKindForParameter() in transformers/opaque-ref/helpers.ts
  • Now checks context.isMapCallback() to determine if a callback was actually transformed
  • Only treats parameters as opaque if the callback was transformed to mapWithPattern
  • Untransformed callbacks (plain .map() inside derives) have regular parameters that shouldn't be captured

Example

Input:

{list.length > 0 && list.map((item) => <div>{item.name}</div>)}

Before (broken):

{derive({ list, item_name: item.name }, ...) => ...}  // ❌ item doesn't exist yet!
{list.mapWithPattern(...)}  // ❌ mapWithPattern doesn't exist on unwrapped arrays!

After (fixed):

{derive({ list }, ({ list }) => list.length > 0 && (
  <div>{list.map((item) => <div>{item.name}</div>)}</div>
))}  // ✅ Plain .map() works on unwrapped array, no incorrect captures

Test Plan

  • Added new test fixture map-array-length-conditional for the specific use case
  • Updated existing test fixtures to reflect correct behavior
  • All 96 test steps pass

🤖 Generated with Claude Code


Summary by cubic

Prevents transforming .map() to .mapWithPattern() when the map is inside a derive-wrapped JSX expression, avoiding runtime errors on plain arrays. Also stops untransformed map callback parameters from being captured by outer derive dependencies.

  • Bug Fixes
    • Use dataflow analysis to detect derive wrapping and skip the mapWithPattern rewrite when needed.
    • Treat map callback parameters as opaque only if the callback was transformed; plain .map() callbacks are not captured.

seefeldb and others added 3 commits October 24, 2025 13:02
When a map is nested in an expression with opaque refs (e.g.,
`list.length > 0 && list.map(...)`), the OpaqueRefJSXTransformer wraps
the entire expression in `derive(list, list => ...)`, which unwraps the
array to a plain array. If ClosureTransformer had already transformed
the map to `mapWithPattern`, this causes runtime errors since plain
arrays don't have `mapWithPattern`.

This fix adds `shouldTransformMap()` which uses dataflow analysis to
detect when a map will be inside a derive and skips the transformation,
keeping it as plain `.map()` which works correctly on unwrapped arrays.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…capture

When a plain .map() call is inside an expression that gets wrapped in derive(),
the map callback parameters should not be treated as opaque parameters for the
purposes of the outer derive. This is because:

1. The map is NOT being transformed to mapWithPattern (stays as plain .map)
2. The callback runs on unwrapped array elements inside the derive
3. The callback parameters are local to that scope, not outer scope variables

This fix modifies getOpaqueCallKindForParameter() to check if a callback was
actually transformed (using context.isMapCallback) before treating its parameters
as opaque. Untransformed callbacks have regular parameters that should not be
captured in outer derives.

Example that's now fixed:
```tsx
{items.map((item) => <div>{item.name && <span>{item.name}</span>}</div>)}
```
Before: derive({ items, item_name: item.name }, ...)  ❌
After: derive({ items }, ...)  ✅

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 7 files

@seefeldb seefeldb merged commit 36d60c5 into main Oct 24, 2025
9 checks passed
@seefeldb seefeldb deleted the berni/fix-map-in-derive branch October 24, 2025 20:59
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