Skip to content

Conversation

@seefeldb
Copy link
Contributor

@seefeldb seefeldb commented Oct 23, 2025

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

    • ClosureTransformer rewrites OpaqueRef/Cell .map(...) to mapWithPattern(recipe, params); runs first in the pipeline and handles nested/typed/destructured params.
    • API/Runner: adds OpaqueRefMethods.mapWithPattern; map builtin now supports both legacy and closure modes based on params.
    • Schema: supports Opaque unions (T | OpaqueRef), synthetic TypeNode schema generation, and exposes a transformer object with generateSchema and generateSchemaFromSyntheticTypeNode.
    • Tests/Docs: extensive closure fixtures; design and roadmap docs added.
  • Bug Fixes

    • Correctly wraps element access like tagCounts[element] in derive when both sides are OpaqueRefs, including inside transformed map callbacks.
    • Improved handling of synthetic nodes and JSX traversal in dataflow and emitters; map chains (e.g., filter().map()) rewrite reliably.

@seefeldb seefeldb requested a review from mathpirate October 23, 2025 16:12
@seefeldb seefeldb force-pushed the claude/investigate-jsx-ast-transform-011CUNeoHwc4qon275nrw3z5 branch from f6611d1 to 64bb4c3 Compare October 23, 2025 16:19
- 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
mathpirate and others added 11 commits October 23, 2025 09:20
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>
@seefeldb seefeldb force-pushed the claude/investigate-jsx-ast-transform-011CUNeoHwc4qon275nrw3z5 branch from 64bb4c3 to 35344da Compare October 23, 2025 16:21
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.

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 &lt;span&gt; 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 &lt;span&gt;.</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);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 23, 2025

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>
Suggested change
return ts.visitEachChild(node, visitor, context);
return ts.visitEachChild(node, visitor, context ?? ts.nullTransformationContext);
Fix with Cubic

Copy link
Contributor Author

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.

Copy link
Collaborator

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

// 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;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 23, 2025

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&lt;T, V extends T = T&gt; = T;
+type Default&lt;T, V = T&gt; = T;
 interface Cell&lt;T&gt; {
   get(): T;
</file context>
Suggested change
type Default<T, V = T> = T;
type Default<T, V extends T = T> = T;
Fix with Cubic

const analyses: InternalAnalysis[] = [];
// Analyze opening element attributes
if (expression.openingElement.attributes) {
expression.openingElement.attributes.properties.forEach((attr) => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 23, 2025

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) =&gt; {
+          if (ts.isExpression(attr)) {
+            analyses.push(analyzeExpression(attr, scope, context));
</file context>
Fix with Cubic

Copy link
Contributor Author

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>
@seefeldb seefeldb merged commit 7ec536e into main Oct 23, 2025
8 checks passed
@seefeldb seefeldb deleted the claude/investigate-jsx-ast-transform-011CUNeoHwc4qon275nrw3z5 branch October 23, 2025 17:41
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.

5 participants