Delegate BabelPlugin to CompilerEntrypoint
Moves the existing logic in BabelPlugin to its own files
Lauren Tan committed
Jun 2, 2023 at 13:06 UTC
b98ee34f4562ac37471e125a78b111bfc4eb92fb
5 files changed
+159
-724
compiler/forget/src/Babel/BabelPlugin.ts
+4
-517
@@ -7,40 +7,9 @@
7
8
/// <reference path="./plugin-syntax-jsx.d.ts" />
9
10
-import type * as BabelCore from "@babel/core";
10
import jsx from "@babel/plugin-syntax-jsx";
12
-import * as t from "@babel/types";
13
-import {
14
- CompilerError,
15
- CompilerErrorDetail,
16
- ErrorSeverity,
17
-} from "../CompilerError";
18
-import { compile } from "../CompilerPipeline";
19
-import { GeneratedSource } from "../HIR";
20
-import { addInstrumentForget } from "./InstrumentForgetBabelPlugin";
21
-import {
22
- ExternalFunction,
23
- PluginOptions,
24
- parsePluginOptions,
25
-} from "./PluginOptions";
26
-
27
-type BabelPluginPass = {
28
- opts: PluginOptions;
29
- filename: string | null;
30
-};
31
-
32
-function hasUseForgetDirective(directive: t.Directive): boolean {
33
- return directive.value.value === "use forget";
34
-}
35
-
36
-function hasAnyUseForgetDirectives(directives: t.Directive[]): boolean {
37
- for (const directive of directives) {
38
- if (hasUseForgetDirective(directive)) {
39
- return true;
40
- }
41
- }
42
- return false;
43
-}
11
+import type * as BabelCore from "@babel/core";
12
+import { compileProgram } from "../CompilerEntrypoint";
13
14
/**
15
* The React Forget Babel Plugin
@@ -50,150 +19,6 @@ function hasAnyUseForgetDirectives(directives: t.Directive[]): boolean {
19
export default function ReactForgetBabelPlugin(
20
_babel: typeof BabelCore
21
): BabelCore.PluginObj {
53
- let hasForgetCompiledCode: boolean = false;
54
-
55
- function visitFn(
56
- fn: BabelCore.NodePath<t.FunctionDeclaration>,
57
- pass: BabelPluginPass
58
- ): void {
59
- try {
60
- const compiled = compile(fn, pass.opts.environment);
61
- if (fn.node.id == null) {
62
- CompilerError.invariant(
63
- "FunctionDeclaration must have a name",
64
- fn.node.loc ?? GeneratedSource
65
- );
66
- }
67
- const originalIdent = fn.node.id;
68
-
69
- if (pass.opts.gating != null) {
70
- // Rename existing function
71
- fn.node.id = addSuffix(fn.node.id, "_uncompiled");
72
-
73
- // Rename and append compiled function
74
- if (compiled.id == null) {
75
- CompilerError.invariant(
76
- "FunctionDeclaration must produce a name",
77
- fn.node.loc ?? GeneratedSource
78
- );
79
- }
80
- compiled.id = addSuffix(compiled.id, "_forget");
81
- const compiledFn = fn.insertAfter(compiled)[0];
82
- compiledFn.skip();
83
-
84
- // Build and append gating test
85
- compiledFn.insertAfter(
86
- buildGatingTest({
87
- originalFnDecl: fn,
88
- compiledIdent: compiled.id,
89
- originalIdent,
90
- gating: pass.opts.gating,
91
- })
92
- );
93
- if (pass.opts.instrumentForget != null) {
94
- const gatingIdentifierName =
95
- pass.opts.instrumentForget.gating.importSpecifierName;
96
- const instrumentFnName =
97
- pass.opts.instrumentForget.instrumentFn.importSpecifierName;
98
- addInstrumentForget(
99
- fn,
100
- originalIdent.name,
101
- gatingIdentifierName,
102
- instrumentFnName
103
- );
104
- addInstrumentForget(
105
- compiledFn,
106
- originalIdent.name,
107
- gatingIdentifierName,
108
- instrumentFnName
109
- );
110
- }
111
- } else {
112
- fn.replaceWith(compiled);
113
- if (pass.opts.instrumentForget != null) {
114
- const gatingIdentifierName =
115
- pass.opts.instrumentForget.gating.importSpecifierName;
116
- const instrumentFnName =
117
- pass.opts.instrumentForget.instrumentFn.importSpecifierName;
118
- addInstrumentForget(
119
- fn,
120
- originalIdent.name,
121
- gatingIdentifierName,
122
- instrumentFnName
123
- );
124
- }
125
- }
126
-
127
- hasForgetCompiledCode = true;
128
- } catch (err) {
129
- if (pass.opts.logger && err) {
130
- pass.opts.logger.logEvent("err", err);
131
- }
132
- /** Always throw if the flag is enabled, otherwise we only throw if the error is critical
133
- * (eg an invariant is broken, meaning the compiler may be buggy). See
134
- * {@link CompilerError.isCritical} for mappings.
135
- * */
136
- if (
137
- pass.opts.panicOnBailout ||
138
- !(err instanceof CompilerError) ||
139
- (err instanceof CompilerError && err.isCritical())
140
- ) {
141
- throw err;
142
- } else {
143
- if (pass.opts.isDev) {
144
- log(err, pass.filename ?? null);
145
- }
146
- }
147
- } finally {
148
- // We are generating a new FunctionDeclaration node, so we must skip over it or this
149
- // traversal will loop infinitely.
150
- fn.skip();
151
- }
152
- }
153
-
154
- const visitor = {
155
- FunctionDeclaration(
156
- fn: BabelCore.NodePath<t.FunctionDeclaration>,
157
- pass: BabelPluginPass
158
- ): void {
159
- if (!shouldCompile(fn, pass)) {
160
- return;
161
- }
162
-
163
- visitFn(fn, pass);
164
- },
165
-
166
- ArrowFunctionExpression(
167
- fn: BabelCore.NodePath<t.ArrowFunctionExpression>,
168
- pass: BabelPluginPass
169
- ): void {
170
- if (!shouldCompile(fn, pass)) {
171
- return;
172
- }
173
-
174
- const loweredFn = buildFunctionDeclaration(fn);
175
- if (loweredFn instanceof CompilerError) {
176
- const error = loweredFn;
177
-
178
- const options = parsePluginOptions(pass.opts);
179
- if (options.logger != null) {
180
- options.logger.logEvent("err", error);
181
- }
182
-
183
- if (options.panicOnBailout || error.isCritical()) {
184
- throw error;
185
- } else {
186
- if (pass.opts.isDev) {
187
- log(error, pass.filename);
188
- }
189
- }
190
- return;
191
- }
192
-
193
- visitFn(loweredFn, pass);
194
- },
195
- };
196
-
22
return {
23
name: "react-forget",
24
inherits: jsx,
@@ -201,347 +26,9 @@ export default function ReactForgetBabelPlugin(
26
// Note: Babel does some "smart" merging of visitors across plugins, so even if A is inserted
27
// prior to B, if A does not have a Program visitor and B does, B will run first. We always
28
// want Forget to run true to source as possible.
204
- Program(path, pass): void {
205
- const options = parsePluginOptions(pass.opts);
206
-
207
- const violations = [];
208
- const fileComments = pass.file.ast.comments;
209
- let fileHasUseForgetDirective = false;
210
- if (Array.isArray(fileComments)) {
211
- for (const comment of fileComments) {
212
- if (
213
- /eslint-disable(-next-line)? react-hooks\/(exhaustive-deps|rules-of-hooks)/.test(
214
- comment.value
215
- )
216
- ) {
217
- violations.push(comment);
218
- }
219
- }
220
- }
221
-
222
- if (violations.length > 0) {
223
- path.traverse({
224
- Directive(path) {
225
- if (hasUseForgetDirective(path.node)) {
226
- fileHasUseForgetDirective = true;
227
- }
228
- },
229
- });
230
-
231
- const reason = `One or more React eslint rules is disabled`;
232
- const error = new CompilerError();
233
- for (const violation of violations) {
234
- if (options.logger != null) {
235
- options.logger.logEvent("err", {
236
- reason,
237
- filename: pass.filename,
238
- violation,
239
- });
240
- }
241
-
242
- error.pushErrorDetail(
243
- new CompilerErrorDetail({
244
- reason,
245
- description: violation.value.trim(),
246
- severity: ErrorSeverity.UnsafeInput,
247
- codeframe: null,
248
- loc: violation.loc ?? null,
249
- })
250
- );
251
- }
252
-
253
- if (fileHasUseForgetDirective) {
254
- if (options.panicOnBailout || error.isCritical()) {
255
- throw error;
256
- } else {
257
- if (options.isDev) {
258
- log(error, pass.filename ?? null);
259
- }
260
- }
261
- }
262
-
263
- return;
264
- }
265
-
266
- path.traverse(visitor, {
267
- ...pass,
268
- opts: { ...pass.opts, ...options },
269
- filename: pass.filename ?? null,
270
- });
271
-
272
- // If there isn't already an import of * as React, insert it so useMemoCache doesn't
273
- // throw
274
- if (hasForgetCompiledCode) {
275
- let didInsertUseMemoCache = false;
276
- let hasExistingReactImport = false;
277
- path.traverse({
278
- CallExpression(callExprPath) {
279
- const callee = callExprPath.get("callee");
280
- const args = callExprPath.get("arguments");
281
- if (
282
- callee.isIdentifier() &&
283
- callee.node.name === "useMemoCache" &&
284
- args.length === 1 &&
285
- args[0].isNumericLiteral()
286
- ) {
287
- didInsertUseMemoCache = true;
288
- }
289
- },
290
- ImportDeclaration(importDeclPath) {
291
- if (isNonNamespacedImportOfReact(importDeclPath)) {
292
- hasExistingReactImport = true;
293
- }
294
- },
295
- });
296
- // If Forget did successfully compile inject/update an import of
297
- // `import {unstable_useMemoCache as useMemoCache} from 'react'` and rename
298
- // `React.unstable_useMemoCache(n)` to `useMemoCache(n)`;
299
- if (didInsertUseMemoCache) {
300
- if (hasExistingReactImport) {
301
- let didUpdateImport = false;
302
- path.traverse({
303
- ImportDeclaration(importDeclPath) {
304
- if (isNonNamespacedImportOfReact(importDeclPath)) {
305
- importDeclPath.pushContainer(
306
- "specifiers",
307
- t.importSpecifier(
308
- t.identifier("useMemoCache"),
309
- t.identifier("unstable_useMemoCache")
310
- )
311
- );
312
- didUpdateImport = true;
313
- }
314
- },
315
- });
316
- if (didUpdateImport === false) {
317
- throw new Error(
318
- "Expected an ImportDeclaration of react in order to update ImportSpecifiers with useMemoCache"
319
- );
320
- }
321
- } else {
322
- path.unshiftContainer(
323
- "body",
324
- t.importDeclaration(
325
- [
326
- t.importSpecifier(
327
- t.identifier("useMemoCache"),
328
- t.identifier("unstable_useMemoCache")
329
- ),
330
- ],
331
- t.stringLiteral("react")
332
- )
333
- );
334
- }
335
- }
336
- // TODO: check for duplicate import specifiers
337
- if (options.gating != null) {
338
- path.unshiftContainer(
339
- "body",
340
- buildImportForExternalFunction(options.gating)
341
- );
342
- }
343
- if (options.instrumentForget != null) {
344
- path.unshiftContainer(
345
- "body",
346
- buildImportForExternalFunction(options.instrumentForget.gating)
347
- );
348
- path.unshiftContainer(
349
- "body",
350
- buildImportForExternalFunction(
351
- options.instrumentForget.instrumentFn
352
- )
353
- );
354
- }
355
- }
29
+ Program(prog, pass): void {
30
+ compileProgram(prog, pass);
31
},
32
},
33
};
34
}
360
-
361
-function shouldCompile(
362
- fn: BabelCore.NodePath<t.FunctionDeclaration | t.ArrowFunctionExpression>,
363
- pass: BabelPluginPass
364
-): boolean {
365
- if (pass.opts.enableOnlyOnUseForgetDirective) {
366
- const body = fn.get("body");
367
- if (!body.isBlockStatement()) {
368
- return false;
369
- }
370
- if (!hasAnyUseForgetDirectives(body.node.directives)) {
371
- return false;
372
- }
373
- }
374
-
375
- if (fn.scope.getProgramParent() !== fn.scope.parent) {
376
- return false;
377
- }
378
-
379
- return true;
380
-}
381
-
382
-function log(error: CompilerError, filename: string | null): void {
383
- const filenameStr = filename ? `in ${filename}` : "";
384
- console.log(
385
- error.details
386
- .map(
387
- (e) =>
388
- `[ReactForget] Skipping compilation of component ${filenameStr}: ${e.printErrorMessage()}`
389
- )
390
- .join("\n")
391
- );
392
-}
393
-
394
-function makeError(
395
- reason: string,
396
- loc: t.SourceLocation | null
397
-): CompilerError {
398
- const error = new CompilerError();
399
- error.pushErrorDetail(
400
- new CompilerErrorDetail({
401
- reason,
402
- description: null,
403
- severity: ErrorSeverity.InvalidInput,
404
- codeframe: null,
405
- loc,
406
- })
407
- );
408
- return error;
409
-}
410
-
411
-function buildFunctionDeclaration(
412
- fn: BabelCore.NodePath<t.ArrowFunctionExpression>
413
-): BabelCore.NodePath<t.FunctionDeclaration> | CompilerError {
414
- if (!fn.parentPath.isVariableDeclarator()) {
415
- return makeError(
416
- "ArrowFunctionExpression must be declared in variable declaration",
417
- fn.node.loc ?? null
418
- );
419
- }
420
- const variableDeclarator = fn.parentPath;
421
-
422
- if (!variableDeclarator.parentPath.isVariableDeclaration()) {
423
- return makeError(
424
- "ArrowFunctionExpression must be a single declaration",
425
- fn.node.loc ?? null
426
- );
427
- }
428
- const variableDeclaration = variableDeclarator.parentPath;
429
-
430
- const id = variableDeclarator.get("id");
431
- if (!id.isIdentifier()) {
432
- return makeError(
433
- "ArrowFunctionExpression must have an id",
434
- fn.node.loc ?? null
435
- );
436
- }
437
-
438
- const rewrittenFn = variableDeclaration.replaceWith(
439
- t.functionDeclaration(
440
- id.node,
441
- fn.node.params,
442
- buildBlockStatement(fn),
443
- fn.node.generator,
444
- fn.node.async
445
- )
446
- )[0];
447
- fn.skip();
448
- return rewrittenFn;
449
-}
450
-
451
-function buildBlockStatement(
452
- fn: BabelCore.NodePath<t.ArrowFunctionExpression>
453
-): t.BlockStatement {
454
- const body = fn.get("body");
455
- if (body.isExpression()) {
456
- const wrappedBody = body.replaceWith(
457
- t.blockStatement([t.returnStatement(body.node)])
458
- )[0];
459
- body.skip();
460
-
461
- return wrappedBody.node;
462
- }
463
-
464
- if (!body.isBlockStatement()) {
465
- CompilerError.invariant(
466
- "Body must be a BlockStatement",
467
- body.node.loc ?? GeneratedSource
468
- );
469
- }
470
- return body.node;
471
-}
472
-
473
-type GatingTestOptions = {
474
- originalFnDecl: BabelCore.NodePath<t.FunctionDeclaration>;
475
- compiledIdent: t.Identifier;
476
- originalIdent: t.Identifier;
477
- gating: ExternalFunction;
478
-};
479
-function buildGatingTest({
480
- originalFnDecl,
481
- compiledIdent,
482
- originalIdent,
483
- gating,
484
-}: GatingTestOptions): t.Node | t.Node[] {
485
- const testVarDecl = t.variableDeclaration("const", [
486
- t.variableDeclarator(
487
- originalIdent,
488
- t.conditionalExpression(
489
- t.callExpression(buildSpecifierIdent(gating), []),
490
- compiledIdent,
491
- originalFnDecl.node.id!
492
- )
493
- ),
494
- ]);
495
-
496
- // Re-export new declaration
497
- const parent = originalFnDecl.parentPath;
498
- if (t.isExportDefaultDeclaration(parent)) {
499
- // Re-add uncompiled function
500
- parent.replaceWith(originalFnDecl)[0].skip();
501
-
502
- // Add test and synthesize new export
503
- return [testVarDecl, t.exportDefaultDeclaration(originalIdent)];
504
- } else if (t.isExportNamedDeclaration(parent)) {
505
- // Re-add uncompiled function
506
- parent.replaceWith(originalFnDecl)[0].skip();
507
-
508
- // Add and export test
509
- return t.exportNamedDeclaration(testVarDecl);
510
- }
511
-
512
- // Just add the test, no need for re-export
513
- return testVarDecl;
514
-}
515
-
516
-function addSuffix(id: t.Identifier, suffix: string): t.Identifier {
517
- return t.identifier(`${id.name}${suffix}`);
518
-}
519
-
520
-function buildImportForExternalFunction(
521
- gating: ExternalFunction
522
-): t.ImportDeclaration {
523
- const specifierIdent = buildSpecifierIdent(gating);
524
- return t.importDeclaration(
525
- [t.importSpecifier(specifierIdent, specifierIdent)],
526
- t.stringLiteral(gating.source)
527
- );
528
-}
529
-
530
-function buildSpecifierIdent(gating: ExternalFunction): t.Identifier {
531
- return t.identifier(gating.importSpecifierName);
532
-}
533
-
534
-/**
535
- * Matches `import { ... } from 'react';`
536
- * but not `import * as React from 'react';`
537
- */
538
-function isNonNamespacedImportOfReact(
539
- importDeclPath: BabelCore.NodePath<t.ImportDeclaration>
540
-): boolean {
541
- return (
542
- importDeclPath.get("source").node.value === "react" &&
543
- importDeclPath
544
- .get("specifiers")
545
- .every((specifier) => specifier.isImportSpecifier())
546
- );
547
-}
compiler/forget/src/Babel/InstrumentForgetBabelPlugin.ts
deleted
-34
@@ -1,34 +0,0 @@
1
-/**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- */
7
-
8
-import { NodePath } from "@babel/core";
9
-import * as t from "@babel/types";
10
-import "./BabelPlugin";
11
-
12
-export function addInstrumentForget(
13
- fn: NodePath<t.FunctionDeclaration>,
14
- fnName: string,
15
- gatingIdentifierName: string,
16
- instrumentFnName: string
17
-): void {
18
- const fnBody = fn.get("body");
19
- // Technically, this is a conditional hook call. However, we expect
20
- // __DEV__ and gatingIdentifier to be runtime constants
21
- const testExpr: t.Node = t.ifStatement(
22
- t.logicalExpression(
23
- "&&",
24
- t.identifier("__DEV__"),
25
- t.identifier(gatingIdentifierName)
26
- ),
27
- t.expressionStatement(
28
- t.callExpression(t.identifier(instrumentFnName), [
29
- t.stringLiteral(fnName),
30
- ])
31
- )
32
- );
33
- fnBody.unshiftContainer("body", testExpr);
34
-}
compiler/forget/src/Babel/RunReactForgetBabelPlugin.ts
+1
-1
@@ -11,7 +11,7 @@ import * as parser from "@babel/parser";
11
import invariant from "invariant";
12
import prettier from "prettier";
13
import ReactForgetBabelPlugin from "./BabelPlugin";
14
-import { PluginOptions } from "./PluginOptions";
14
+import type { PluginOptions } from "../CompilerOptions";
15
16
type ReactForgetBabelPluginResult = {
17
ast: BabelCore.BabelFileResult["ast"];
compiler/forget/src/CompilerEntrypoint.ts
+153
-171
@@ -5,8 +5,7 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import type * as BabelCore from "@babel/core";
9
-import jsx from "@babel/plugin-syntax-jsx";
8
+import { NodePath, PluginPass } from "@babel/core";
9
import * as t from "@babel/types";
10
import {
11
CompilerError,
@@ -20,7 +19,7 @@ import {
19
ExternalFunction,
20
PluginOptions,
21
parsePluginOptions,
23
-} from "./Babel/PluginOptions";
22
+} from "./CompilerOptions";
23
24
type BabelPluginPass = {
25
opts: PluginOptions;
@@ -40,18 +39,14 @@ function hasAnyUseForgetDirectives(directives: t.Directive[]): boolean {
39
return false;
40
}
41
43
-/**
44
- * The React Forget Babel Plugin
45
- * @param {*} _babel
46
- * @returns
47
- */
48
-export default function ReactForgetBabelPlugin(
49
- _babel: typeof BabelCore
50
-): BabelCore.PluginObj {
42
+export function compileProgram(
43
+ program: NodePath<t.Program>,
44
+ pass: PluginPass
45
+): void {
46
let hasForgetCompiledCode: boolean = false;
47
48
function visitFn(
54
- fn: BabelCore.NodePath<t.FunctionDeclaration>,
49
+ fn: NodePath<t.FunctionDeclaration>,
50
pass: BabelPluginPass
51
): void {
52
try {
@@ -151,7 +146,7 @@ export default function ReactForgetBabelPlugin(
146
147
const visitor = {
148
FunctionDeclaration(
154
- fn: BabelCore.NodePath<t.FunctionDeclaration>,
149
+ fn: NodePath<t.FunctionDeclaration>,
150
pass: BabelPluginPass
151
): void {
152
if (!shouldCompile(fn, pass)) {
@@ -162,7 +157,7 @@ export default function ReactForgetBabelPlugin(
157
},
158
159
ArrowFunctionExpression(
165
- fn: BabelCore.NodePath<t.ArrowFunctionExpression>,
160
+ fn: NodePath<t.ArrowFunctionExpression>,
161
pass: BabelPluginPass
162
): void {
163
if (!shouldCompile(fn, pass)) {
@@ -192,172 +187,159 @@ export default function ReactForgetBabelPlugin(
187
},
188
};
189
195
- return {
196
- name: "react-forget",
197
- inherits: jsx,
198
- visitor: {
199
- // Note: Babel does some "smart" merging of visitors across plugins, so even if A is inserted
200
- // prior to B, if A does not have a Program visitor and B does, B will run first. We always
201
- // want Forget to run true to source as possible.
202
- Program(path, pass): void {
203
- const options = parsePluginOptions(pass.opts);
204
-
205
- const violations = [];
206
- const fileComments = pass.file.ast.comments;
207
- let fileHasUseForgetDirective = false;
208
- if (Array.isArray(fileComments)) {
209
- for (const comment of fileComments) {
210
- if (
211
- /eslint-disable(-next-line)? react-hooks\/(exhaustive-deps|rules-of-hooks)/.test(
212
- comment.value
213
- )
214
- ) {
215
- violations.push(comment);
216
- }
217
- }
218
- }
190
+ const options = parsePluginOptions(pass.opts);
191
220
- if (violations.length > 0) {
221
- path.traverse({
222
- Directive(path) {
223
- if (hasUseForgetDirective(path.node)) {
224
- fileHasUseForgetDirective = true;
225
- }
226
- },
227
- });
228
-
229
- const reason = `One or more React eslint rules is disabled`;
230
- const error = new CompilerError();
231
- for (const violation of violations) {
232
- if (options.logger != null) {
233
- options.logger.logEvent("err", {
234
- reason,
235
- filename: pass.filename,
236
- violation,
237
- });
238
- }
192
+ const violations = [];
193
+ const fileComments = pass.file.ast.comments;
194
+ let fileHasUseForgetDirective = false;
195
+ if (Array.isArray(fileComments)) {
196
+ for (const comment of fileComments) {
197
+ if (
198
+ /eslint-disable(-next-line)? react-hooks\/(exhaustive-deps|rules-of-hooks)/.test(
199
+ comment.value
200
+ )
201
+ ) {
202
+ violations.push(comment);
203
+ }
204
+ }
205
+ }
206
240
- error.pushErrorDetail(
241
- new CompilerErrorDetail({
242
- reason,
243
- description: violation.value.trim(),
244
- severity: ErrorSeverity.UnsafeInput,
245
- codeframe: null,
246
- loc: violation.loc ?? null,
247
- })
248
- );
249
- }
207
+ if (violations.length > 0) {
208
+ program.traverse({
209
+ Directive(directive) {
210
+ if (hasUseForgetDirective(directive.node)) {
211
+ fileHasUseForgetDirective = true;
212
+ }
213
+ },
214
+ });
215
+
216
+ const reason = `One or more React eslint rules is disabled`;
217
+ const error = new CompilerError();
218
+ for (const violation of violations) {
219
+ if (options.logger != null) {
220
+ options.logger.logEvent("err", {
221
+ reason,
222
+ filename: pass.filename,
223
+ violation,
224
+ });
225
+ }
226
251
- if (fileHasUseForgetDirective) {
252
- if (options.panicOnBailout || error.isCritical()) {
253
- throw error;
254
- } else {
255
- if (options.isDev) {
256
- log(error, pass.filename ?? null);
257
- }
258
- }
259
- }
227
+ error.pushErrorDetail(
228
+ new CompilerErrorDetail({
229
+ reason,
230
+ description: violation.value.trim(),
231
+ severity: ErrorSeverity.UnsafeInput,
232
+ codeframe: null,
233
+ loc: violation.loc ?? null,
234
+ })
235
+ );
236
+ }
237
261
- return;
238
+ if (fileHasUseForgetDirective) {
239
+ if (options.panicOnBailout || error.isCritical()) {
240
+ throw error;
241
+ } else {
242
+ if (options.isDev) {
243
+ log(error, pass.filename ?? null);
244
}
245
+ }
246
+ }
247
264
- path.traverse(visitor, {
265
- ...pass,
266
- opts: { ...pass.opts, ...options },
267
- filename: pass.filename ?? null,
268
- });
248
+ return;
249
+ }
250
270
- // If there isn't already an import of * as React, insert it so useMemoCache doesn't
271
- // throw
272
- if (hasForgetCompiledCode) {
273
- let didInsertUseMemoCache = false;
274
- let hasExistingReactImport = false;
275
- path.traverse({
276
- CallExpression(callExprPath) {
277
- const callee = callExprPath.get("callee");
278
- const args = callExprPath.get("arguments");
279
- if (
280
- callee.isIdentifier() &&
281
- callee.node.name === "useMemoCache" &&
282
- args.length === 1 &&
283
- args[0].isNumericLiteral()
284
- ) {
285
- didInsertUseMemoCache = true;
286
- }
287
- },
288
- ImportDeclaration(importDeclPath) {
289
- if (isNonNamespacedImportOfReact(importDeclPath)) {
290
- hasExistingReactImport = true;
291
- }
292
- },
293
- });
294
- // If Forget did successfully compile inject/update an import of
295
- // `import {unstable_useMemoCache as useMemoCache} from 'react'` and rename
296
- // `React.unstable_useMemoCache(n)` to `useMemoCache(n)`;
297
- if (didInsertUseMemoCache) {
298
- if (hasExistingReactImport) {
299
- let didUpdateImport = false;
300
- path.traverse({
301
- ImportDeclaration(importDeclPath) {
302
- if (isNonNamespacedImportOfReact(importDeclPath)) {
303
- importDeclPath.pushContainer(
304
- "specifiers",
305
- t.importSpecifier(
306
- t.identifier("useMemoCache"),
307
- t.identifier("unstable_useMemoCache")
308
- )
309
- );
310
- didUpdateImport = true;
311
- }
312
- },
313
- });
314
- if (didUpdateImport === false) {
315
- throw new Error(
316
- "Expected an ImportDeclaration of react in order to update ImportSpecifiers with useMemoCache"
317
- );
318
- }
319
- } else {
320
- path.unshiftContainer(
321
- "body",
322
- t.importDeclaration(
323
- [
324
- t.importSpecifier(
325
- t.identifier("useMemoCache"),
326
- t.identifier("unstable_useMemoCache")
327
- ),
328
- ],
329
- t.stringLiteral("react")
251
+ program.traverse(visitor, {
252
+ ...pass,
253
+ opts: { ...pass.opts, ...options },
254
+ filename: pass.filename ?? null,
255
+ });
256
+
257
+ // If there isn't already an import of * as React, insert it so useMemoCache doesn't
258
+ // throw
259
+ if (hasForgetCompiledCode) {
260
+ let didInsertUseMemoCache = false;
261
+ let hasExistingReactImport = false;
262
+ program.traverse({
263
+ CallExpression(callExprPath) {
264
+ const callee = callExprPath.get("callee");
265
+ const args = callExprPath.get("arguments");
266
+ if (
267
+ callee.isIdentifier() &&
268
+ callee.node.name === "useMemoCache" &&
269
+ args.length === 1 &&
270
+ args[0].isNumericLiteral()
271
+ ) {
272
+ didInsertUseMemoCache = true;
273
+ }
274
+ },
275
+ ImportDeclaration(importDeclPath) {
276
+ if (isNonNamespacedImportOfReact(importDeclPath)) {
277
+ hasExistingReactImport = true;
278
+ }
279
+ },
280
+ });
281
+ // If Forget did successfully compile inject/update an import of
282
+ // `import {unstable_useMemoCache as useMemoCache} from 'react'` and rename
283
+ // `React.unstable_useMemoCache(n)` to `useMemoCache(n)`;
284
+ if (didInsertUseMemoCache) {
285
+ if (hasExistingReactImport) {
286
+ let didUpdateImport = false;
287
+ program.traverse({
288
+ ImportDeclaration(importDeclPath) {
289
+ if (isNonNamespacedImportOfReact(importDeclPath)) {
290
+ importDeclPath.pushContainer(
291
+ "specifiers",
292
+ t.importSpecifier(
293
+ t.identifier("useMemoCache"),
294
+ t.identifier("unstable_useMemoCache")
295
)
296
);
297
+ didUpdateImport = true;
298
}
333
- }
334
- // TODO: check for duplicate import specifiers
335
- if (options.gating != null) {
336
- path.unshiftContainer(
337
- "body",
338
- buildImportForExternalFunction(options.gating)
339
- );
340
- }
341
- if (options.instrumentForget != null) {
342
- path.unshiftContainer(
343
- "body",
344
- buildImportForExternalFunction(options.instrumentForget.gating)
345
- );
346
- path.unshiftContainer(
347
- "body",
348
- buildImportForExternalFunction(
349
- options.instrumentForget.instrumentFn
350
- )
351
- );
352
- }
299
+ },
300
+ });
301
+ if (didUpdateImport === false) {
302
+ throw new Error(
303
+ "Expected an ImportDeclaration of react in order to update ImportSpecifiers with useMemoCache"
304
+ );
305
}
354
- },
355
- },
356
- };
306
+ } else {
307
+ program.unshiftContainer(
308
+ "body",
309
+ t.importDeclaration(
310
+ [
311
+ t.importSpecifier(
312
+ t.identifier("useMemoCache"),
313
+ t.identifier("unstable_useMemoCache")
314
+ ),
315
+ ],
316
+ t.stringLiteral("react")
317
+ )
318
+ );
319
+ }
320
+ }
321
+ // TODO: check for duplicate import specifiers
322
+ if (options.gating != null) {
323
+ program.unshiftContainer(
324
+ "body",
325
+ buildImportForExternalFunction(options.gating)
326
+ );
327
+ }
328
+ if (options.instrumentForget != null) {
329
+ program.unshiftContainer(
330
+ "body",
331
+ buildImportForExternalFunction(options.instrumentForget.gating)
332
+ );
333
+ program.unshiftContainer(
334
+ "body",
335
+ buildImportForExternalFunction(options.instrumentForget.instrumentFn)
336
+ );
337
+ }
338
+ }
339
}
340
341
function shouldCompile(
360
- fn: BabelCore.NodePath<t.FunctionDeclaration | t.ArrowFunctionExpression>,
342
+ fn: NodePath<t.FunctionDeclaration | t.ArrowFunctionExpression>,
343
pass: BabelPluginPass
344
): boolean {
345
if (pass.opts.enableOnlyOnUseForgetDirective) {
@@ -407,8 +389,8 @@ function makeError(
389
}
390
391
function buildFunctionDeclaration(
410
- fn: BabelCore.NodePath<t.ArrowFunctionExpression>
411
-): BabelCore.NodePath<t.FunctionDeclaration> | CompilerError {
392
+ fn: NodePath<t.ArrowFunctionExpression>
393
+): NodePath<t.FunctionDeclaration> | CompilerError {
394
if (!fn.parentPath.isVariableDeclarator()) {
395
return makeError(
396
"ArrowFunctionExpression must be declared in variable declaration",
@@ -447,7 +429,7 @@ function buildFunctionDeclaration(
429
}
430
431
function buildBlockStatement(
450
- fn: BabelCore.NodePath<t.ArrowFunctionExpression>
432
+ fn: NodePath<t.ArrowFunctionExpression>
433
): t.BlockStatement {
434
const body = fn.get("body");
435
if (body.isExpression()) {
@@ -469,7 +451,7 @@ function buildBlockStatement(
451
}
452
453
type GatingTestOptions = {
472
- originalFnDecl: BabelCore.NodePath<t.FunctionDeclaration>;
454
+ originalFnDecl: NodePath<t.FunctionDeclaration>;
455
compiledIdent: t.Identifier;
456
originalIdent: t.Identifier;
457
gating: ExternalFunction;
@@ -534,7 +516,7 @@ function buildSpecifierIdent(gating: ExternalFunction): t.Identifier {
516
* but not `import * as React from 'react';`
517
*/
518
function isNonNamespacedImportOfReact(
537
- importDeclPath: BabelCore.NodePath<t.ImportDeclaration>
519
+ importDeclPath: NodePath<t.ImportDeclaration>
520
): boolean {
521
return (
522
importDeclPath.get("source").node.value === "react" &&
compiler/forget/src/CompilerOptions.ts
renamed
+1
-1
@@ -5,7 +5,7 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import { EnvironmentConfig } from "../HIR/Environment";
8
+import { EnvironmentConfig } from "./HIR/Environment";
9
10
export type ExternalFunction = {
11
/**