Skip to content

Commit 191ddc1

Browse files
Umar Mohammadfacebook-github-bot
authored andcommitted
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: #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
1 parent aaa7ba4 commit 191ddc1

File tree

4 files changed

+413
-6
lines changed

4 files changed

+413
-6
lines changed

packages/babel-plugin-codegen/__test_fixtures__/failures.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,147 @@ export {Commands};
8181
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
8282
`;
8383

84+
const COMMANDS_WITH_COVERAGE_INVALID = `
85+
// @flow
86+
87+
const codegenNativeComponent = require('codegenNativeComponent');
88+
import type {NativeComponentType} from 'codegenNativeComponent';
89+
90+
import type {ViewProps} from 'ViewPropTypes';
91+
92+
type ModuleProps = $ReadOnly<{|
93+
...ViewProps,
94+
|}>;
95+
96+
type NativeType = NativeComponentType<ModuleProps>;
97+
98+
// Coverage instrumentation of invalid Commands export - should still fail
99+
export const Commands = (cov_1234567890().s[0]++, {
100+
hotspotUpdate: () => {},
101+
scrollTo: () => {},
102+
});
103+
104+
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
105+
`;
106+
107+
const COMMANDS_WITH_COVERAGE_WRONG_FUNCTION = `
108+
// @flow
109+
110+
const codegenNativeComponent = require('codegenNativeComponent');
111+
import type {NativeComponentType} from 'codegenNativeComponent';
112+
113+
import type {ViewProps} from 'ViewPropTypes';
114+
115+
type ModuleProps = $ReadOnly<{|
116+
...ViewProps,
117+
|}>;
118+
119+
type NativeType = NativeComponentType<ModuleProps>;
120+
121+
// Coverage instrumentation of wrong function call - should fail
122+
export const Commands = (cov_abcdef123().s[0]++, someOtherFunction({
123+
supportedCommands: ['pause', 'play'],
124+
}));
125+
126+
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
127+
`;
128+
129+
const COMMANDS_WITH_COMPLEX_COVERAGE_INVALID = `
130+
// @flow
131+
132+
const codegenNativeComponent = require('codegenNativeComponent');
133+
import type {NativeComponentType} from 'codegenNativeComponent';
134+
135+
import type {ViewProps} from 'ViewPropTypes';
136+
137+
type ModuleProps = $ReadOnly<{|
138+
...ViewProps,
139+
|}>;
140+
141+
type NativeType = NativeComponentType<ModuleProps>;
142+
143+
// Complex coverage instrumentation with invalid nested structure - should fail
144+
export const Commands = (
145+
cov_xyz789().f[1]++,
146+
cov_xyz789().s[2]++,
147+
{
148+
pause: (ref) => {},
149+
play: (ref) => {},
150+
}
151+
);
152+
153+
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
154+
`;
155+
156+
const COMMANDS_WITH_COVERAGE_WRONG_NAME = `
157+
// @flow
158+
159+
const codegenNativeCommands = require('codegenNativeCommands');
160+
const codegenNativeComponent = require('codegenNativeComponent');
161+
import type {NativeComponentType} from 'codegenNativeComponent';
162+
163+
import type {ViewProps} from 'ViewPropTypes';
164+
165+
type ModuleProps = $ReadOnly<{|
166+
...ViewProps,
167+
|}>;
168+
169+
type NativeType = NativeComponentType<ModuleProps>;
170+
171+
interface NativeCommands {
172+
+pause: (viewRef: React.ElementRef<NativeType>) => void;
173+
+play: (viewRef: React.ElementRef<NativeType>) => void;
174+
}
175+
176+
// Coverage instrumentation with correct function but wrong export name - should fail
177+
export const WrongName = (cov_wrong123().s[0]++, codegenNativeCommands<NativeCommands>({
178+
supportedCommands: ['pause', 'play'],
179+
}));
180+
181+
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
182+
`;
183+
184+
const COMMANDS_WITH_COVERAGE_TYPE_CAST_INVALID = `
185+
// @flow
186+
187+
const codegenNativeComponent = require('codegenNativeComponent');
188+
import type {NativeComponentType} from 'codegenNativeComponent';
189+
190+
import type {ViewProps} from 'ViewPropTypes';
191+
192+
type ModuleProps = $ReadOnly<{|
193+
...ViewProps,
194+
|}>;
195+
196+
type NativeType = NativeComponentType<ModuleProps>;
197+
198+
interface NativeCommands {
199+
+pause: (viewRef: React.ElementRef<NativeType>) => void;
200+
+play: (viewRef: React.ElementRef<NativeType>) => void;
201+
}
202+
203+
// Coverage instrumentation with type cast but wrong function - should fail
204+
export const Commands: NativeCommands = (cov_cast123().s[0]++, invalidFunction({
205+
supportedCommands: ['pause', 'play'],
206+
}));
207+
208+
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
209+
`;
210+
84211
module.exports = {
85212
'CommandsExportedWithDifferentNameNativeComponent.js':
86213
COMMANDS_EXPORTED_WITH_DIFFERENT_NAME,
87214
'CommandsExportedWithShorthandNativeComponent.js':
88215
COMMANDS_EXPORTED_WITH_SHORTHAND,
89216
'OtherCommandsExportNativeComponent.js': OTHER_COMMANDS_EXPORT,
217+
'CommandsWithCoverageInvalidNativeComponent.js':
218+
COMMANDS_WITH_COVERAGE_INVALID,
219+
'CommandsWithCoverageWrongFunctionNativeComponent.js':
220+
COMMANDS_WITH_COVERAGE_WRONG_FUNCTION,
221+
'CommandsWithComplexCoverageInvalidNativeComponent.js':
222+
COMMANDS_WITH_COMPLEX_COVERAGE_INVALID,
223+
'CommandsWithCoverageWrongNameNativeComponent.js':
224+
COMMANDS_WITH_COVERAGE_WRONG_NAME,
225+
'CommandsWithCoverageTypeCastInvalidNativeComponent.js':
226+
COMMANDS_WITH_COVERAGE_TYPE_CAST_INVALID,
90227
};

packages/babel-plugin-codegen/__test_fixtures__/fixtures.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,92 @@ export default codegenNativeComponent<ModuleProps>('Module', {
5959
});
6060
`;
6161

