-
Notifications
You must be signed in to change notification settings - Fork 9
feat(transform): rewrite .map function closures to pass in any closed over cells #1948
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(transform): rewrite .map function closures to pass in any closed over cells #1948
Conversation
f6611d1 to
64bb4c3
Compare
- Created closure transform rule using TypeScript's symbol table - Added closure rule to transformer pipeline (runs before JSX) - Added closures directory to fixture test configuration - Created initial map callback transformation logic - Moved closure code to separate src/closures directory for better organization Note: Tests are failing due to dataflow issues, will fix after rebase
Current issue: collectCaptures is capturing wrong variables - Capturing individual identifiers instead of full expressions - Need to properly handle property access like state.discount - Tests show we're capturing 'span', 'price', 'state', 'discount' separately when we should only capture 'state.discount' as a unit
…e by including the first string description parameter
…tures accordingly. still one failing test
This test verifies that when both the array and index in an element access expression (foo[bar]) are OpaqueRefs (cells), the transformation correctly wraps them in a derive call with both as inputs. The dataflow analysis correctly handles this by: - Analyzing both the target and argument expressions - Merging dataflows from both sides - Setting requiresRewrite to true - Creating a derive call with both OpaqueRefs as inputs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This test documents the expected behavior when:
- Mapping over state.sortedTags (OpaqueRef array)
- Accessing state.tagCounts[element] inside the callback
- With the fix from fix/opaqueref-map-parameters-are-opaque-ref,
element is now an OpaqueRef in the callback context
- tagCounts is also an OpaqueRef (captured as opaque param)
The test currently FAILS because tagCounts[element] is not being
wrapped in derive. Once the transformation is fixed to handle
element access where both object and index are OpaqueRefs in the
map callback context, this test will pass.
Expected transformation:
tagCounts[element] -> derive({ tagCounts, element }, (v1, v2) => v1[v2])
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes a bug where element access expressions like `tagCounts[element]`
inside map callbacks weren't being wrapped in derive() calls. The issue
occurred because these expressions are synthetic (created by the
ClosureTransformer) and the dataflow analyzer wasn't properly handling
synthetic element access with dynamic opaque indices.
Changes:
- Extract isStaticElementAccess() helper to share logic between synthetic
and non-synthetic code paths
- Add proper handling for synthetic element access expressions:
- Static indices (e.g., element[0]) preserve merged analysis
- Dynamic indices with opaque refs (e.g., tagCounts[element]) set
requiresRewrite=true to trigger derive wrapper
- Update test expectations to use original variable names instead of
_v1, _v2 (matches pattern in other test fixtures)
- Fix HTML-encoded directive in test input file
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Problem: When the ClosureTransformer creates synthetic nodes for map callbacks (e.g., `tagCounts[element]` inside a recipe callback), the dataflow analysis was marking them as `requiresRewrite: false`, preventing the OpaqueRefJSXTransformer from wrapping them in derive calls. Root Cause: In dataflow.ts, synthetic element access expressions were falling through to the default case (line 483-487) which sets `requiresRewrite: false`. Solution: Added special handling for synthetic element access expressions (lines 470-477 in dataflow.ts) to match the behavior of non-synthetic element access expressions, setting `requiresRewrite: true` when they contain OpaqueRefs. Also simplified filterRelevantDataFlows in helpers.ts to keep all dataflows except those originating from ignored parameters, since both builder parameters and map parameters are now OpaqueRefs that need to be tracked. Test Changes: - Updated map-element-access-opaque.expected.tsx with correct parameter naming - Updated map-array-destructured.expected.tsx to wrap element[0] and element[1] - Fixed element-access-both-opaque input/expected files to use correct directive 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
64bb4c3 to
35344da
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
9 issues found across 97 files
Prompt for AI agents (all 9 issues)
Understand the root cause of the following 9 issues and fix them.
<file name="packages/ts-transformers/src/ast/utils.ts">
<violation number="1" location="packages/ts-transformers/src/ast/utils.ts:205">
`visitEachChildWithJsx` is invoked with `undefined`, so this call into `ts.visitEachChild` will throw when `context` is missing. Default the context (e.g. `context ?? ts.nullTransformationContext`) before calling.</violation>
</file>
<file name="packages/ts-transformers/test/fixtures/closures/map-conditional-expression.expected.tsx">
<violation number="1" location="packages/ts-transformers/test/fixtures/closures/map-conditional-expression.expected.tsx:86">
Wrap the computed price in JSX braces instead of a template literal so the value renders correctly.</violation>
</file>
<file name="packages/ts-transformers/test/fixtures/closures/map-element-access-opaque.input.tsx">
<violation number="1" location="packages/ts-transformers/test/fixtures/closures/map-element-access-opaque.input.tsx:14">
Add a stable key to each <span> returned from sortedTags.map so React can reconcile the list correctly.</violation>
</file>
<file name="packages/ts-transformers/src/closures/transformer.ts">
<violation number="1" location="packages/ts-transformers/src/closures/transformer.ts:1006">
The rewritten map callback drops the original third parameter (the source array). If a callback uses that argument, the transformed version has no binding for it, so references like `array.some(...)` crash at runtime. Please preserve the third parameter when rebuilding the destructured signature.</violation>
</file>
<file name="packages/ts-transformers/test/fixtures/closures/map-import-reference.input.tsx">
<violation number="1" location="packages/ts-transformers/test/fixtures/closures/map-import-reference.input.tsx:24">
Add a stable key to each element returned from state.items.map so React can reconcile the list correctly.</violation>
</file>
<file name="packages/ts-transformers/test/fixtures/jsx-expressions/opaque-ref-cell-map.expected.tsx">
<violation number="1" location="packages/ts-transformers/test/fixtures/jsx-expressions/opaque-ref-cell-map.expected.tsx:119">
The goToCharm handler expects a `charm` property, but the new callback now calls it with `{ element }`, so the handler receives `undefined` and navigation will fail. Use the correct property name.</violation>
</file>
<file name="packages/ts-transformers/test/fixtures/closures/map-multiple-captures.input.tsx">
<violation number="1" location="packages/ts-transformers/test/fixtures/closures/map-multiple-captures.input.tsx:24">
Elements produced from state.items.map need a stable key to avoid React reconciliation issues; please add a key to the <span>.</violation>
</file>
<file name="packages/schema-generator/test/fixtures/schema/wrapper-aliases.input.ts">
<violation number="1" location="packages/schema-generator/test/fixtures/schema/wrapper-aliases.input.ts:3">
Dropping the `extends T` constraint makes this test fixture diverge from the actual Default wrapper and lets defaults that aren’t assignable to T slip through; please keep the constraint so the schema test stays faithful to production types.</violation>
</file>
<file name="packages/ts-transformers/src/ast/dataflow.ts">
<violation number="1" location="packages/ts-transformers/src/ast/dataflow.ts:995">
JSX attribute expressions are no longer analyzed because JsxAttribute/JsxSpreadAttribute are not Expressions, so the guard always fails; their initializers should be inspected instead.</violation>
</file>
React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.
|
|
||
| // Handle JSX self-closing elements | ||
| if (ts.isJsxSelfClosingElement(node)) { | ||
| return ts.visitEachChild(node, visitor, context); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
visitEachChildWithJsx is invoked with undefined, so this call into ts.visitEachChild will throw when context is missing. Default the context (e.g. context ?? ts.nullTransformationContext) before calling.
Prompt for AI agents
Address the following comment on packages/ts-transformers/src/ast/utils.ts at line 205:
<comment>`visitEachChildWithJsx` is invoked with `undefined`, so this call into `ts.visitEachChild` will throw when `context` is missing. Default the context (e.g. `context ?? ts.nullTransformationContext`) before calling.</comment>
<file context>
@@ -135,3 +165,66 @@ export function isFunctionParameter(
+
+ // Handle JSX self-closing elements
+ if (ts.isJsxSelfClosingElement(node)) {
+ return ts.visitEachChild(node, visitor, context);
+ }
+
</file context>
| return ts.visitEachChild(node, visitor, context); | |
| return ts.visitEachChild(node, visitor, context ?? ts.nullTransformationContext); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mathpirate there are indeed a lot of places that do call this with undefined, but nullTransformationContext doesn't exist and i can't tell whether we should fix the call sites to pass context along or create one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Context can be undefined here
packages/ts-transformers/test/fixtures/closures/map-conditional-expression.expected.tsx
Show resolved
Hide resolved
packages/ts-transformers/test/fixtures/closures/map-element-access-opaque.input.tsx
Show resolved
Hide resolved
packages/ts-transformers/test/fixtures/closures/map-import-reference.input.tsx
Show resolved
Hide resolved
packages/ts-transformers/test/fixtures/jsx-expressions/opaque-ref-cell-map.expected.tsx
Outdated
Show resolved
Hide resolved
packages/ts-transformers/test/fixtures/closures/map-multiple-captures.input.tsx
Show resolved
Hide resolved
| // Test wrapper type aliases - both direct and aliased wrapper types | ||
| // Using proper interface definitions that match actual CommonTools wrappers | ||
| type Default<T, V extends T = T> = T; | ||
| type Default<T, V = T> = T; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dropping the extends T constraint makes this test fixture diverge from the actual Default wrapper and lets defaults that aren’t assignable to T slip through; please keep the constraint so the schema test stays faithful to production types.
Prompt for AI agents
Address the following comment on packages/schema-generator/test/fixtures/schema/wrapper-aliases.input.ts at line 3:
<comment>Dropping the `extends T` constraint makes this test fixture diverge from the actual Default wrapper and lets defaults that aren’t assignable to T slip through; please keep the constraint so the schema test stays faithful to production types.</comment>
<file context>
@@ -1,6 +1,6 @@
// Test wrapper type aliases - both direct and aliased wrapper types
// Using proper interface definitions that match actual CommonTools wrappers
-type Default<T, V extends T = T> = T;
+type Default<T, V = T> = T;
interface Cell<T> {
get(): T;
</file context>
| type Default<T, V = T> = T; | |
| type Default<T, V extends T = T> = T; |
| const analyses: InternalAnalysis[] = []; | ||
| // Analyze opening element attributes | ||
| if (expression.openingElement.attributes) { | ||
| expression.openingElement.attributes.properties.forEach((attr) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSX attribute expressions are no longer analyzed because JsxAttribute/JsxSpreadAttribute are not Expressions, so the guard always fails; their initializers should be inspected instead.
Prompt for AI agents
Address the following comment on packages/ts-transformers/src/ast/dataflow.ts at line 995:
<comment>JSX attribute expressions are no longer analyzed because JsxAttribute/JsxSpreadAttribute are not Expressions, so the guard always fails; their initializers should be inspected instead.</comment>
<file context>
@@ -668,6 +987,31 @@ export function createDataFlowAnalyzer(
+ const analyses: InternalAnalysis[] = [];
+ // Analyze opening element attributes
+ if (expression.openingElement.attributes) {
+ expression.openingElement.attributes.properties.forEach((attr) => {
+ if (ts.isExpression(attr)) {
+ analyses.push(analyzeExpression(attr, scope, context));
</file context>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mathpirate Can you take a look?
…tDataFlows Removed unused functions that were no longer needed after simplifying the dataflow filtering logic: - resolvesToMapParameter - resolvesToBuilderParameter - resolvesToParameterOfKind - isFunctionParameter import These were only used in the old filtering logic that distinguished between different parameter types. The new simplified approach treats all parameters uniformly except for ignored ones. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
The rewritten map callback was dropping the original third parameter (the source array). When a callback uses that argument, the transformed version had no binding for it, causing runtime crashes when code references array.some(...), array.length, etc. Changes: - Added transformArrayReferences function to rename array parameter uses - Updated buildCallbackParamTypeNode to include optional array property - Updated createRecipeCallWithParams to include array in destructured binding - Updated transformMapCallback to extract and transform array parameter - Added test fixture to verify array parameter is preserved 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
The transformation was incorrectly renaming property names in object
shorthand notation. When transforming { charm } where 'charm' is the
original parameter name, it was being converted to { element } instead
of { charm: element }, causing handlers to receive undefined values.
Changes:
- Created generic transformParameterReferences helper to reduce duplication
- Added handling for ShorthandPropertyAssignment nodes
- Converts shorthand to regular property assignments when renaming
(e.g., { charm } becomes { charm: element })
- Updated test fixture to expect correct property names
- Applied fix to element, index, and array parameter transformations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Resubmitting #1864 by @mathpirate , after applying a fix to support
foo[bar]inside map function where both are cells.Summary by cubic
Transforms reactive array .map callbacks to pass closed-over cells as explicit params, ensuring correct reactivity and schema generation. Adds mapWithPattern and unifies the map builtin; fixes element access cases like foo[bar] where both sides are cells.
New Features
Bug Fixes