@samitouri / QOS-React-2 / commits / adadc21021

[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() {}