[babel] Check if the gated component is used before decl
Sathya Gunasekaran committed
Nov 2, 2023 at 11:10 UTC
adadc21021f18ca774cccb8c8c23d729c5ca7a5a
3 files changed
+114
-7
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Program.ts
+89
-7
@@ -50,6 +50,16 @@ function isCriticalError(err: unknown): boolean {
50
return !(err instanceof CompilerError) || err.isCritical();
51
}
52
53
+type BabelFn =
54
+ | NodePath<t.FunctionDeclaration>
55
+ | NodePath<t.FunctionExpression>
56
+ | NodePath<t.ArrowFunctionExpression>;
57
+
58
+type CompileResult = {
59
+ originalFn: BabelFn;
60
+ compiledFn: CodegenFunction;
61
+};
62
+
63
function handleError(
64
pass: CompilerPass,
65
fnLoc: t.SourceLocation | null,
@@ -95,9 +105,7 @@ function handleError(
105
* Mutates the source AST to include a newly Forget-compiled function.
106
*/
107
function insertNewFunctionDeclaration(
98
- originalFn: NodePath<
99
- t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
100
- >,
108
+ originalFn: BabelFn,
109
compiledFn: CodegenFunction,
110
pass: CompilerPass
111
): void {
@@ -230,6 +238,7 @@ export function compileProgram(
238
const lintError = findEslintSuppressions(pass.comments);
239
let hasCriticalError = lintError != null;
240
let hasForgetMutatedOriginalSource: boolean = false;
241
+ const compiledFns: CompileResult[] = [];
242
243
const traverseFunction = (
244
fn:
@@ -273,10 +282,7 @@ export function compileProgram(
282
if (pass.opts.noEmit) {
283
return;
284
} else if (!hasCriticalError) {
276
- // Only insert Forget-ified functions if we have not encountered a critical
277
- // error elsewhere in the file, regardless of bailout mode.
278
- insertNewFunctionDeclaration(fn, compiledFn, pass);
279
- hasForgetMutatedOriginalSource = true;
285
+ compiledFns.push({ originalFn: fn, compiledFn });
286
}
287
};
288
@@ -310,6 +316,22 @@ export function compileProgram(
316
}
317
);
318
319
+ const error = checkFunctionReferencedBeforeDeclarationAtTopLevel(
320
+ program,
321
+ compiledFns.map(({ originalFn }) => originalFn)
322
+ );
323
+ if (error) {
324
+ handleError(pass, null, error);
325
+ return;
326
+ }
327
+
328
+ for (const { originalFn: fn, compiledFn } of compiledFns) {
329
+ // Only insert Forget-ified functions if we have not encountered a critical
330
+ // error elsewhere in the file, regardless of bailout mode.
331
+ insertNewFunctionDeclaration(fn, compiledFn, pass);
332
+ hasForgetMutatedOriginalSource = true;
333
+ }
334
+
335
// Forget compiled the component, we need to update existing imports of unstable_useMemoCache
336
if (hasForgetMutatedOriginalSource) {
337
updateUseMemoCacheImport(program, options);
@@ -579,3 +601,63 @@ function getFunctionName(
601
return null;
602
}
603
}
604
+
605
+function checkFunctionReferencedBeforeDeclarationAtTopLevel(
606
+ program: NodePath<t.Program>,
607
+ fns: BabelFn[]
608
+): CompilerError | null {
609
+ const fnIds = new Set(
610
+ fns
611
+ .map((fn) => getFunctionName(fn))
612
+ .filter(
613
+ (name): name is NodePath<t.Identifier> => !!name && name.isIdentifier()
614
+ )
615
+ .map((name) => name.node)
616
+ );
617
+ const fnNames = new Map([...fnIds].map((id) => [id.name, id]));
618
+ const errors = new CompilerError();
619
+
620
+ program.traverse({
621
+ Identifier(id) {
622
+ const fn = fnNames.get(id.node.name);
623
+ if (fnIds.has(id.node) || !fn) {
624
+ return;
625
+ }
626
+
627
+ const scope = id.scope.getFunctionParent();
628
+ // A null scope means there's no function scope, which means we're at the
629
+ // top level scope.
630
+ if (
631
+ scope === null &&
632
+ id.node.loc &&
633
+ fn.loc &&
634
+ occursBefore(id.node.loc, fn.loc)
635
+ ) {
636
+ errors.pushErrorDetail(
637
+ new CompilerErrorDetail({
638
+ reason: `Encountered ${fn.name} used before declaration which breaks Forget's gating codegen due to hoisting`,
639
+ description:
640
+ "Rewrite the reference to not use hoisting to fix this issue",
641
+ loc: fn.loc ?? null,
642
+ suggestions: null,
643
+ severity: ErrorSeverity.InvalidConfig,
644
+ })
645
+ );
646
+ }
647
+ },
648
+ });
649
+
650
+ return errors.details.length > 0 ? errors : null;
651
+}
652
+
653
+function occursBefore(a: t.SourceLocation, b: t.SourceLocation): boolean {
654
+ if (a.start.line > b.start.line) {
655
+ return false;
656
+ }
657
+
658
+ if (a.start.line < b.start.line) {
659
+ return true;
660
+ }
661
+
662
+ return a.start.column < b.start.column;
663
+}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.gating-use-before-decl.expect.md
new
+20
@@ -0,0 +1,20 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @gating
6
+import { memo } from "react";
7
+
8
+export default memo(Foo);
9
+function Foo() {}
10
+
11
+```
12
+
13
+
14
+## Error
15
+
16
+```
17
+[ReactForget] InvalidConfig: Encountered Foo used before declaration which breaks Forget's gating codegen due to hoisting. Rewrite the reference to not use hoisting to fix this issue (5:5)
18
+```
19
+
20
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.gating-use-before-decl.js
new
+5
@@ -0,0 +1,5 @@
1
+// @gating
2
+import { memo } from "react";
3
+
4
+export default memo(Foo);
5
+function Foo() {}