Fix React Native Commands Export Validation in Coverage Mode (#53381)

Summary:
Changelog: [GENERAL] [FIXED] - Fixed babel plugin validation error when coverage instrumentation is enabled

Pull Request resolved: https://github.com/facebook/react-native/pull/53381

### Problem

[Workplace post](https://fb.workplace.com/groups/235694244595999/permalink/1278937163605030/)

React Native tests were failing **only when coverage collection was enabled** with the error:

`'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.`

### Root Cause

The React Native Babel plugin's `codegenNativeCommands` validation logic only handled direct `CallExpression` AST nodes. When coverage instrumentation was enabled, it transformed:

**Normal code:**

`export const Commands = codegenNativeCommands<NativeCommands>({...})`

**With coverage:**

`export const Commands = (cov_xxx().s[0]++, codegenNativeCommands<NativeCommands>({...}))`

The plugin failed to recognize the valid `codegenNativeCommands` call wrapped in a `SequenceExpression` by coverage instrumentation.

### **Solution**

Added `isCodegenNativeCommandsDeclaration` function to handle:

1.  **Coverage instrumentation**: `SequenceExpression` nodes containing the function call
2.  **Flow type casts**: `TypeCastExpression` and `AsExpression`
3.  **TypeScript assertions**: `TSAsExpression`
4.  **Direct calls**: Original `CallExpression` (backward compatibility)

Reviewed By: andrewdacenko

Differential Revision: D80572666

fbshipit-source-id: 465f4312a0229d8a92e495c685f46b607ce326e4
This commit is contained in:
Umar Mohammad
2025-08-21 11:33:52 -07:00
committed by Facebook GitHub Bot
parent aaa7ba4ab3
commit 191ddc1ec7
4 changed files with 413 additions and 6 deletions
@@ -81,10 +81,147 @@ export {Commands};
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
`;
const COMMANDS_WITH_COVERAGE_INVALID = `
// @flow
const codegenNativeComponent = require('codegenNativeComponent');
import type {NativeComponentType} from 'codegenNativeComponent';
import type {ViewProps} from 'ViewPropTypes';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
// Coverage instrumentation of invalid Commands export - should still fail
export const Commands = (cov_1234567890().s[0]++, {
hotspotUpdate: () => {},
scrollTo: () => {},
});
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
`;
const COMMANDS_WITH_COVERAGE_WRONG_FUNCTION = `
// @flow
const codegenNativeComponent = require('codegenNativeComponent');
import type {NativeComponentType} from 'codegenNativeComponent';
import type {ViewProps} from 'ViewPropTypes';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
// Coverage instrumentation of wrong function call - should fail
export const Commands = (cov_abcdef123().s[0]++, someOtherFunction({
supportedCommands: ['pause', 'play'],
}));
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
`;
const COMMANDS_WITH_COMPLEX_COVERAGE_INVALID = `
// @flow
const codegenNativeComponent = require('codegenNativeComponent');
import type {NativeComponentType} from 'codegenNativeComponent';
import type {ViewProps} from 'ViewPropTypes';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
// Complex coverage instrumentation with invalid nested structure - should fail
export const Commands = (
cov_xyz789().f[1]++,
cov_xyz789().s[2]++,
{
pause: (ref) => {},
play: (ref) => {},
}
);
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
`;
const COMMANDS_WITH_COVERAGE_WRONG_NAME = `
// @flow
const codegenNativeCommands = require('codegenNativeCommands');
const codegenNativeComponent = require('codegenNativeComponent');
import type {NativeComponentType} from 'codegenNativeComponent';
import type {ViewProps} from 'ViewPropTypes';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+pause: (viewRef: React.ElementRef<NativeType>) => void;
+play: (viewRef: React.ElementRef<NativeType>) => void;
}
// Coverage instrumentation with correct function but wrong export name - should fail
export const WrongName = (cov_wrong123().s[0]++, codegenNativeCommands<NativeCommands>({
supportedCommands: ['pause', 'play'],
}));
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
`;
const COMMANDS_WITH_COVERAGE_TYPE_CAST_INVALID = `
// @flow
const codegenNativeComponent = require('codegenNativeComponent');
import type {NativeComponentType} from 'codegenNativeComponent';
import type {ViewProps} from 'ViewPropTypes';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+pause: (viewRef: React.ElementRef<NativeType>) => void;
+play: (viewRef: React.ElementRef<NativeType>) => void;
}
// Coverage instrumentation with type cast but wrong function - should fail
export const Commands: NativeCommands = (cov_cast123().s[0]++, invalidFunction({
supportedCommands: ['pause', 'play'],
}));
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
`;
module.exports = {
'CommandsExportedWithDifferentNameNativeComponent.js':
COMMANDS_EXPORTED_WITH_DIFFERENT_NAME,
'CommandsExportedWithShorthandNativeComponent.js':
COMMANDS_EXPORTED_WITH_SHORTHAND,
'OtherCommandsExportNativeComponent.js': OTHER_COMMANDS_EXPORT,
'CommandsWithCoverageInvalidNativeComponent.js':
COMMANDS_WITH_COVERAGE_INVALID,
'CommandsWithCoverageWrongFunctionNativeComponent.js':
COMMANDS_WITH_COVERAGE_WRONG_FUNCTION,
'CommandsWithComplexCoverageInvalidNativeComponent.js':
COMMANDS_WITH_COMPLEX_COVERAGE_INVALID,
'CommandsWithCoverageWrongNameNativeComponent.js':
COMMANDS_WITH_COVERAGE_WRONG_NAME,
'CommandsWithCoverageTypeCastInvalidNativeComponent.js':
COMMANDS_WITH_COVERAGE_TYPE_CAST_INVALID,
};
@@ -59,6 +59,92 @@ export default codegenNativeComponent<ModuleProps>('Module', {
});
`;
// Coverage instrumentation test cases - should be recognized as valid
const COMMANDS_WITH_SIMPLE_COVERAGE = `
// @flow
const codegenNativeCommands = require('codegenNativeCommands');
const codegenNativeComponent = require('codegenNativeComponent');
import type {ViewProps} from 'ViewPropTypes';
import type {NativeComponentType} from 'codegenNativeComponent';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+pause: (viewRef: React.ElementRef<NativeType>) => void;
+play: (viewRef: React.ElementRef<NativeType>) => void;
}
export const Commands = (cov_1234567890.s[0]++, codegenNativeCommands<NativeCommands>({
supportedCommands: ['pause', 'play'],
}));
export default codegenNativeComponent<ModuleProps>('Module');
`;
const COMMANDS_WITH_COMPLEX_COVERAGE = `
// @flow
const codegenNativeCommands = require('codegenNativeCommands');
const codegenNativeComponent = require('codegenNativeComponent');
import type {ViewProps} from 'ViewPropTypes';
import type {NativeComponentType} from 'codegenNativeComponent';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+seek: (viewRef: React.ElementRef<NativeType>, position: number) => void;
+stop: (viewRef: React.ElementRef<NativeType>) => void;
}
export const Commands = (
cov_abcdef123().f[2]++,
cov_abcdef123().s[5]++,
codegenNativeCommands<NativeCommands>({
supportedCommands: ['seek', 'stop'],
})
);
export default codegenNativeComponent<ModuleProps>('Module');
`;
const COMMANDS_WITH_TYPE_CAST_COVERAGE = `
// @flow
const codegenNativeCommands = require('codegenNativeCommands');
const codegenNativeComponent = require('codegenNativeComponent');
import type {ViewProps} from 'ViewPropTypes';
import type {NativeComponentType} from 'codegenNativeComponent';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+mute: (viewRef: React.ElementRef<NativeType>) => void;
+unmute: (viewRef: React.ElementRef<NativeType>) => void;
}
export const Commands: NativeCommands = (cov_xyz789().s[1]++, codegenNativeCommands<NativeCommands>({
supportedCommands: ['mute', 'unmute'],
}));
export default codegenNativeComponent<ModuleProps>('Module');
`;
const FULL_NATIVE_COMPONENT_WITH_TYPE_EXPORT = `
// @flow
@@ -107,4 +193,9 @@ module.exports = {
'NotANativeComponent.js': NOT_A_NATIVE_COMPONENT,
'FullNativeComponent.js': FULL_NATIVE_COMPONENT,
'FullTypedNativeComponent.js': FULL_NATIVE_COMPONENT_WITH_TYPE_EXPORT,
'CommandsWithSimpleCoverageNativeComponent.js': COMMANDS_WITH_SIMPLE_COVERAGE,
'CommandsWithComplexCoverageNativeComponent.js':
COMMANDS_WITH_COMPLEX_COVERAGE,
'CommandsWithTypeCastCoverageNativeComponent.js':
COMMANDS_WITH_TYPE_CAST_COVERAGE,
};
@@ -1,5 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Babel plugin inline view configs can inline config for CommandsWithComplexCoverageNativeComponent.js 1`] = `
"// @flow
const codegenNativeCommands = require('codegenNativeCommands');
const codegenNativeComponent = require('codegenNativeComponent');
import type { ViewProps } from 'ViewPropTypes';
import type { NativeComponentType } from 'codegenNativeComponent';
type ModuleProps = $ReadOnly<{|
...ViewProps
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+seek: (viewRef: React.ElementRef<NativeType>, position: number) => void,
+stop: (viewRef: React.ElementRef<NativeType>) => void,
}
const NativeComponentRegistry = require('react-native/Libraries/NativeComponent/NativeComponentRegistry');
let nativeComponentName = 'Module';
export const __INTERNAL_VIEW_CONFIG = {
uiViewClassName: \\"Module\\",
validAttributes: {}
};
export default NativeComponentRegistry.get(nativeComponentName, () => __INTERNAL_VIEW_CONFIG);"
`;
exports[`Babel plugin inline view configs can inline config for CommandsWithSimpleCoverageNativeComponent.js 1`] = `
"// @flow
const codegenNativeCommands = require('codegenNativeCommands');
const codegenNativeComponent = require('codegenNativeComponent');
import type { ViewProps } from 'ViewPropTypes';
import type { NativeComponentType } from 'codegenNativeComponent';
type ModuleProps = $ReadOnly<{|
...ViewProps
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+pause: (viewRef: React.ElementRef<NativeType>) => void,
+play: (viewRef: React.ElementRef<NativeType>) => void,
}
const NativeComponentRegistry = require('react-native/Libraries/NativeComponent/NativeComponentRegistry');
let nativeComponentName = 'Module';
export const __INTERNAL_VIEW_CONFIG = {
uiViewClassName: \\"Module\\",
validAttributes: {}
};
export default NativeComponentRegistry.get(nativeComponentName, () => __INTERNAL_VIEW_CONFIG);"
`;
exports[`Babel plugin inline view configs can inline config for CommandsWithTypeCastCoverageNativeComponent.js 1`] = `
"// @flow
const codegenNativeCommands = require('codegenNativeCommands');
const codegenNativeComponent = require('codegenNativeComponent');
import type { ViewProps } from 'ViewPropTypes';
import type { NativeComponentType } from 'codegenNativeComponent';
type ModuleProps = $ReadOnly<{|
...ViewProps
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+mute: (viewRef: React.ElementRef<NativeType>) => void,
+unmute: (viewRef: React.ElementRef<NativeType>) => void,
}
const NativeComponentRegistry = require('react-native/Libraries/NativeComponent/NativeComponentRegistry');
let nativeComponentName = 'Module';
export const __INTERNAL_VIEW_CONFIG = {
uiViewClassName: \\"Module\\",
validAttributes: {}
};
export default NativeComponentRegistry.get(nativeComponentName, () => __INTERNAL_VIEW_CONFIG);"
`;
exports[`Babel plugin inline view configs can inline config for FullNativeComponent.js 1`] = `
"// @flow
@@ -153,6 +225,61 @@ exports[`Babel plugin inline view configs fails on inline config for CommandsExp
24 |"
`;
exports[`Babel plugin inline view configs fails on inline config for CommandsWithComplexCoverageInvalidNativeComponent.js 1`] = `
"/CommandsWithComplexCoverageInvalidNativeComponent.js: 'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.
14 |
15 | // Complex coverage instrumentation with invalid nested structure - should fail
> 16 | export const Commands = (
| ^
17 | cov_xyz789().f[1]++,
18 | cov_xyz789().s[2]++,
19 | {"
`;
exports[`Babel plugin inline view configs fails on inline config for CommandsWithCoverageInvalidNativeComponent.js 1`] = `
"/CommandsWithCoverageInvalidNativeComponent.js: 'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.
14 |
15 | // Coverage instrumentation of invalid Commands export - should still fail
> 16 | export const Commands = (cov_1234567890().s[0]++, {
| ^
17 | hotspotUpdate: () => {},
18 | scrollTo: () => {},
19 | });"
`;
exports[`Babel plugin inline view configs fails on inline config for CommandsWithCoverageTypeCastInvalidNativeComponent.js 1`] = `
"/CommandsWithCoverageTypeCastInvalidNativeComponent.js: 'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.
19 |
20 | // Coverage instrumentation with type cast but wrong function - should fail
> 21 | export const Commands: NativeCommands = (cov_cast123().s[0]++, invalidFunction({
| ^
22 | supportedCommands: ['pause', 'play'],
23 | }));
24 |"
`;
exports[`Babel plugin inline view configs fails on inline config for CommandsWithCoverageWrongFunctionNativeComponent.js 1`] = `
"/CommandsWithCoverageWrongFunctionNativeComponent.js: 'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.
14 |
15 | // Coverage instrumentation of wrong function call - should fail
> 16 | export const Commands = (cov_abcdef123().s[0]++, someOtherFunction({
| ^
17 | supportedCommands: ['pause', 'play'],
18 | }));
19 |"
`;
exports[`Babel plugin inline view configs fails on inline config for CommandsWithCoverageWrongNameNativeComponent.js 1`] = `
"/CommandsWithCoverageWrongNameNativeComponent.js: Native commands must be exported with the name 'Commands'
20 |
21 | // Coverage instrumentation with correct function but wrong export name - should fail
> 22 | export const WrongName = (cov_wrong123().s[0]++, codegenNativeCommands<NativeCommands>({
| ^
23 | supportedCommands: ['pause', 'play'],
24 | }));
25 |"
`;
exports[`Babel plugin inline view configs fails on inline config for OtherCommandsExportNativeComponent.js 1`] = `
"/OtherCommandsExportNativeComponent.js: 'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.
17 | }
+58 -6
View File
@@ -102,6 +102,58 @@ function isCodegenDeclaration(declaration) {
return false;
}
function isCodegenNativeCommandsDeclaration(declaration) {
if (!declaration) {
return false;
}
// Handle direct calls: codegenNativeCommands()
if (
declaration.type === 'CallExpression' &&
declaration.callee &&
declaration.callee.type === 'Identifier' &&
declaration.callee.name === 'codegenNativeCommands'
) {
return true;
}
// Handle coverage instrumentation: (cov_xxx().s[0]++, codegenNativeCommands())
if (declaration.type === 'SequenceExpression' && declaration.expressions) {
// Get the last expression in the sequence (the actual function call)
const lastExpression =
declaration.expressions[declaration.expressions.length - 1];
// Recursively check if the last expression is a valid codegenNativeCommands call
return isCodegenNativeCommandsDeclaration(lastExpression);
}
// Handle Flow type casts: (codegenNativeCommands(): NativeCommands)
if (
(declaration.type === 'TypeCastExpression' ||
declaration.type === 'AsExpression') &&
declaration.expression &&
declaration.expression.type === 'CallExpression' &&
declaration.expression.callee &&
declaration.expression.callee.type === 'Identifier' &&
declaration.expression.callee.name === 'codegenNativeCommands'
) {
return true;
}
// Handle TypeScript assertions: codegenNativeCommands() as NativeCommands
if (
declaration.type === 'TSAsExpression' &&
declaration.expression &&
declaration.expression.type === 'CallExpression' &&
declaration.expression.callee &&
declaration.expression.callee.type === 'Identifier' &&
declaration.expression.callee.name === 'codegenNativeCommands'
) {
return true;
}
return false;
}
module.exports = function ({parse, types: t}) {
return {
pre(state) {
@@ -125,12 +177,12 @@ module.exports = function ({parse, types: t}) {
const firstDeclaration = path.node.declaration.declarations[0];
if (firstDeclaration.type === 'VariableDeclarator') {
if (
firstDeclaration.init &&
firstDeclaration.init.type === 'CallExpression' &&
firstDeclaration.init.callee.type === 'Identifier' &&
firstDeclaration.init.callee.name === 'codegenNativeCommands'
) {
// Check if this is a valid codegenNativeCommands call, handling type annotations
const isValidCommandsExport = isCodegenNativeCommandsDeclaration(
firstDeclaration.init,
);
if (isValidCommandsExport) {
if (
firstDeclaration.id.type === 'Identifier' &&
firstDeclaration.id.name !== 'Commands'