[compiler] detect and throw on untransformed required features (#32512)
Traverse program after running compiler transform to find untransformed references to compiler features (e.g. `inferEffectDeps`, `fire`). Hard error to fail the babel pipeline when the compiler fails to transform these features to give predictable runtime semantics. Untransformed calls to functions like `fire` will throw at runtime anyways, so let's fail the build to catch these earlier. Note that with this fails the build *regardless of panicThreshold*
mofeiZ committed
Mar 14, 2025 at 11:44 UTC
5398b7115847e87c0053aa719728d8dd1a635ccd
31 files changed
+864
-146
compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts
+9
-1
@@ -11,6 +11,7 @@ import {
11
injectReanimatedFlag,
12
pipelineUsesReanimatedPlugin,
13
} from '../Entrypoint/Reanimated';
14
+import validateNoUntransformedReferences from '../Entrypoint/ValidateNoUntransformedReferences';
15
16
const ENABLE_REACT_COMPILER_TIMINGS =
17
process.env['ENABLE_REACT_COMPILER_TIMINGS'] === '1';
@@ -61,12 +62,19 @@ export default function BabelPluginReactCompiler(
62
},
63
};
64
}
64
- compileProgram(prog, {
65
+ const result = compileProgram(prog, {
66
opts,
67
filename: pass.filename ?? null,
68
comments: pass.file.ast.comments ?? [],
69
code: pass.file.code,
70
});
71
+ validateNoUntransformedReferences(
72
+ prog,
73
+ pass.filename ?? null,
74
+ opts.logger,
75
+ opts.environment,
76
+ result?.retryErrors ?? [],
77
+ );
78
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
79
performance.mark(`${filename}:end`, {
80
detail: 'BabelPlugin:Program:end',
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
+16
-7
@@ -271,6 +271,9 @@ function isFilePartOfSources(
271
return false;
272
}
273
274
+type CompileProgramResult = {
275
+ retryErrors: Array<{fn: BabelFn; error: CompilerError}>;
276
+};
277
/**
278
* `compileProgram` is directly invoked by the react-compiler babel plugin, so
279
* exceptions thrown by this function will fail the babel build.
@@ -285,16 +288,16 @@ function isFilePartOfSources(
288
export function compileProgram(
289
program: NodePath<t.Program>,
290
pass: CompilerPass,
288
-): void {
291
+): CompileProgramResult | null {
292
if (shouldSkipCompilation(program, pass)) {
290
- return;
293
+ return null;
294
}
295
296
const environment = pass.opts.environment;
297
const restrictedImportsErr = validateRestrictedImports(program, environment);
298
if (restrictedImportsErr) {
299
handleError(restrictedImportsErr, pass, null);
297
- return;
300
+ return null;
301
}
302
const useMemoCacheIdentifier = program.scope.generateUidIdentifier('c');
303
@@ -365,7 +368,7 @@ export function compileProgram(
368
filename: pass.filename ?? null,
369
},
370
);
368
-
371
+ const retryErrors: Array<{fn: BabelFn; error: CompilerError}> = [];
372
const processFn = (
373
fn: BabelFn,
374
fnType: ReactFunctionType,
@@ -429,7 +432,9 @@ export function compileProgram(
432
handleError(compileResult.error, pass, fn.node.loc ?? null);
433
}
434
// If non-memoization features are enabled, retry regardless of error kind
432
- if (!environment.enableFire) {
435
+ if (
436
+ !(environment.enableFire || environment.inferEffectDependencies != null)
437
+ ) {
438
return null;
439
}
440
try {
@@ -448,6 +453,9 @@ export function compileProgram(
453
};
454
} catch (err) {
455
// TODO: we might want to log error here, but this will also result in duplicate logging
456
+ if (err instanceof CompilerError) {
457
+ retryErrors.push({fn, error: err});
458
+ }
459
return null;
460
}
461
}
@@ -538,7 +546,7 @@ export function compileProgram(
546
program.node.directives,
547
);
548
if (moduleScopeOptOutDirectives.length > 0) {
541
- return;
549
+ return null;
550
}
551
let gating: null | {
552
gatingFn: ExternalFunction;
@@ -596,7 +604,7 @@ export function compileProgram(
604
}
605
} catch (err) {
606
handleError(err, pass, null);
599
- return;
607
+ return null;
608
}
609
610
/*
@@ -638,6 +646,7 @@ export function compileProgram(
646
}
647
addImportsToProgram(program, externalFunctions);
648
}
649
+ return {retryErrors};
650
}
651
652
function shouldSkipCompilation(
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts
new
+275
@@ -0,0 +1,275 @@
1
+import {NodePath} from '@babel/core';
2
+import * as t from '@babel/types';
3
+
4
+import {
5
+ CompilerError,
6
+ CompilerErrorDetailOptions,
7
+ EnvironmentConfig,
8
+ ErrorSeverity,
9
+ Logger,
10
+} from '..';
11
+import {getOrInsertWith} from '../Utils/utils';
12
+import {Environment} from '../HIR';
13
+import {DEFAULT_EXPORT} from '../HIR/Environment';
14
+
15
+function throwInvalidReact(
16
+ options: Omit<CompilerErrorDetailOptions, 'severity'>,
17
+ {logger, filename}: TraversalState,
18
+): never {
19
+ const detail: CompilerErrorDetailOptions = {
20
+ ...options,
21
+ severity: ErrorSeverity.InvalidReact,
22
+ };
23
+ logger?.logEvent(filename, {
24
+ kind: 'CompileError',
25
+ fnLoc: null,
26
+ detail,
27
+ });
28
+ CompilerError.throw(detail);
29
+}
30
+function assertValidEffectImportReference(
31
+ numArgs: number,
32
+ paths: Array<NodePath<t.Node>>,
33
+ context: TraversalState,
34
+): void {
35
+ for (const path of paths) {
36
+ const parent = path.parentPath;
37
+ if (parent != null && parent.isCallExpression()) {
38
+ const args = parent.get('arguments');
39
+ /**
40
+ * Only error on untransformed references of the form `useMyEffect(...)`
41
+ * or `moduleNamespace.useMyEffect(...)`, with matching argument counts.
42
+ * TODO: do we also want a mode to also hard error on non-call references?
43
+ */
44
+ if (args.length === numArgs) {
45
+ const maybeErrorDiagnostic = matchCompilerDiagnostic(
46
+ path,
47
+ context.transformErrors,
48
+ );
49
+ /**
50
+ * Note that we cannot easily check the type of the first argument here,
51
+ * as it may have already been transformed by the compiler (and not
52
+ * memoized).
53
+ */
54
+ throwInvalidReact(
55
+ {
56
+ reason:
57
+ '[InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. ' +
58
+ 'This will break your build! ' +
59
+ 'To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics.',
60
+ description: maybeErrorDiagnostic
61
+ ? `(Bailout reason: ${maybeErrorDiagnostic})`
62
+ : null,
63
+ loc: parent.node.loc ?? null,
64
+ },
65
+ context,
66
+ );
67
+ }
68
+ }
69
+ }
70
+}
71
+
72
+function assertValidFireImportReference(
73
+ paths: Array<NodePath<t.Node>>,
74
+ context: TraversalState,
75
+): void {
76
+ if (paths.length > 0) {
77
+ const maybeErrorDiagnostic = matchCompilerDiagnostic(
78
+ paths[0],
79
+ context.transformErrors,
80
+ );
81
+ throwInvalidReact(
82
+ {
83
+ reason:
84
+ '[Fire] Untransformed reference to compiler-required feature. ' +
85
+ 'Either remove this `fire` call or ensure it is successfully transformed by the compiler',
86
+ description: maybeErrorDiagnostic
87
+ ? `(Bailout reason: ${maybeErrorDiagnostic})`
88
+ : null,
89
+ loc: paths[0].node.loc ?? null,
90
+ },
91
+ context,
92
+ );
93
+ }
94
+}
95
+export default function validateNoUntransformedReferences(
96
+ path: NodePath<t.Program>,
97
+ filename: string | null,
98
+ logger: Logger | null,
99
+ env: EnvironmentConfig,
100
+ transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>,
101
+): void {
102
+ const moduleLoadChecks = new Map<
103
+ string,
104
+ Map<string, CheckInvalidReferenceFn>
105
+ >();
106
+ if (env.enableFire) {
107
+ /**
108
+ * Error on any untransformed references to `fire` (e.g. including non-call
109
+ * expressions)
110
+ */
111
+ for (const module of Environment.knownReactModules) {
112
+ const react = getOrInsertWith(moduleLoadChecks, module, () => new Map());
113
+ react.set('fire', assertValidFireImportReference);
114
+ }
115
+ }
116
+ if (env.inferEffectDependencies) {
117
+ for (const {
118
+ function: {source, importSpecifierName},
119
+ numRequiredArgs,
120
+ } of env.inferEffectDependencies) {
121
+ const module = getOrInsertWith(moduleLoadChecks, source, () => new Map());
122
+ module.set(
123
+ importSpecifierName,
124
+ assertValidEffectImportReference.bind(null, numRequiredArgs),
125
+ );
126
+ }
127
+ }
128
+ if (moduleLoadChecks.size > 0) {
129
+ transformProgram(path, moduleLoadChecks, filename, logger, transformErrors);
130
+ }
131
+}
132
+
133
+type TraversalState = {
134
+ shouldInvalidateScopes: boolean;
135
+ program: NodePath<t.Program>;
136
+ logger: Logger | null;
137
+ filename: string | null;
138
+ transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>;
139
+};
140
+type CheckInvalidReferenceFn = (
141
+ paths: Array<NodePath<t.Node>>,
142
+ context: TraversalState,
143
+) => void;
144
+
145
+function validateImportSpecifier(
146
+ specifier: NodePath<t.ImportSpecifier>,
147
+ importSpecifierChecks: Map<string, CheckInvalidReferenceFn>,
148
+ state: TraversalState,
149
+): void {
150
+ const imported = specifier.get('imported');
151
+ const specifierName: string =
152
+ imported.node.type === 'Identifier'
153
+ ? imported.node.name
154
+ : imported.node.value;
155
+ const checkFn = importSpecifierChecks.get(specifierName);
156
+ if (checkFn == null) {
157
+ return;
158
+ }
159
+ if (state.shouldInvalidateScopes) {
160
+ state.shouldInvalidateScopes = false;
161
+ state.program.scope.crawl();
162
+ }
163
+
164
+ const local = specifier.get('local');
165
+ const binding = local.scope.getBinding(local.node.name);
166
+ CompilerError.invariant(binding != null, {
167
+ reason: 'Expected binding to be found for import specifier',
168
+ loc: local.node.loc ?? null,
169
+ });
170
+ checkFn(binding.referencePaths, state);
171
+}
172
+
173
+function validateNamespacedImport(
174
+ specifier: NodePath<t.ImportNamespaceSpecifier | t.ImportDefaultSpecifier>,
175
+ importSpecifierChecks: Map<string, CheckInvalidReferenceFn>,
176
+ state: TraversalState,
177
+): void {
178
+ if (state.shouldInvalidateScopes) {
179
+ state.shouldInvalidateScopes = false;
180
+ state.program.scope.crawl();
181
+ }
182
+ const local = specifier.get('local');
183
+ const binding = local.scope.getBinding(local.node.name);
184
+ const defaultCheckFn = importSpecifierChecks.get(DEFAULT_EXPORT);
185
+
186
+ CompilerError.invariant(binding != null, {
187
+ reason: 'Expected binding to be found for import specifier',
188
+ loc: local.node.loc ?? null,
189
+ });
190
+ const filteredReferences = new Map<
191
+ CheckInvalidReferenceFn,
192
+ Array<NodePath<t.Node>>
193
+ >();
194
+ for (const reference of binding.referencePaths) {
195
+ if (defaultCheckFn != null) {
196
+ getOrInsertWith(filteredReferences, defaultCheckFn, () => []).push(
197
+ reference,
198
+ );
199
+ }
200
+ const parent = reference.parentPath;
201
+ if (
202
+ parent != null &&
203
+ parent.isMemberExpression() &&
204
+ parent.get('object') === reference
205
+ ) {
206
+ if (parent.node.computed || parent.node.property.type !== 'Identifier') {
207
+ continue;
208
+ }
209
+ const checkFn = importSpecifierChecks.get(parent.node.property.name);
210
+ if (checkFn != null) {
211
+ getOrInsertWith(filteredReferences, checkFn, () => []).push(parent);
212
+ }
213
+ }
214
+ }
215
+
216
+ for (const [checkFn, references] of filteredReferences) {
217
+ checkFn(references, state);
218
+ }
219
+}
220
+function transformProgram(
221
+ path: NodePath<t.Program>,
222
+
223
+ moduleLoadChecks: Map<string, Map<string, CheckInvalidReferenceFn>>,
224
+ filename: string | null,
225
+ logger: Logger | null,
226
+ transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>,
227
+): void {
228
+ const traversalState: TraversalState = {
229
+ shouldInvalidateScopes: true,
230
+ program: path,
231
+ filename,
232
+ logger,
233
+ transformErrors,
234
+ };
235
+ path.traverse({
236
+ ImportDeclaration(path: NodePath<t.ImportDeclaration>) {
237
+ const importSpecifierChecks = moduleLoadChecks.get(
238
+ path.node.source.value,
239
+ );
240
+ if (importSpecifierChecks == null) {
241
+ return;
242
+ }
243
+ const specifiers = path.get('specifiers');
244
+ for (const specifier of specifiers) {
245
+ if (specifier.isImportSpecifier()) {
246
+ validateImportSpecifier(
247
+ specifier,
248
+ importSpecifierChecks,
249
+ traversalState,
250
+ );
251
+ } else {
252
+ validateNamespacedImport(
253
+ specifier as NodePath<
254
+ t.ImportNamespaceSpecifier | t.ImportDefaultSpecifier
255
+ >,
256
+ importSpecifierChecks,
257
+ traversalState,
258
+ );
259
+ }
260
+ }
261
+ },
262
+ });
263
+}
264
+
265
+function matchCompilerDiagnostic(
266
+ badReference: NodePath<t.Node>,
267
+ transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>,
268
+): string | null {
269
+ for (const {fn, error} of transformErrors) {
270
+ if (fn.isAncestor(badReference)) {
271
+ return error.toString();
272
+ }
273
+ }
274
+ return null;
275
+}
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+1
@@ -1121,6 +1121,7 @@ export class Environment {
1121
moduleName.toLowerCase() === 'react-dom'
1122
);
1123
}
1124
+ static knownReactModules: ReadonlyArray<string> = ['react', 'react-dom'];
1125
1126
getFallthroughPropertyType(
1127
receiver: Type,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.callsite-in-non-react-fn-default-import.expect.md
new
+26
@@ -0,0 +1,26 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @inferEffectDependencies @compilationMode(infer) @panicThreshold(none)
6
+import useMyEffect from 'useEffectWrapper';
7
+
8
+function nonReactFn(arg) {
9
+ useMyEffect(() => [1, 2, arg]);
10
+}
11
+
12
+```
13
+
14
+
15
+## Error
16
+
17
+```
18
+ 3 |
19
+ 4 | function nonReactFn(arg) {
20
+> 5 | useMyEffect(() => [1, 2, arg]);
21
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics. (5:5)
22
+ 6 | }
23
+ 7 |
24
+```
25
+
26
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.callsite-in-non-react-fn-default-import.js
new
+6
@@ -0,0 +1,6 @@
1
+// @inferEffectDependencies @compilationMode(infer) @panicThreshold(none)
2
+import useMyEffect from 'useEffectWrapper';
3
+
4
+function nonReactFn(arg) {
5
+ useMyEffect(() => [1, 2, arg]);
6
+}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.callsite-in-non-react-fn.expect.md
new
+26
@@ -0,0 +1,26 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @inferEffectDependencies @compilationMode(infer) @panicThreshold(none)
6
+import {useEffect} from 'react';
7
+
8
+function nonReactFn(arg) {
9
+ useEffect(() => [1, 2, arg]);
10
+}
11
+
12
+```
13
+
14
+
15
+## Error
16
+
17
+```
18
+ 3 |
19
+ 4 | function nonReactFn(arg) {
20
+> 5 | useEffect(() => [1, 2, arg]);
21
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics. (5:5)
22
+ 6 | }
23
+ 7 |
24
+```
25
+
26
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.callsite-in-non-react-fn.js
new
+6
@@ -0,0 +1,6 @@
1
+// @inferEffectDependencies @compilationMode(infer) @panicThreshold(none)
2
+import {useEffect} from 'react';
3
+
4
+function nonReactFn(arg) {
5
+ useEffect(() => [1, 2, arg]);
6
+}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.non-inlined-effect-fn.expect.md
new
+41
@@ -0,0 +1,41 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @inferEffectDependencies @panicThreshold(none)
6
+import {useEffect} from 'react';
7
+
8
+/**
9
+ * Error on non-inlined effect functions:
10
+ * 1. From the effect hook callee's perspective, it only makes sense
11
+ * to either
12
+ * (a) never hard error (i.e. failing to infer deps is acceptable) or
13
+ * (b) always hard error,
14
+ * regardless of whether the callback function is an inline fn.
15
+ * 2. (Technical detail) it's harder to support detecting cases in which
16
+ * function (pre-Forget transform) was inline but becomes memoized
17
+ */
18
+function Component({foo}) {
19
+ function f() {
20
+ console.log(foo);
21
+ }
22
+
23
+ // No inferred dep array, the argument is not a lambda
24
+ useEffect(f);
25
+}
26
+
27
+```
28
+
29
+
30
+## Error
31
+
32
+```
33
+ 18 |
34
+ 19 | // No inferred dep array, the argument is not a lambda
35
+> 20 | useEffect(f);
36
+ | ^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics. (20:20)
37
+ 21 | }
38
+ 22 |
39
+```
40
+
41
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.non-inlined-effect-fn.js
new
+21
@@ -0,0 +1,21 @@
1
+// @inferEffectDependencies @panicThreshold(none)
2
+import {useEffect} from 'react';
3
+
4
+/**
5
+ * Error on non-inlined effect functions:
6
+ * 1. From the effect hook callee's perspective, it only makes sense
7
+ * to either
8
+ * (a) never hard error (i.e. failing to infer deps is acceptable) or
9
+ * (b) always hard error,
10
+ * regardless of whether the callback function is an inline fn.
11
+ * 2. (Technical detail) it's harder to support detecting cases in which
12
+ * function (pre-Forget transform) was inline but becomes memoized
13
+ */
14
+function Component({foo}) {
15
+ function f() {
16
+ console.log(foo);
17
+ }
18
+
19
+ // No inferred dep array, the argument is not a lambda
20
+ useEffect(f);
21
+}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-import-default-property-useEffect.expect.md
new
+27
@@ -0,0 +1,27 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @inferEffectDependencies @panicThreshold(none)
6
+import React from 'react';
7
+
8
+function NonReactiveDepInEffect() {
9
+ const obj = makeObject_Primitives();
10
+ React.useEffect(() => print(obj));
11
+}
12
+
13
+```
14
+
15
+
16
+## Error
17
+
18
+```
19
+ 4 | function NonReactiveDepInEffect() {
20
+ 5 | const obj = makeObject_Primitives();
21
+> 6 | React.useEffect(() => print(obj));
22
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics. (6:6)
23
+ 7 | }
24
+ 8 |
25
+```
26
+
27
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-import-default-property-useEffect.js
renamed
+1
-1
@@ -1,4 +1,4 @@
1
-// @inferEffectDependencies
1
+// @inferEffectDependencies @panicThreshold(none)
2
import React from 'react';
3
4
function NonReactiveDepInEffect() {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-syntax.expect.md
new
+51
@@ -0,0 +1,51 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @inferEffectDependencies @panicThreshold(none)
6
+import {useSpecialEffect} from 'shared-runtime';
7
+
8
+/**
9
+ * Note that a react compiler-based transform still has limitations on JS syntax.
10
+ * We should surface these as actionable lint / build errors to devs.
11
+ */
12
+function Component({prop1}) {
13
+ 'use memo';
14
+ useSpecialEffect(() => {
15
+ try {
16
+ console.log(prop1);
17
+ } finally {
18
+ console.log('exiting');
19
+ }
20
+ }, [prop1]);
21
+ return <div>{prop1}</div>;
22
+}
23
+
24
+```
25
+
26
+
27
+## Error
28
+
29
+```
30
+ 8 | function Component({prop1}) {
31
+ 9 | 'use memo';
32
+> 10 | useSpecialEffect(() => {
33
+ | ^^^^^^^^^^^^^^^^^^^^^^^^
34
+> 11 | try {
35
+ | ^^^^^^^^^
36
+> 12 | console.log(prop1);
37
+ | ^^^^^^^^^
38
+> 13 | } finally {
39
+ | ^^^^^^^^^
40
+> 14 | console.log('exiting');
41
+ | ^^^^^^^^^
42
+> 15 | }
43
+ | ^^^^^^^^^
44
+> 16 | }, [prop1]);
45
+ | ^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics.. (Bailout reason: Todo: (BuildHIR::lowerStatement) Handle TryStatement without a catch clause (11:15)) (10:16)
46
+ 17 | return <div>{prop1}</div>;
47
+ 18 | }
48
+ 19 |
49
+```
50
+
51
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-syntax.js
new
+18
@@ -0,0 +1,18 @@
1
+// @inferEffectDependencies @panicThreshold(none)
2
+import {useSpecialEffect} from 'shared-runtime';
3
+
4
+/**
5
+ * Note that a react compiler-based transform still has limitations on JS syntax.
6
+ * We should surface these as actionable lint / build errors to devs.
7
+ */
8
+function Component({prop1}) {
9
+ 'use memo';
10
+ useSpecialEffect(() => {
11
+ try {
12
+ console.log(prop1);
13
+ } finally {
14
+ console.log('exiting');
15
+ }
16
+ }, [prop1]);
17
+ return <div>{prop1}</div>;
18
+}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.use-no-memo.expect.md
new
+27
@@ -0,0 +1,27 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @inferEffectDependencies @panicThreshold(none)
6
+import {useEffect} from 'react';
7
+
8
+function Component({propVal}) {
9
+ 'use no memo';
10
+ useEffect(() => [propVal]);
11
+}
12
+
13
+```
14
+
15
+
16
+## Error
17
+
18
+```
19
+ 4 | function Component({propVal}) {
20
+ 5 | 'use no memo';
21
+> 6 | useEffect(() => [propVal]);
22
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics. (6:6)
23
+ 7 | }
24
+ 8 |
25
+```
26
+
27
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.use-no-memo.js
new
+7
@@ -0,0 +1,7 @@
1
+// @inferEffectDependencies @panicThreshold(none)
2
+import {useEffect} from 'react';
3
+
4
+function Component({propVal}) {
5
+ 'use no memo';
6
+ useEffect(() => [propVal]);
7
+}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/infer-effect-dependencies.expect.md
+3
-23
@@ -34,13 +34,6 @@ function Component({foo, bar}) {
34
console.log(bar.qux);
35
});
36
37
- function f() {
38
- console.log(foo);
39
- }
40
-
41
- // No inferred dep array, the argument is not a lambda
42
- useEffect(f);
43
-
37
useEffectWrapper(() => {
38
console.log(foo);
39
});
@@ -58,7 +51,7 @@ import useEffectWrapper from "useEffectWrapper";
51
const moduleNonReactive = 0;
52
53
function Component(t0) {
61
- const $ = _c(14);
54
+ const $ = _c(12);
55
const { foo, bar } = t0;
56
57
const ref = useRef(0);
@@ -119,7 +112,7 @@ function Component(t0) {
112
useEffect(t4, [bar.baz, bar.qux]);
113
let t5;
114
if ($[10] !== foo) {
122
- t5 = function f() {
115
+ t5 = () => {
116
console.log(foo);
117
};
118
$[10] = foo;
@@ -127,20 +120,7 @@ function Component(t0) {
120
} else {
121
t5 = $[11];
122
}
130
- const f = t5;
131
-
132
- useEffect(f);
133
- let t6;
134
- if ($[12] !== foo) {
135
- t6 = () => {
136
- console.log(foo);
137
- };
138
- $[12] = foo;
139
- $[13] = t6;
140
- } else {
141
- t6 = $[13];
142
- }
143
- useEffectWrapper(t6, [foo]);
123
+ useEffectWrapper(t5, [foo]);
124
}
125
126
```
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/infer-effect-dependencies.js
-7
@@ -30,13 +30,6 @@ function Component({foo, bar}) {
30
console.log(bar.qux);
31
});
32
33
- function f() {
34
- console.log(foo);
35
- }
36
-
37
- // No inferred dep array, the argument is not a lambda
38
- useEffect(f);
39
-
33
useEffectWrapper(() => {
34
console.log(foo);
35
});
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/todo.import-default-property-useEffect.expect.md
deleted
-44
@@ -1,44 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-// @inferEffectDependencies
6
-import React from 'react';
7
-
8
-function NonReactiveDepInEffect() {
9
- const obj = makeObject_Primitives();
10
- React.useEffect(() => print(obj));
11
-}
12
-
13
-```
14
-
15
-## Code
16
-
17
-```javascript
18
-import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
19
-import React from "react";
20
-
21
-function NonReactiveDepInEffect() {
22
- const $ = _c(2);
23
- let t0;
24
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
25
- t0 = makeObject_Primitives();
26
- $[0] = t0;
27
- } else {
28
- t0 = $[0];
29
- }
30
- const obj = t0;
31
- let t1;
32
- if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
33
- t1 = () => print(obj);
34
- $[1] = t1;
35
- } else {
36
- t1 = $[1];
37
- }
38
- React.useEffect(t1);
39
-}
40
-
41
-```
42
-
43
-### Eval output
44
-(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.todo-infer-deps-on-retry.expect.md
new
+42
@@ -0,0 +1,42 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @inferEffectDependencies @panicThreshold(none)
6
+import {useRef} from 'react';
7
+import {useSpecialEffect} from 'shared-runtime';
8
+
9
+/**
10
+ * The retry pipeline disables memoization features, which means we need to
11
+ * provide an alternate implementation of effect dependencies which does not
12
+ * rely on memoization.
13
+ */
14
+function useFoo({cond}) {
15
+ const ref = useRef();
16
+ const derived = cond ? ref.current : makeObject();
17
+ useSpecialEffect(() => {
18
+ log(derived);
19
+ }, [derived]);
20
+ return ref;
21
+}
22
+
23
+```
24
+
25
+
26
+## Error
27
+
28
+```
29
+ 11 | const ref = useRef();
30
+ 12 | const derived = cond ? ref.current : makeObject();
31
+> 13 | useSpecialEffect(() => {
32
+ | ^^^^^^^^^^^^^^^^^^^^^^^^
33
+> 14 | log(derived);
34
+ | ^^^^^^^^^^^^^^^^^
35
+> 15 | }, [derived]);
36
+ | ^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics.. (Bailout reason: Invariant: Expected function expression scope to exist (13:15)) (13:15)
37
+ 16 | return ref;
38
+ 17 | }
39
+ 18 |
40
+```
41
+
42
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.todo-infer-deps-on-retry.js
new
+17
@@ -0,0 +1,17 @@
1
+// @inferEffectDependencies @panicThreshold(none)
2
+import {useRef} from 'react';
3
+import {useSpecialEffect} from 'shared-runtime';
4
+
5
+/**
6
+ * The retry pipeline disables memoization features, which means we need to
7
+ * provide an alternate implementation of effect dependencies which does not
8
+ * rely on memoization.
9
+ */
10
+function useFoo({cond}) {
11
+ const ref = useRef();
12
+ const derived = cond ? ref.current : makeObject();
13
+ useSpecialEffect(() => {
14
+ log(derived);
15
+ }, [derived]);
16
+ return ref;
17
+}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.todo-syntax.expect.md
+6
-14
@@ -2,7 +2,7 @@
2
## Input
3
4
```javascript
5
-// @enableFire
5
+// @enableFire @panicThreshold(none)
6
import {fire} from 'react';
7
8
/**
@@ -29,21 +29,13 @@ function Component({prop1}) {
29
## Error
30
31
```
32
- 9 | function Component({prop1}) {
33
- 10 | const foo = () => {
34
-> 11 | try {
35
- | ^^^^^
36
-> 12 | console.log(prop1);
37
- | ^^^^^^^^^^^^^^^^^^^^^^^^^
38
-> 13 | } finally {
39
- | ^^^^^^^^^^^^^^^^^^^^^^^^^
40
-> 14 | console.log('jbrown215');
41
- | ^^^^^^^^^^^^^^^^^^^^^^^^^
42
-> 15 | }
43
- | ^^^^^^ Todo: (BuildHIR::lowerStatement) Handle TryStatement without a catch clause (11:15)
32
16 | };
33
17 | useEffect(() => {
46
- 18 | fire(foo());
34
+> 18 | fire(foo());
35
+ | ^^^^ InvalidReact: [Fire] Untransformed reference to compiler-required feature. Either remove this `fire` call or ensure it is successfully transformed by the compiler. (Bailout reason: Todo: (BuildHIR::lowerStatement) Handle TryStatement without a catch clause (11:15)) (18:18)
36
+ 19 | });
37
+ 20 | }
38
+ 21 |
39
```
40
41
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.todo-syntax.js
+1
-1
@@ -1,4 +1,4 @@
1
-// @enableFire
1
+// @enableFire @panicThreshold(none)
2
import {fire} from 'react';
3
4
/**
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.untransformed-fire-reference.expect.md
new
+23
@@ -0,0 +1,23 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableFire @panicThreshold(none)
6
+import {fire} from 'react';
7
+
8
+console.log(fire == null);
9
+
10
+```
11
+
12
+
13
+## Error
14
+
15
+```
16
+ 2 | import {fire} from 'react';
17
+ 3 |
18
+> 4 | console.log(fire == null);
19
+ | ^^^^ InvalidReact: [Fire] Untransformed reference to compiler-required feature. Either remove this `fire` call or ensure it is successfully transformed by the compiler (4:4)
20
+ 5 |
21
+```
22
+
23
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.untransformed-fire-reference.js
new
+4
@@ -0,0 +1,4 @@
1
+// @enableFire @panicThreshold(none)
2
+import {fire} from 'react';
3
+
4
+console.log(fire == null);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.expect.md
new
+42
@@ -0,0 +1,42 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableFire @panicThreshold(none)
6
+import {fire} from 'react';
7
+
8
+/**
9
+ * TODO: we should eventually distinguish between `use no memo` and `use no
10
+ * compiler` directives. The former should be used to *only* disable memoization
11
+ * features.
12
+ */
13
+function Component({props, bar}) {
14
+ 'use no memo';
15
+ const foo = () => {
16
+ console.log(props);
17
+ };
18
+ useEffect(() => {
19
+ fire(foo(props));
20
+ fire(foo());
21
+ fire(bar());
22
+ });
23
+
24
+ return null;
25
+}
26
+
27
+```
28
+
29
+
30
+## Error
31
+
32
+```
33
+ 13 | };
34
+ 14 | useEffect(() => {
35
+> 15 | fire(foo(props));
36
+ | ^^^^ InvalidReact: [Fire] Untransformed reference to compiler-required feature. Either remove this `fire` call or ensure it is successfully transformed by the compiler (15:15)
37
+ 16 | fire(foo());
38
+ 17 | fire(bar());
39
+ 18 | });
40
+```
41
+
42
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.js
renamed
+6
-1
@@ -1,6 +1,11 @@
1
-// @enableFire
1
+// @enableFire @panicThreshold(none)
2
import {fire} from 'react';
3
4
+/**
5
+ * TODO: we should eventually distinguish between `use no memo` and `use no
6
+ * compiler` directives. The former should be used to *only* disable memoization
7
+ * features.
8
+ */
9
function Component({props, bar}) {
10
'use no memo';
11
const foo = () => {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/no-fire-todo-syntax-shouldnt-throw.expect.md
new
+95
@@ -0,0 +1,95 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableFire @panicThreshold(none)
6
+import {fire} from 'react';
7
+
8
+/**
9
+ * Compilation of this file should succeed.
10
+ */
11
+function NonFireComponent({prop1}) {
12
+ /**
13
+ * This component bails out but does not use fire
14
+ */
15
+ const foo = () => {
16
+ try {
17
+ console.log(prop1);
18
+ } finally {
19
+ console.log('jbrown215');
20
+ }
21
+ };
22
+ useEffect(() => {
23
+ foo();
24
+ });
25
+}
26
+
27
+function FireComponent(props) {
28
+ /**
29
+ * This component uses fire and compiles successfully
30
+ */
31
+ const foo = props => {
32
+ console.log(props);
33
+ };
34
+ useEffect(() => {
35
+ fire(foo(props));
36
+ });
37
+
38
+ return null;
39
+}
40
+
41
+```
42
+
43
+## Code
44
+
45
+```javascript
46
+import { useFire } from "react/compiler-runtime";
47
+import { c as _c } from "react/compiler-runtime"; // @enableFire @panicThreshold(none)
48
+import { fire } from "react";
49
+
50
+/**
51
+ * Compilation of this file should succeed.
52
+ */
53
+function NonFireComponent({ prop1 }) {
54
+ /**
55
+ * This component bails out but does not use fire
56
+ */
57
+ const foo = () => {
58
+ try {
59
+ console.log(prop1);
60
+ } finally {
61
+ console.log("jbrown215");
62
+ }
63
+ };
64
+ useEffect(() => {
65
+ foo();
66
+ });
67
+}
68
+
69
+function FireComponent(props) {
70
+ const $ = _c(3);
71
+
72
+ const foo = _temp;
73
+ const t0 = useFire(foo);
74
+ let t1;
75
+ if ($[0] !== props || $[1] !== t0) {
76
+ t1 = () => {
77
+ t0(props);
78
+ };
79
+ $[0] = props;
80
+ $[1] = t0;
81
+ $[2] = t1;
82
+ } else {
83
+ t1 = $[2];
84
+ }
85
+ useEffect(t1);
86
+ return null;
87
+}
88
+function _temp(props_0) {
89
+ console.log(props_0);
90
+}
91
+
92
+```
93
+
94
+### Eval output
95
+(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/no-fire-todo-syntax-shouldnt-throw.js
new
+35
@@ -0,0 +1,35 @@
1
+// @enableFire @panicThreshold(none)
2
+import {fire} from 'react';
3
+
4
+/**
5
+ * Compilation of this file should succeed.
6
+ */
7
+function NonFireComponent({prop1}) {
8
+ /**
9
+ * This component bails out but does not use fire
10
+ */
11
+ const foo = () => {
12
+ try {
13
+ console.log(prop1);
14
+ } finally {
15
+ console.log('jbrown215');
16
+ }
17
+ };
18
+ useEffect(() => {
19
+ foo();
20
+ });
21
+}
22
+
23
+function FireComponent(props) {
24
+ /**
25
+ * This component uses fire and compiles successfully
26
+ */
27
+ const foo = props => {
28
+ console.log(props);
29
+ };
30
+ useEffect(() => {
31
+ fire(foo(props));
32
+ });
33
+
34
+ return null;
35
+}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/todo-use-no-memo.expect.md
deleted
-47
@@ -1,47 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-// @enableFire
6
-import {fire} from 'react';
7
-
8
-function Component({props, bar}) {
9
- 'use no memo';
10
- const foo = () => {
11
- console.log(props);
12
- };
13
- useEffect(() => {
14
- fire(foo(props));
15
- fire(foo());
16
- fire(bar());
17
- });
18
-
19
- return null;
20
-}
21
-
22
-```
23
-
24
-## Code
25
-
26
-```javascript
27
-// @enableFire
28
-import { fire } from "react";
29
-
30
-function Component({ props, bar }) {
31
- "use no memo";
32
- const foo = () => {
33
- console.log(props);
34
- };
35
- useEffect(() => {
36
- fire(foo(props));
37
- fire(foo());
38
- fire(bar());
39
- });
40
-
41
- return null;
42
-}
43
-
44
-```
45
-
46
-### Eval output
47
-(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts
+32
@@ -274,6 +274,38 @@ const tests: CompilerTestCases = {
274
},
275
],
276
},
277
+ {
278
+ name: 'Pipeline errors are reported',
279
+ code: normalizeIndent`
280
+ import useMyEffect from 'useMyEffect';
281
+ function Component({a}) {
282
+ 'use no memo';
283
+ useMyEffect(() => console.log(a.b));
284
+ return <div>Hello world</div>;
285
+ }
286
+ `,
287
+ options: [
288
+ {
289
+ environment: {
290
+ inferEffectDependencies: [
291
+ {
292
+ function: {
293
+ source: 'useMyEffect',
294
+ importSpecifierName: 'default',
295
+ },
296
+ numRequiredArgs: 1,
297
+ },
298
+ ],
299
+ },
300
+ },
301
+ ],
302
+ errors: [
303
+ {
304
+ message:
305
+ '[InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics.',
306
+ },
307
+ ],
308
+ },
309
],
310
};
311