@samitouri / QOS-React-2 / commits / 9077ac1f27

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 +}