Repository navigation
[NOJIRA] fix: support expressions in action name attributes (babel) - #1487
Open
marco-saia-datadog wants to merge 5 commits into
Open
marco-saia-datadog wants to merge 5 commits into
marco-saia-datadog wants to merge 5 commits into
Conversation
|
✅ All CI checks and tests passed. 🎉 All green!🧪 All tests passed 🔗 Commit SHA: bb5632e | Docs | View more details | Give us feedback! |
marco-saia-datadog
force-pushed
the
marco.saia/fix-babel-plugin-action-name-expressions
branch
from
October 12, 2026 08:41
5ae0758 to
c18f2de
Compare
`dd-action-name`, `accessibilityLabel` and the custom `actionNameAttribute`
were only collected when written as string literal attributes, so values
like `testID={TestIDs.button}` or `accessibilityLabel={t('save')}` were
silently dropped and the action name fell back to content.
Expressions are now forwarded and evaluated at press time inside the
generated wrapper, as long as all their bindings are visible from where
the wrapper runs. Memoized handlers (`useCallback`/`useMemo`) get a
separate args object that only keeps values visible from the component
body, which also fixes a `ReferenceError` when content candidates or
children referenced `.map()` params.
At runtime, empty or unresolved values are skipped when resolving the
target name instead of producing `"undefined"`.
marco-saia-datadog
force-pushed
the
marco.saia/fix-babel-plugin-action-name-expressions
branch
from
October 12, 2026 09:33
c18f2de to
5657daf
Compare
marco-saia-datadog
marked this pull request as ready for review
October 12, 2026 10:18
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Expression-local bindings are rejected and runtime filtering can corrupt compound-component name indexing.
2 open findings
What changed in this PR
Adds runtime support for expression-valued action names while preventing out-of-scope expressions in memoized handlers.
Changes:
- Collects and scope-checks JSX expressions.
- Separates inline and hoisted tracking values.
- Improves runtime fallback and adds regression tests.
| File | Description |
|---|---|
packages/react-native-babel-plugin/test/actionNameAttribute.test.ts |
Tests expression-valued attributes and memoized handlers. |
packages/react-native-babel-plugin/src/utils/nodeProcessing.ts |
Adds expression scope validation. |
packages/react-native-babel-plugin/src/actions/rum/tap.ts |
Uses scope-safe values for memoized handlers. |
packages/react-native-babel-plugin/src/actions/rum/index.ts |
Collects expressions and builds scoped content resolvers. |
packages/core/src/rum/instrumentation/interactionTracking/DdBabelInteractionTracking.ts |
Validates runtime naming values. |
packages/core/src/rum/instrumentation/interactionTracking/__tests__/DdBabelInteractionTracking.test.ts |
Tests naming fallback behavior. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Runtime: resolve the indexed value per naming source instead of filtering
the arrays first, so compound component positions are preserved. An
unresolved value at the index falls through to the next source.
- Plugin: `isExpressionInScope` ignores bindings declared within the
expression itself (e.g. callback params), so values like
`testID={items.map(item => item.id).join(',')}` and children such as
`{items.map(i => <Text>{i.name}</Text>)}` are no longer rejected.
`jsxChildToRuntimeCall` only converts JSX it reaches directly. JSX nested in
callbacks (e.g. `{items.map(i => <Text>{i.name}</Text>)}`), non-JSX ternary
branches or attribute values was cloned as-is. Inline, the JSX transform
still picked it up, but code hoisted into `useCallback`/`useMemo` is never
revisited, so raw JSX ended up in the bundle and broke it.
Run a post-pass over getContent children and candidates that replaces any
remaining JSX with jsx()/jsxs() runtime calls.
- Expressions that are not forwarded now leave a `void 0` placeholder instead of being skipped, so compound component positions indexed by the handler argument don't shift onto the wrong value. - `useMemo` factories run during render at the hook call, so component bindings declared after it are still in their temporal dead zone. A separate args object for `useMemo` only keeps expressions whose bindings are program-scoped (imports, top-level declarations).
- Program scope only was too strict for `useMemo` factories: params and bindings declared before the hook are initialized when the factory runs. `isExpressionInScope` now takes an optional position and accepts program-scope bindings, params, function declarations, and declarations that end before it. The position is the earliest `useMemo` call used by the element's handlers. - `useMemo` gets its own `getContent` resolver with the same check, so content candidates or children referencing bindings declared after the hook no longer throw during render.
Comment on lines
+764
to
+770
| const expression = data.expression; | ||
|
|
||
| const isInline = isExpressionInScope(exprPath, path); | ||
| const getValue = (isVisible: boolean) => | ||
| isVisible | ||
| ? t.cloneNode(expression, true) | ||
| : t.unaryExpression('void', t.numericLiteral(0)); |
Comment on lines
+118
to
+126
| if (typeof exportsObj.render === 'function') { | ||
| exportsObj.render(); | ||
| } | ||
|
|
||
| for (const props of rendered) { | ||
| props.onPress?.(); | ||
| } | ||
|
|
||
| return { output, captured, contentErrors }; |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Note
Fixes issue #1486
Summary
The Babel plugin previously only recognized
dd-action-name,accessibilityLabel, and customactionNameAttributevalues when provided as string literals. Expressions inside JSX braces were ignored, causing action names to fall back to the accessibility label, content, or component name.This PR adds support for expression-valued attributes and fixes a scope-related
ReferenceErrorin memoized handlers.Supported values
testID="save"testID={"save"},testID={`save`}testID={SAVE_ID}(module constant)testID={TestIDs.save}(imported)accessibilityLabel={t('save')}Implementation
Expressions are evaluated at runtime, not resolved by Babel. The plugin clones the expression into the generated wrapper, where it is evaluated when the element is pressed. This follows the existing behavior for content candidates such as
titleandlabel.Expressions are forwarded only when all referenced bindings are accessible from the generated wrapper (
isExpressionInScope)..map()parameters. Attributes from nested children with inaccessible bindings are skipped.useCallback/useMemo): Arguments are hoisted into the hook call. A separatehoistedDdValuesmap retains only expressions accessible from the component scope. This excludes.map()parameters and other local bindings. The same scope checks apply togetContentcandidates and children.Memoized handler regression fix
Previously, this pattern could throw a
ReferenceErrorwhen pressed:When
handlePresswas memoized withuseCallback, the plugin hoisteditem.nameinto the hook, whereitemwas out of scope. The error occurred ingetContent()before the user's handler executed.The new scope checks prevent inaccessible expressions from being hoisted.
Runtime name resolution
getTargetNamenow skipsundefined,null, empty strings, and empty arrays. It falls back to the next available source instead of producing"undefined"as the action name.Limitations
The following behaviors are unchanged:
Tests
actionNameAttribute.test.ts(new): Executes transformed components with stubbed modules and verifies action names passed towrapRumActionon press. Covers supported expression types,.map()scopes, nested children, memoized handlers, and theReferenceErrorregression.DdBabelInteractionTracking.test.ts: Covers fallback behavior for empty or unresolved values and numeric action names.