62+
// Coverage instrumentation test cases - should be recognized as valid
63+
const COMMANDS_WITH_SIMPLE_COVERAGE = `
64+
// @flow
65+
66+
const codegenNativeCommands = require('codegenNativeCommands');
67+
const codegenNativeComponent = require('codegenNativeComponent');
68+
69+
import type {ViewProps} from 'ViewPropTypes';
70+
import type {NativeComponentType} from 'codegenNativeComponent';
71+
72+
type ModuleProps = $ReadOnly<{|
73+
...ViewProps,
74+
|}>;
75+
76+
type NativeType = NativeComponentType<ModuleProps>;
77+
78+
interface NativeCommands {
79+
+pause: (viewRef: React.ElementRef<NativeType>) => void;
80+
+play: (viewRef: React.ElementRef<NativeType>) => void;
81+
}
82+
83+
export const Commands = (cov_1234567890.s[0]++, codegenNativeCommands<NativeCommands>({
84+
supportedCommands: ['pause', 'play'],
85+
}));
86+
87+
export default codegenNativeComponent<ModuleProps>('Module');
88+
`;
89+
90+
const COMMANDS_WITH_COMPLEX_COVERAGE = `
91+
// @flow
92+
93+
const codegenNativeCommands = require('codegenNativeCommands');
94+
const codegenNativeComponent = require('codegenNativeComponent');
95+
96+
import type {ViewProps} from 'ViewPropTypes';
97+
import type {NativeComponentType} from 'codegenNativeComponent';
98+
99+
type ModuleProps = $ReadOnly<{|
100+
...ViewProps,
101+
|}>;
102+
103+
type NativeType = NativeComponentType<ModuleProps>;
104+
105+
interface NativeCommands {
106+
+seek: (viewRef: React.ElementRef<NativeType>, position: number) => void;
107+
+stop: (viewRef: React.ElementRef<NativeType>) => void;
108+
}
109+
110+
export const Commands = (
111+
cov_abcdef123().f[2]++,
112+
cov_abcdef123().s[5]++,
113+
codegenNativeCommands<NativeCommands>({
114+
supportedCommands: ['seek', 'stop'],
115+
})
116+
);
117+
118+
export default codegenNativeComponent<ModuleProps>('Module');
119+
`;
120+
121+
const COMMANDS_WITH_TYPE_CAST_COVERAGE = `
122+
// @flow
123+
124+
const codegenNativeCommands = require('codegenNativeCommands');
125+
const codegenNativeComponent = require('codegenNativeComponent');
126+
127+
import type {ViewProps} from 'ViewPropTypes';
128+
import type {NativeComponentType} from 'codegenNativeComponent';
129+
130+
type ModuleProps = $ReadOnly<{|
131+
...ViewProps,
132+
|}>;
133+
134+
type NativeType = NativeComponentType<ModuleProps>;
135+
136+
interface NativeCommands {
137+
+mute: (viewRef: React.ElementRef<NativeType>) => void;
138+
+unmute: (viewRef: React.ElementRef<NativeType>) => void;
139+
}
140+
141+
export const Commands: NativeCommands = (cov_xyz789().s[1]++, codegenNativeCommands<NativeCommands>({
142+
supportedCommands: ['mute', 'unmute'],
143+
}));
144+
145+
export default codegenNativeComponent<ModuleProps>('Module');
146+
`;
147+
62148
const FULL_NATIVE_COMPONENT_WITH_TYPE_EXPORT = `
63149
// @flow
64150
@@ -107,4 +193,9 @@ module.exports = {
107193
'NotANativeComponent.js': NOT_A_NATIVE_COMPONENT,
108194
'FullNativeComponent.js': FULL_NATIVE_COMPONENT,
109195
'FullTypedNativeComponent.js': FULL_NATIVE_COMPONENT_WITH_TYPE_EXPORT,
196+
'CommandsWithSimpleCoverageNativeComponent.js': COMMANDS_WITH_SIMPLE_COVERAGE,
197+
'CommandsWithComplexCoverageNativeComponent.js':
198+
COMMANDS_WITH_COMPLEX_COVERAGE,
199+
'CommandsWithTypeCastCoverageNativeComponent.js':
200+
COMMANDS_WITH_TYPE_CAST_COVERAGE,
110201
};

packages/babel-plugin-codegen/__tests__/__snapshots__/index-test.js.snap

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,77 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`Babel plugin inline view configs can inline config for CommandsWithComplexCoverageNativeComponent.js 1`] = `
4+
"// @flow
5+
6+
const codegenNativeCommands = require('codegenNativeCommands');
7+
const codegenNativeComponent = require('codegenNativeComponent');
8+
import type { ViewProps } from 'ViewPropTypes';
9+
import type { NativeComponentType } from 'codegenNativeComponent';
10+
type ModuleProps = $ReadOnly<{|
11+
...ViewProps
12+
|}>;
13+
type NativeType = NativeComponentType<ModuleProps>;
14+
interface NativeCommands {
15+
+seek: (viewRef: React.ElementRef<NativeType>, position: number) => void,
16+
+stop: (viewRef: React.ElementRef<NativeType>) => void,
17+
}
18+
const NativeComponentRegistry = require('react-native/Libraries/NativeComponent/NativeComponentRegistry');
19+
let nativeComponentName = 'Module';
20+
export const __INTERNAL_VIEW_CONFIG = {
21+
uiViewClassName: \\"Module\\",
22+
validAttributes: {}
23+
};
24+
export default NativeComponentRegistry.get(nativeComponentName, () => __INTERNAL_VIEW_CONFIG);"
25+
`;
26+
27+
exports[`Babel plugin inline view configs can inline config for CommandsWithSimpleCoverageNativeComponent.js 1`] = `
28+
"// @flow
29+
30+
const codegenNativeCommands = require('codegenNativeCommands');
31+
const codegenNativeComponent = require('codegenNativeComponent');
32+
import type { ViewProps } from 'ViewPropTypes';
33+
import type { NativeComponentType } from 'codegenNativeComponent';
34+
type ModuleProps = $ReadOnly<{|
35+
...ViewProps
36+
|}>;
37+
type NativeType = NativeComponentType<ModuleProps>;
38+
interface NativeCommands {
39+
+pause: (viewRef: React.ElementRef<NativeType>) => void,
40+
+play: (viewRef: React.ElementRef<NativeType>) => void,
41+
}
42+
const NativeComponentRegistry = require('react-native/Libraries/NativeComponent/NativeComponentRegistry');
43+
let nativeComponentName = 'Module';
44+
export const __INTERNAL_VIEW_CONFIG = {
45+
uiViewClassName: \\"Module\\",
46+
validAttributes: {}
47+
};
48+
export default NativeComponentRegistry.get(nativeComponentName, () => __INTERNAL_VIEW_CONFIG);"
49+
`;
50+
51+
exports[`Babel plugin inline view configs can inline config for CommandsWithTypeCastCoverageNativeComponent.js 1`] = `
52+
"// @flow
53+
54+
const codegenNativeCommands = require('codegenNativeCommands');
55+
const codegenNativeComponent = require('codegenNativeComponent');
56+
import type { ViewProps } from 'ViewPropTypes';
57+
import type { NativeComponentType } from 'codegenNativeComponent';
58+
type ModuleProps = $ReadOnly<{|
59+
...ViewProps
60+
|}>;
61+
type NativeType = NativeComponentType<ModuleProps>;
62+
interface NativeCommands {
63+
+mute: (viewRef: React.ElementRef<NativeType>) => void,
64+
+unmute: (viewRef: React.ElementRef<NativeType>) => void,
65+
}
66+
const NativeComponentRegistry = require('react-native/Libraries/NativeComponent/NativeComponentRegistry');
67+
let nativeComponentName = 'Module';
68+
export const __INTERNAL_VIEW_CONFIG = {
69+
uiViewClassName: \\"Module\\",
70+
validAttributes: {}
71+
};
72+
export default NativeComponentRegistry.get(nativeComponentName, () => __INTERNAL_VIEW_CONFIG);"
73+
`;
74+
375
exports[`Babel plugin inline view configs can inline config for FullNativeComponent.js 1`] = `
476
"// @flow
577
@@ -153,6 +225,61 @@ exports[`Babel plugin inline view configs fails on inline config for CommandsExp
153225
24 |"
154226
`;
155227
228+
exports[`Babel plugin inline view configs fails on inline config for CommandsWithComplexCoverageInvalidNativeComponent.js 1`] = `
229+
"/CommandsWithComplexCoverageInvalidNativeComponent.js: 'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.
230+
14 |
231+
15 | // Complex coverage instrumentation with invalid nested structure - should fail
232+
> 16 | export const Commands = (
233+
| ^
234+
17 | cov_xyz789().f[1]++,
235+
18 | cov_xyz789().s[2]++,
236+
19 | {"
237+
`;
238+
239+
exports[`Babel plugin inline view configs fails on inline config for CommandsWithCoverageInvalidNativeComponent.js 1`] = `
240+
"/CommandsWithCoverageInvalidNativeComponent.js: 'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.
241+
14 |
242+
15 | // Coverage instrumentation of invalid Commands export - should still fail
243+
> 16 | export const Commands = (cov_1234567890().s[0]++, {
244+
| ^
245+
17 | hotspotUpdate: () => {},
246+
18 | scrollTo: () => {},
247+
19 | });"
248+
`;
249+
250+
exports[`Babel plugin inline view configs fails on inline config for CommandsWithCoverageTypeCastInvalidNativeComponent.js 1`] = `
251+
"/CommandsWithCoverageTypeCastInvalidNativeComponent.js: 'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.
252+
19 |
253+
20 | // Coverage instrumentation with type cast but wrong function - should fail
254+
> 21 | export const Commands: NativeCommands = (cov_cast123().s[0]++, invalidFunction({
255+
| ^
256+
22 | supportedCommands: ['pause', 'play'],
257+
23 | }));
258+
24 |"
259+
`;
260+
261+
exports[`Babel plugin inline view configs fails on inline config for CommandsWithCoverageWrongFunctionNativeComponent.js 1`] = `
262+
"/CommandsWithCoverageWrongFunctionNativeComponent.js: 'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.
263+
14 |
264+
15 | // Coverage instrumentation of wrong function call - should fail
265+
> 16 | export const Commands = (cov_abcdef123().s[0]++, someOtherFunction({
266+
| ^
267+
17 | supportedCommands: ['pause', 'play'],
268+
18 | }));
269+
19 |"
270+
`;
271+
272+
exports[`Babel plugin inline view configs fails on inline config for CommandsWithCoverageWrongNameNativeComponent.js 1`] = `
273+
"/CommandsWithCoverageWrongNameNativeComponent.js: Native commands must be exported with the name 'Commands'
274+
20 |
275+
21 | // Coverage instrumentation with correct function but wrong export name - should fail
276+
> 22 | export const WrongName = (cov_wrong123().s[0]++, codegenNativeCommands<NativeCommands>({
277+
| ^
278+
23 | supportedCommands: ['pause', 'play'],
279+
24 | }));
280+
25 |"
281+
`;
282+
156283
exports[`Babel plugin inline view configs fails on inline config for OtherCommandsExportNativeComponent.js 1`] = `
157284
"/OtherCommandsExportNativeComponent.js: 'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.
158285
17 | }

0 commit comments

Comments
 (0)