Copy files from src/Babel to src/
Lauren Tan committed
Jun 2, 2023 at 13:06 UTC
9077ac1f27008bf3d273c4873ca5edcca155ab05
2 files changed
+578
compiler/forget/src/CompilerEntrypoint.ts
new
+545
@@ -0,0 +1,545 @@
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 type * as BabelCore from "@babel/core";
9
+import jsx from "@babel/plugin-syntax-jsx";
10
+import * as t from "@babel/types";
11
+import {
12
+ CompilerError,
13
+ CompilerErrorDetail,
14
+ ErrorSeverity,
15
+} from "./CompilerError";
16
+import { compile } from "./CompilerPipeline";
17
+import { GeneratedSource } from "./HIR";
18
+import { addInstrumentForget } from "./CompilerInstrumentation";
19
+import {
20
+ ExternalFunction,
21
+ PluginOptions,
22
+ parsePluginOptions,
23
+} from "./Babel/PluginOptions";
24
+
25
+type BabelPluginPass = {
26
+ opts: PluginOptions;
27
+ filename: string | null;
28
+};
29
+
30
+function hasUseForgetDirective(directive: t.Directive): boolean {
31
+ return directive.value.value === "use forget";
32
+}
33
+
34
+function hasAnyUseForgetDirectives(directives: t.Directive[]): boolean {
35
+ for (const directive of directives) {
36
+ if (hasUseForgetDirective(directive)) {
37
+ return true;
38
+ }
39
+ }
40
+ return false;
41
+}
42
+
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 {
51
+ let hasForgetCompiledCode: boolean = false;
52
+
53
+ function visitFn(
54
+ fn: BabelCore.NodePath<t.FunctionDeclaration>,
55
+ pass: BabelPluginPass
56
+ ): void {
57
+ try {
58
+ const compiled = compile(fn, pass.opts.environment);
59
+ if (fn.node.id == null) {
60
+ CompilerError.invariant(
61
+ "FunctionDeclaration must have a name",
62
+ fn.node.loc ?? GeneratedSource
63
+ );
64
+ }
65
+ const originalIdent = fn.node.id;
66
+
67
+ if (pass.opts.gating != null) {
68
+ // Rename existing function
69
+ fn.node.id = addSuffix(fn.node.id, "_uncompiled");
70
+
71
+ // Rename and append compiled function
72
+ if (compiled.id == null) {
73
+ CompilerError.invariant(
74
+ "FunctionDeclaration must produce a name",
75
+ fn.node.loc ?? GeneratedSource
76
+ );
77
+ }
78
+ compiled.id = addSuffix(compiled.id, "_forget");
79
+ const compiledFn = fn.insertAfter(compiled)[0];
80
+ compiledFn.skip();
81
+
82
+ // Build and append gating test
83
+ compiledFn.insertAfter(
84
+ buildGatingTest({
85
+ originalFnDecl: fn,
86
+ compiledIdent: compiled.id,
87
+ originalIdent,
88
+ gating: pass.opts.gating,
89
+ })
90
+ );
91
+ if (pass.opts.instrumentForget != null) {
92
+ const gatingIdentifierName =
93
+ pass.opts.instrumentForget.gating.importSpecifierName;
94
+ const instrumentFnName =
95
+ pass.opts.instrumentForget.instrumentFn.importSpecifierName;
96
+ addInstrumentForget(
97
+ fn,
98
+ originalIdent.name,
99
+ gatingIdentifierName,
100
+ instrumentFnName
101
+ );
102
+ addInstrumentForget(
103
+ compiledFn,
104
+ originalIdent.name,
105
+ gatingIdentifierName,
106
+ instrumentFnName
107
+ );
108
+ }
109
+ } else {
110
+ fn.replaceWith(compiled);
111
+ if (pass.opts.instrumentForget != null) {
112
+ const gatingIdentifierName =
113
+ pass.opts.instrumentForget.gating.importSpecifierName;
114
+ const instrumentFnName =
115
+ pass.opts.instrumentForget.instrumentFn.importSpecifierName;
116
+ addInstrumentForget(
117
+ fn,
118
+ originalIdent.name,
119
+ gatingIdentifierName,
120
+ instrumentFnName
121
+ );
122
+ }
123
+ }
124
+
125
+ hasForgetCompiledCode = true;
126
+ } catch (err) {
127
+ if (pass.opts.logger && err) {
128
+ pass.opts.logger.logEvent("err", err);
129
+ }
130
+ /** Always throw if the flag is enabled, otherwise we only throw if the error is critical
131
+ * (eg an invariant is broken, meaning the compiler may be buggy). See
132
+ * {@link CompilerError.isCritical} for mappings.
133
+ * */
134
+ if (
135
+ pass.opts.panicOnBailout ||
136
+ !(err instanceof CompilerError) ||
137
+ (err instanceof CompilerError && err.isCritical())
138
+ ) {
139
+ throw err;
140
+ } else {
141
+ if (pass.opts.isDev) {
142
+ log(err, pass.filename ?? null);
143
+ }
144
+ }
145
+ } finally {
146
+ // We are generating a new FunctionDeclaration node, so we must skip over it or this
147
+ // traversal will loop infinitely.
148
+ fn.skip();
149
+ }
150
+ }
151
+
152
+ const visitor = {
153
+ FunctionDeclaration(
154
+ fn: BabelCore.NodePath<t.FunctionDeclaration>,
155
+ pass: BabelPluginPass
156
+ ): void {
157
+ if (!shouldCompile(fn, pass)) {
158
+ return;
159
+ }
160
+
161
+ visitFn(fn, pass);
162
+ },
163
+
164
+ ArrowFunctionExpression(
165
+ fn: BabelCore.NodePath<t.ArrowFunctionExpression>,
166
+ pass: BabelPluginPass
167
+ ): void {
168
+ if (!shouldCompile(fn, pass)) {
169
+ return;
170
+ }
171
+
172
+ const loweredFn = buildFunctionDeclaration(fn);
173
+ if (loweredFn instanceof CompilerError) {
174
+ const error = loweredFn;
175
+
176
+ const options = parsePluginOptions(pass.opts);
177
+ if (options.logger != null) {
178
+ options.logger.logEvent("err", error);
179
+ }
180
+
181
+ if (options.panicOnBailout || error.isCritical()) {
182
+ throw error;
183
+ } else {
184
+ if (pass.opts.isDev) {
185
+ log(error, pass.filename);
186
+ }
187
+ }
188
+ return;
189
+ }
190
+
191
+ visitFn(loweredFn, pass);
192
+ },
193
+ };
194
+
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
+ }
219
+
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
+ }
239
+
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
+ }
250
+
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
+ }
260
+
261
+ return;
262
+ }
263
+
264
+ path.traverse(visitor, {
265
+ ...pass,
266
+ opts: { ...pass.opts, ...options },
267
+ filename: pass.filename ?? null,
268
+ });
269
+
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")
330
+ )
331
+ );
332
+ }
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
+ }
353
+ }
354
+ },
355
+ },
356
+ };
357
+}
358
+
359
+function shouldCompile(
360
+ fn: BabelCore.NodePath<t.FunctionDeclaration | t.ArrowFunctionExpression>,
361
+ pass: BabelPluginPass
362
+): boolean {
363
+ if (pass.opts.enableOnlyOnUseForgetDirective) {
364
+ const body = fn.get("body");
365
+ if (!body.isBlockStatement()) {
366
+ return false;
367
+ }
368
+ if (!hasAnyUseForgetDirectives(body.node.directives)) {
369
+ return false;
370
+ }
371
+ }
372
+
373
+ if (fn.scope.getProgramParent() !== fn.scope.parent) {
374
+ return false;
375
+ }
376
+
377
+ return true;
378
+}
379
+
380
+function log(error: CompilerError, filename: string | null): void {
381
+ const filenameStr = filename ? `in ${filename}` : "";
382
+ console.log(
383
+ error.details
384
+ .map(
385
+ (e) =>
386
+ `[ReactForget] Skipping compilation of component ${filenameStr}: ${e.printErrorMessage()}`
387
+ )
388
+ .join("\n")
389
+ );
390
+}
391
+
392
+function makeError(
393
+ reason: string,
394
+ loc: t.SourceLocation | null
395
+): CompilerError {
396
+ const error = new CompilerError();
397
+ error.pushErrorDetail(
398
+ new CompilerErrorDetail({
399
+ reason,
400
+ description: null,
401
+ severity: ErrorSeverity.InvalidInput,
402
+ codeframe: null,
403
+ loc,
404
+ })
405
+ );
406
+ return error;
407
+}
408
+
409
+function buildFunctionDeclaration(
410
+ fn: BabelCore.NodePath<t.ArrowFunctionExpression>
411
+): BabelCore.NodePath<t.FunctionDeclaration> | CompilerError {
412
+ if (!fn.parentPath.isVariableDeclarator()) {
413
+ return makeError(
414
+ "ArrowFunctionExpression must be declared in variable declaration",
415
+ fn.node.loc ?? null
416
+ );
417
+ }
418
+ const variableDeclarator = fn.parentPath;
419
+
420
+ if (!variableDeclarator.parentPath.isVariableDeclaration()) {
421
+ return makeError(
422
+ "ArrowFunctionExpression must be a single declaration",
423
+ fn.node.loc ?? null
424
+ );
425
+ }
426
+ const variableDeclaration = variableDeclarator.parentPath;
427
+
428
+ const id = variableDeclarator.get("id");
429
+ if (!id.isIdentifier()) {
430
+ return makeError(
431
+ "ArrowFunctionExpression must have an id",
432
+ fn.node.loc ?? null
433
+ );
434
+ }
435
+
436
+ const rewrittenFn = variableDeclaration.replaceWith(
437
+ t.functionDeclaration(
438
+ id.node,
439
+ fn.node.params,
440
+ buildBlockStatement(fn),
441
+ fn.node.generator,
442
+ fn.node.async
443
+ )
444
+ )[0];
445
+ fn.skip();
446
+ return rewrittenFn;
447
+}
448
+
449
+function buildBlockStatement(
450
+ fn: BabelCore.NodePath<t.ArrowFunctionExpression>
451
+): t.BlockStatement {
452
+ const body = fn.get("body");
453
+ if (body.isExpression()) {
454
+ const wrappedBody = body.replaceWith(
455
+ t.blockStatement([t.returnStatement(body.node)])
456
+ )[0];
457
+ body.skip();
458
+
459
+ return wrappedBody.node;
460
+ }
461
+
462
+ if (!body.isBlockStatement()) {
463
+ CompilerError.invariant(
464
+ "Body must be a BlockStatement",
465
+ body.node.loc ?? GeneratedSource
466
+ );
467
+ }
468
+ return body.node;
469
+}
470
+
471
+type GatingTestOptions = {
472
+ originalFnDecl: BabelCore.NodePath<t.FunctionDeclaration>;
473
+ compiledIdent: t.Identifier;
474
+ originalIdent: t.Identifier;
475
+ gating: ExternalFunction;
476
+};
477
+function buildGatingTest({
478
+ originalFnDecl,
479
+ compiledIdent,
480
+ originalIdent,
481
+ gating,
482
+}: GatingTestOptions): t.Node | t.Node[] {
483
+ const testVarDecl = t.variableDeclaration("const", [
484
+ t.variableDeclarator(
485
+ originalIdent,
486
+ t.conditionalExpression(
487
+ t.callExpression(buildSpecifierIdent(gating), []),
488
+ compiledIdent,
489
+ originalFnDecl.node.id!
490
+ )
491
+ ),
492
+ ]);
493
+
494
+ // Re-export new declaration
495
+ const parent = originalFnDecl.parentPath;
496
+ if (t.isExportDefaultDeclaration(parent)) {
497
+ // Re-add uncompiled function
498
+ parent.replaceWith(originalFnDecl)[0].skip();
499
+
500
+ // Add test and synthesize new export
501
+ return [testVarDecl, t.exportDefaultDeclaration(originalIdent)];
502
+ } else if (t.isExportNamedDeclaration(parent)) {
503
+ // Re-add uncompiled function
504
+ parent.replaceWith(originalFnDecl)[0].skip();
505
+
506
+ // Add and export test
507
+ return t.exportNamedDeclaration(testVarDecl);
508
+ }
509
+
510
+ // Just add the test, no need for re-export
511
+ return testVarDecl;
512
+}
513
+
514
+function addSuffix(id: t.Identifier, suffix: string): t.Identifier {
515
+ return t.identifier(`${id.name}${suffix}`);
516
+}
517
+
518
+function buildImportForExternalFunction(
519
+ gating: ExternalFunction
520
+): t.ImportDeclaration {
521
+ const specifierIdent = buildSpecifierIdent(gating);
522
+ return t.importDeclaration(
523
+ [t.importSpecifier(specifierIdent, specifierIdent)],
524
+ t.stringLiteral(gating.source)
525
+ );
526
+}
527
+
528
+function buildSpecifierIdent(gating: ExternalFunction): t.Identifier {
529
+ return t.identifier(gating.importSpecifierName);
530
+}
531
+
532
+/**
533
+ * Matches `import { ... } from 'react';`
534
+ * but not `import * as React from 'react';`
535
+ */
536
+function isNonNamespacedImportOfReact(
537
+ importDeclPath: BabelCore.NodePath<t.ImportDeclaration>
538
+): boolean {
539
+ return (
540
+ importDeclPath.get("source").node.value === "react" &&
541
+ importDeclPath
542
+ .get("specifiers")
543
+ .every((specifier) => specifier.isImportSpecifier())
544
+ );
545
+}
compiler/forget/src/CompilerInstrumentation.ts
new
+33
@@ -0,0 +1,33 @@
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
+
11
+export function addInstrumentForget(
12
+ fn: NodePath<t.FunctionDeclaration>,
13
+ fnName: string,
14
+ gatingIdentifierName: string,
15
+ instrumentFnName: string
16
+): void {
17
+ const fnBody = fn.get("body");
18
+ // Technically, this is a conditional hook call. However, we expect
19
+ // __DEV__ and gatingIdentifier to be runtime constants
20
+ const testExpr: t.Node = t.ifStatement(
21
+ t.logicalExpression(
22
+ "&&",
23
+ t.identifier("__DEV__"),
24
+ t.identifier(gatingIdentifierName)
25
+ ),
26
+ t.expressionStatement(
27
+ t.callExpression(t.identifier(instrumentFnName), [
28
+ t.stringLiteral(fnName),
29
+ ])
30
+ )
31
+ );
32
+ fnBody.unshiftContainer("body", testExpr);
33
+}