12
CompilerErrorDetail,
13
ErrorSeverity,
14
} from '../CompilerError';
15
-import {ReactFunctionType} from '../HIR/Environment';
15
+import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';
16
import {CodegenFunction} from '../ReactiveScopes';
17
import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
18
import {isHookDeclaration} from '../Utils/HookDeclaration';
31
suppressionsToCompilerError,
32
} from './Suppression';
33
import {GeneratedSource} from '../HIR';
34
+import {Err, Ok, Result} from '../Utils/Result';
35
36
export type CompilerPass = {
37
opts: PluginOptions;
41
};
42
export const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']);
43
export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);
44
+const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$');
45
44
-export function findDirectiveEnablingMemoization(
46
+export function tryFindDirectiveEnablingMemoization(
47
directives: Array<t.Directive>,
46
-): t.Directive | null {
47
- return (
48
- directives.find(directive =>
49
- OPT_IN_DIRECTIVES.has(directive.value.value),
50
- ) ?? null
48
+ opts: PluginOptions,
49
+): Result<t.Directive | null, CompilerError> {
50
+ const optIn = directives.find(directive =>
51
+ OPT_IN_DIRECTIVES.has(directive.value.value),
52
);
53
+ if (optIn != null) {
54
+ return Ok(optIn);
55
+ }
56
+ const dynamicGating = findDirectivesDynamicGating(directives, opts);
57
+ if (dynamicGating.isOk()) {
58
+ return Ok(dynamicGating.unwrap()?.directive ?? null);
59
+ } else {
60
+ return Err(dynamicGating.unwrapErr());
61
+ }
62
}
63
64
export function findDirectiveDisablingMemoization(
70
) ?? null
71
);
72
}
73
+function findDirectivesDynamicGating(
74
+ directives: Array<t.Directive>,
75
+ opts: PluginOptions,
76
+): Result<
77
+ {
78
+ gating: ExternalFunction;
79
+ directive: t.Directive;
80
+ } | null,
81
+ CompilerError
82
+> {
83
+ if (opts.dynamicGating === null) {
84
+ return Ok(null);
85
+ }
86
+ const errors = new CompilerError();
87
+ const result: Array<{directive: t.Directive; match: string}> = [];
88
+
89
+ for (const directive of directives) {
90
+ const maybeMatch = DYNAMIC_GATING_DIRECTIVE.exec(directive.value.value);
91
+ if (maybeMatch != null && maybeMatch[1] != null) {
92
+ if (t.isValidIdentifier(maybeMatch[1])) {
93
+ result.push({directive, match: maybeMatch[1]});
94
+ } else {
95
+ errors.push({
96
+ reason: `Dynamic gating directive is not a valid JavaScript identifier`,
97
+ description: `Found '${directive.value.value}'`,
98
+ severity: ErrorSeverity.InvalidReact,
99
+ loc: directive.loc ?? null,
100
+ suggestions: null,
101
+ });
102
+ }
103
+ }
104
+ }
105
+ if (errors.hasErrors()) {
106
+ return Err(errors);
107
+ } else if (result.length > 1) {
108
+ const error = new CompilerError();
109
+ error.push({
110
+ reason: `Multiple dynamic gating directives found`,
111
+ description: `Expected a single directive but found [${result
112
+ .map(r => r.directive.value.value)
113
+ .join(', ')}]`,
114
+ severity: ErrorSeverity.InvalidReact,
115
+ loc: result[0].directive.loc ?? null,
116
+ suggestions: null,
117
+ });
118
+ return Err(error);
119
+ } else if (result.length === 1) {
120
+ return Ok({
121
+ gating: {
122
+ source: opts.dynamicGating.source,
123
+ importSpecifierName: result[0].match,
124
+ },
125
+ directive: result[0].directive,
126
+ });
127
+ } else {
128
+ return Ok(null);
129
+ }
130
+}
131
132
function isCriticalError(err: unknown): boolean {
133
return !(err instanceof CompilerError) || err.isCritical();
545
fnType: ReactFunctionType,
546
programContext: ProgramContext,
547
): null | CodegenFunction {
480
- let directives;
548
+ let directives: {
549
+ optIn: t.Directive | null;
550
+ optOut: t.Directive | null;
551
+ };
552
if (fn.node.body.type !== 'BlockStatement') {
482
- directives = {optIn: null, optOut: null};
553
+ directives = {
554
+ optIn: null,
555
+ optOut: null,
556
+ };
557
} else {
558
+ const optIn = tryFindDirectiveEnablingMemoization(
559
+ fn.node.body.directives,
560
+ programContext.opts,
561
+ );
562
+ if (optIn.isErr()) {
563
+ /**
564
+ * If parsing opt-in directive fails, it's most likely that React Compiler
565
+ * was not tested or rolled out on this function. In that case, we handle
566
+ * the error and fall back to the safest option which is to not optimize
567
+ * the function.
568
+ */
569
+ handleError(optIn.unwrapErr(), programContext, fn.node.loc ?? null);
570
+ return null;
571
+ }
572
directives = {
485
- optIn: findDirectiveEnablingMemoization(fn.node.body.directives),
573
+ optIn: optIn.unwrapOr(null),
574
optOut: findDirectiveDisablingMemoization(fn.node.body.directives),
575
};
576
}
747
pass: CompilerPass,
748
programContext: ProgramContext,
749
): void {
662
- const referencedBeforeDeclared =
663
- pass.opts.gating != null
664
- ? getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns)
665
- : null;
750
+ let referencedBeforeDeclared = null;
751
for (const result of compiledFns) {
752
const {kind, originalFn, compiledFn} = result;
753
const transformedFn = createNewFunctionNode(originalFn, compiledFn);
754
programContext.alreadyCompiled.add(transformedFn);
755
671
- if (referencedBeforeDeclared != null && kind === 'original') {
672
- CompilerError.invariant(pass.opts.gating != null, {
673
- reason: "Expected 'gating' import to be present",
674
- loc: null,
675
- });
756
+ let dynamicGating: ExternalFunction | null = null;
757
+ if (originalFn.node.body.type === 'BlockStatement') {
758
+ const result = findDirectivesDynamicGating(
759
+ originalFn.node.body.directives,
760
+ pass.opts,
761
+ );
762
+ if (result.isOk()) {
763
+ dynamicGating = result.unwrap()?.gating ?? null;
764
+ }
765
+ }
766
+ const functionGating = dynamicGating ?? pass.opts.gating;
767
+ if (kind === 'original' && functionGating != null) {
768
+ referencedBeforeDeclared ??=
769
+ getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns);
770
insertGatedFunctionDeclaration(
771
originalFn,
772
transformedFn,
773
programContext,
680
- pass.opts.gating,
774
+ functionGating,
775
referencedBeforeDeclared.has(result),
776
);
777
} else {
827
): ReactFunctionType | null {
828
const hookPattern = pass.opts.environment.hookPattern;
829
if (fn.node.body.type === 'BlockStatement') {
736
- if (findDirectiveEnablingMemoization(fn.node.body.directives) != null)
830
+ const optInDirectives = tryFindDirectiveEnablingMemoization(
831
+ fn.node.body.directives,
832
+ pass.opts,
833
+ );
834
+ if (optInDirectives.unwrapOr(null) != null) {
835
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
836
+ }
837
}
838
839
// Component and hook declarations are known components/hooks