@samitouri / QOS-React / commits / e9a869fbb5

[compiler] Run compiler pipeline on 'use no forget'

This PR updates the babel plugin to continue the compilation pipeline as normal on components/hooks that have been opted out using a directive. Instead, we no longer emit the compiled function when the directive is present. Previously, we would skip over the entire pipeline. By continuing to enter the pipeline, we'll be able to detect if there are unused directives. The end result is: - (no change) 'use forget' will always opt into compilation - (new) 'use no forget' will opt out of compilation but continue to log errors without throwing them. This means that a Program containing multiple functions (some of which are opted out) will continue to compile correctly ghstack-source-id: 5bd85df2f81350cb2c1998a8761b8ed3fec32a40 Pull Request resolved: https://github.com/facebook/react/pull/30720

Lauren Tan committed Aug 16, 2024 at 17:39 UTC e9a869fbb59a634fb9a39c9480ffa34970f35858
5 files changed +136 -62
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts
+6
@@ -165,6 +165,12 @@ export type LoggerEvent =
165 fnLoc: t.SourceLocation | null;
166 detail: Omit<Omit<CompilerErrorDetailOptions, 'severity'>, 'suggestions'>;
167 }
168 + | {
169 + kind: 'CompileSkip';
170 + fnLoc: t.SourceLocation | null;
171 + reason: string;
172 + loc: t.SourceLocation | null;
173 + }
174 | {
175 kind: 'CompileSuccess';
176 fnLoc: t.SourceLocation | null;
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
+84 -62
@@ -43,34 +43,23 @@ export type CompilerPass = {
43 comments: Array<t.CommentBlock | t.CommentLine>;
44 code: string | null;
45 };
46 +const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']);
47 +export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);
48
49 function findDirectiveEnablingMemoization(
50 directives: Array<t.Directive>,
49 -): t.Directive | null {
50 - for (const directive of directives) {
51 - const directiveValue = directive.value.value;
52 - if (directiveValue === 'use forget' || directiveValue === 'use memo') {
53 - return directive;
54 - }
55 - }
56 - return null;
51 +): Array<t.Directive> {
52 + return directives.filter(directive =>
53 + OPT_IN_DIRECTIVES.has(directive.value.value),
54 + );
55 }
56
57 function findDirectiveDisablingMemoization(
58 directives: Array<t.Directive>,
61 - options: PluginOptions,
62 -): t.Directive | null {
63 - for (const directive of directives) {
64 - const directiveValue = directive.value.value;
65 - if (
66 - (directiveValue === 'use no forget' ||
67 - directiveValue === 'use no memo') &&
68 - !options.ignoreUseNoForget
69 - ) {
70 - return directive;
71 - }
72 - }
73 - return null;
59 +): Array<t.Directive> {
60 + return directives.filter(directive =>
61 + OPT_OUT_DIRECTIVES.has(directive.value.value),
62 + );
63 }
64
65 function isCriticalError(err: unknown): boolean {
@@ -102,7 +91,7 @@ export type CompileResult = {
91 compiledFn: CodegenFunction;
92 };
93
105 -function handleError(
94 +function logError(
95 err: unknown,
96 pass: CompilerPass,
97 fnLoc: t.SourceLocation | null,
@@ -131,6 +120,13 @@ function handleError(
120 });
121 }
122 }
123 +}
124 +function handleError(
125 + err: unknown,
126 + pass: CompilerPass,
127 + fnLoc: t.SourceLocation | null,
128 +): void {
129 + logError(err, pass, fnLoc);
130 if (
131 pass.opts.panicThreshold === 'all_errors' ||
132 (pass.opts.panicThreshold === 'critical_errors' && isCriticalError(err)) ||
@@ -393,6 +389,17 @@ export function compileProgram(
389 fn: BabelFn,
390 fnType: ReactFunctionType,
391 ): null | CodegenFunction => {
392 + let optInDirectives: Array<t.Directive> = [];
393 + let optOutDirectives: Array<t.Directive> = [];
394 + if (fn.node.body.type === 'BlockStatement') {
395 + optInDirectives = findDirectiveEnablingMemoization(
396 + fn.node.body.directives,
397 + );
398 + optOutDirectives = findDirectiveDisablingMemoization(
399 + fn.node.body.directives,
400 + );
401 + }
402 +
403 if (lintError != null) {
404 /**
405 * Note that Babel does not attach comment nodes to nodes; they are dangling off of the
@@ -404,7 +411,11 @@ export function compileProgram(
411 fn,
412 );
413 if (suppressionsInFunction.length > 0) {
407 - handleError(lintError, pass, fn.node.loc ?? null);
414 + if (optOutDirectives.length > 0) {
415 + logError(lintError, pass, fn.node.loc ?? null);
416 + } else {
417 + handleError(lintError, pass, fn.node.loc ?? null);
418 + }
419 }
420 }
421
@@ -430,11 +441,50 @@ export function compileProgram(
441 prunedMemoValues: compiledFn.prunedMemoValues,
442 });
443 } catch (err) {
444 + /**
445 + * If an opt out directive is present, log only instead of throwing and don't mark as
446 + * containing a critical error.
447 + */
448 + if (fn.node.body.type === 'BlockStatement') {
449 + if (optOutDirectives.length > 0) {
450 + logError(err, pass, fn.node.loc ?? null);
451 + return null;
452 + }
453 + }
454 hasCriticalError ||= isCriticalError(err);
455 handleError(err, pass, fn.node.loc ?? null);
456 return null;
457 }
458
459 + /**
460 + * Always compile functions with opt in directives.
461 + */
462 + if (optInDirectives.length > 0) {
463 + return compiledFn;
464 + } else if (pass.opts.compilationMode === 'annotation') {
465 + /**
466 + * No opt-in directive in annotation mode, so don't insert the compiled function.
467 + */
468 + return null;
469 + }
470 +
471 + /**
472 + * Otherwise if 'use no forget/memo' is present, we still run the code through the compiler
473 + * for validation but we don't mutate the babel AST. This allows us to flag if there is an
474 + * unused 'use no forget/memo' directive.
475 + */
476 + if (pass.opts.ignoreUseNoForget === false && optOutDirectives.length > 0) {
477 + for (const directive of optOutDirectives) {
478 + pass.opts.logger?.logEvent(pass.filename, {
479 + kind: 'CompileSkip',
480 + fnLoc: fn.node.body.loc ?? null,
481 + reason: `Skipped due to '${directive.value.value}' directive.`,
482 + loc: directive.loc ?? null,
483 + });
484 + }
485 + return null;
486 + }
487 +
488 if (!pass.opts.noEmit && !hasCriticalError) {
489 return compiledFn;
490 }
@@ -481,6 +531,16 @@ export function compileProgram(
531 });
532 }
533
534 + /**
535 + * Do not modify source if there is a module scope level opt out directive.
536 + */
537 + const moduleScopeOptOutDirectives = findDirectiveDisablingMemoization(
538 + program.node.directives,
539 + );
540 + if (moduleScopeOptOutDirectives.length > 0) {
541 + return;
542 + }
543 +
544 if (pass.opts.gating != null) {
545 const error = checkFunctionReferencedBeforeDeclarationAtTopLevel(
546 program,
@@ -596,24 +656,6 @@ function shouldSkipCompilation(
656 }
657 }
658
599 - // Top level "use no forget", skip this file entirely
600 - const useNoForget = findDirectiveDisablingMemoization(
601 - program.node.directives,
602 - pass.opts,
603 - );
604 - if (useNoForget != null) {
605 - pass.opts.logger?.logEvent(pass.filename, {
606 - kind: 'CompileError',
607 - fnLoc: null,
608 - detail: {
609 - severity: ErrorSeverity.Todo,
610 - reason: 'Skipped due to "use no forget" directive.',
611 - loc: useNoForget.loc ?? null,
612 - suggestions: null,
613 - },
614 - });
615 - return true;
616 - }
659 const moduleName = pass.opts.runtimeModule ?? 'react/compiler-runtime';
660 if (hasMemoCacheFunctionImport(program, moduleName)) {
661 return true;
@@ -631,28 +673,8 @@ function getReactFunctionType(
673 ): ReactFunctionType | null {
674 const hookPattern = environment.hookPattern;
675 if (fn.node.body.type === 'BlockStatement') {
634 - // Opt-outs disable compilation regardless of mode
635 - const useNoForget = findDirectiveDisablingMemoization(
636 - fn.node.body.directives,
637 - pass.opts,
638 - );
639 - if (useNoForget != null) {
640 - pass.opts.logger?.logEvent(pass.filename, {
641 - kind: 'CompileError',
642 - fnLoc: fn.node.body.loc ?? null,
643 - detail: {
644 - severity: ErrorSeverity.Todo,
645 - reason: 'Skipped due to "use no forget" directive.',
646 - loc: useNoForget.loc ?? null,
647 - suggestions: null,
648 - },
649 - });
650 - return null;
651 - }
652 - // Otherwise opt-ins enable compilation regardless of mode
653 - if (findDirectiveEnablingMemoization(fn.node.body.directives) != null) {
676 + if (findDirectiveEnablingMemoization(fn.node.body.directives).length > 0)
677 return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
655 - }
678 }
679
680 // Component and hook declarations are known components/hooks
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-forget-with-no-errors.expect.md new
+35
@@ -0,0 +1,35 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component() {
6 + 'use no forget';
7 + return <div>Hello World</div>;
8 +}
9 +
10 +export const FIXTURE_ENTRYPOINT = {
11 + fn: Component,
12 + params: [],
13 + isComponent: true,
14 +};
15 +
16 +```
17 +
18 +## Code
19 +
20 +```javascript
21 +function Component() {
22 + "use no forget";
23 + return <div>Hello World</div>;
24 +}
25 +
26 +export const FIXTURE_ENTRYPOINT = {
27 + fn: Component,
28 + params: [],
29 + isComponent: true,
30 +};
31 +
32 +```
33 +
34 +### Eval output
35 +(kind: ok) <div>Hello World</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-forget-with-no-errors.js new
+10
@@ -0,0 +1,10 @@
1 +function Component() {
2 + 'use no forget';
3 + return <div>Hello World</div>;
4 +}
5 +
6 +export const FIXTURE_ENTRYPOINT = {
7 + fn: Component,
8 + params: [],
9 + isComponent: true,
10 +};
compiler/packages/babel-plugin-react-compiler/src/index.ts
+1
@@ -18,6 +18,7 @@ export {
18 compileProgram,
19 parsePluginOptions,
20 run,
21 + OPT_OUT_DIRECTIVES,
22 type CompilerPipelineValue,
23 type PluginOptions,
24 } from './Entrypoint';