Skip to content

Commit e9a869f

Browse files
committed
[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: 5bd85df Pull Request resolved: #30720
1 parent 7b41cdc commit e9a869f

File tree

5 files changed

+136
-62
lines changed

5 files changed

+136
-62
lines changed

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ export type LoggerEvent =
165165
fnLoc: t.SourceLocation | null;
166166
detail: Omit<Omit<CompilerErrorDetailOptions, 'severity'>, 'suggestions'>;
167167
}
168+
| {
169+
kind: 'CompileSkip';
170+
fnLoc: t.SourceLocation | null;
171+
reason: string;
172+
loc: t.SourceLocation | null;
173+
}
168174
| {
169175
kind: 'CompileSuccess';
170176
fnLoc: t.SourceLocation | null;

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts

Lines changed: 84 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -43,34 +43,23 @@ export type CompilerPass = {
4343
comments: Array<t.CommentBlock | t.CommentLine>;
4444
code: string | null;
4545
};
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']);
4648

4749
function findDirectiveEnablingMemoization(
4850
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+
);
5755
}
5856

5957
function findDirectiveDisablingMemoization(
6058
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+
);
7463
}
7564

7665
function isCriticalError(err: unknown): boolean {
@@ -102,7 +91,7 @@ export type CompileResult = {
10291
compiledFn: CodegenFunction;
10392
};
10493

105-
function handleError(
94+
function logError(
10695
err: unknown,
10796
pass: CompilerPass,
10897
fnLoc: t.SourceLocation | null,
@@ -131,6 +120,13 @@ function handleError(
131120
});
132121
}
133122
}
123+
}
124+
function handleError(
125+
err: unknown,
126+
pass: CompilerPass,
127+
fnLoc: t.SourceLocation | null,
128+
): void {
129+
logError(err, pass, fnLoc);
134130
if (
135131
pass.opts.panicThreshold === 'all_errors' ||
136132
(pass.opts.panicThreshold === 'critical_errors' && isCriticalError(err)) ||
@@ -393,6 +389,17 @@ export function compileProgram(
393389
fn: BabelFn,
394390
fnType: ReactFunctionType,
395391
): 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+
396403
if (lintError != null) {
397404
/**
398405
* Note that Babel does not attach comment nodes to nodes; they are dangling off of the
@@ -404,7 +411,11 @@ export function compileProgram(
404411
fn,
405412
);
406413
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+
}
408419
}
409420
}
410421

@@ -430,11 +441,50 @@ export function compileProgram(
430441
prunedMemoValues: compiledFn.prunedMemoValues,
431442
});
432443
} 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+
}
433454
hasCriticalError ||= isCriticalError(err);
434455
handleError(err, pass, fn.node.loc ?? null);
435456
return null;
436457
}
437458

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+
438488
if (!pass.opts.noEmit && !hasCriticalError) {
439489
return compiledFn;
440490
}
@@ -481,6 +531,16 @@ export function compileProgram(
481531
});
482532
}
483533

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+
484544
if (pass.opts.gating != null) {
485545
const error = checkFunctionReferencedBeforeDeclarationAtTopLevel(
486546
program,
@@ -596,24 +656,6 @@ function shouldSkipCompilation(
596656
}
597657
}
598658

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-
}
617659
const moduleName = pass.opts.runtimeModule ?? 'react/compiler-runtime';
618660
if (hasMemoCacheFunctionImport(program, moduleName)) {
619661
return true;
@@ -631,28 +673,8 @@ function getReactFunctionType(
631673
): ReactFunctionType | null {
632674
const hookPattern = environment.hookPattern;
633675
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)
654677
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
655-
}
656678
}
657679

658680
// Component and hook declarations are known components/hooks
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export {
1818
compileProgram,
1919
parsePluginOptions,
2020
run,
21+
OPT_OUT_DIRECTIVES,
2122
type CompilerPipelineValue,
2223
type PluginOptions,
2324
} from './Entrypoint';

0 commit comments

Comments
 (0)