Repository: facebook/react
Pull request: [compiler][entrypoint] Fix edgecases for noEmit and opt-outs


Title
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33148).
* #33149
* __->__ #33148

Changed files:
filename: compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts
patch: @@ -59,6 +59,7 @@ type ProgramContextOptions = {
   opts: PluginOptions;
   filename: string | null;
   code: string | null;
+  hasModuleScopeOptOut: boolean;
 };
 export class ProgramContext {
   /**
@@ -70,6 +71,7 @@ export class ProgramContext {
   code: string | null;
   reactRuntimeModule: string;
   suppressions: Array<SuppressionRange>;
+  hasModuleScopeOptOut: boolean;
 
   /*
    * This is a hack to work around what seems to be a Babel bug. Babel doesn't
@@ -94,13 +96,15 @@ export class ProgramContext {
     opts,
     filename,
     code,
+    hasModuleScopeOptOut,
   }: ProgramContextOptions) {
     this.scope = program.scope;
     this.opts = opts;
     this.filename = filename;
     this.code = code;
     this.reactRuntimeModule = getReactCompilerRuntimeModule(opts.target);
     this.suppressions = suppressions;
+    this.hasModuleScopeOptOut = hasModuleScopeOptOut;
   }
 
   isHookName(name: string): boolean {

filename: compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
patch: @@ -325,6 +325,8 @@ export function compileProgram(
     filename: pass.filename,
     code: pass.code,
     suppressions,
+    hasModuleScopeOptOut:
+      findDirectiveDisablingMemoization(program.node.directives) != null,
   });
 
   const queue: Array<CompileSource> = findFunctionsToCompile(
@@ -368,7 +370,19 @@ export function compileProgram(
   }
 
   // Avoid modifying the program if we find a program level opt-out
-  if (findDirectiveDisablingMemoization(program.node.directives) != null) {
+  if (programContext.hasModuleScopeOptOut) {
+    if (compiledFns.length > 0) {
+      const error = new CompilerError();
+      error.pushErrorDetail(
+        new CompilerErrorDetail({
+          reason:
+            'Unexpected compiled functions when module scope opt-out is present',
+          severity: ErrorSeverity.Invariant,
+          loc: null,
+        }),
+      );
+      handleError(error, programContext, null);
+    }
     return null;
   }
 
@@ -491,9 +505,10 @@ function processFn(
   }
 
   /**
-   * Otherwise if 'use no forget/memo' is present, we still run the code through the compiler
-   * for validation but we don't mutate the babel AST. This allows us to flag if there is an
-   * unused 'use no forget/memo' directive.
+   * If 'use no forget/memo' is present and we still ran the code through the
+   * compiler for validation, log a skip event and don't mutate the babel AST.
+   * This allows us to flag if there is an unused 'use no forget/memo'
+   * directive.
    */
   if (
     programContext.opts.ignoreUseNoForget === false &&
@@ -518,16 +533,7 @@ function processFn(
     prunedMemoValues: compiledFn.prunedMemoValues,
   });
 
-  /**
-   * Always compile functions with opt in directives.
-   */
-  if (directives.optIn != null) {
-    return compiledFn;
-  } else if (programContext.opts.compilationMode === 'annotation') {
-    /**
-     * If no opt-in directive is found and the compiler is configured in
-     * annotation mode, don't insert the compiled function.
-     */
+  if (programContext.hasModuleScopeOptOut) {
     return null;
   } else if (programContext.opts.noEmit) {
     /**
@@ -541,6 +547,15 @@ function processFn(
       }
     }
     return null;
+  } else if (
+    programContext.opts.compilationMode === 'annotation' &&
+    directives.optIn == null
+  ) {
+    /**
+     * If no opt-in directive is found and the compiler is configured in
+     * annotation mode, don't insert the compiled function.
+     */
+    return null;
   } else {
     return compiledFn;
   }

filename: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-opt-in--no-emit.expect.md
patch: @@ -33,16 +33,15 @@ export const FIXTURE_ENTRYPOINT = {
 import { print } from "shared-runtime";
 import useEffectWrapper from "useEffectWrapper";
 
-function Foo(t0) {
+function Foo({ propVal }) {
   "use memo";
-  const { propVal } = t0;
-
   const arr = [propVal];
-  useEffectWrapper(() => print(arr), [arr]);
+  useEffectWrapper(() => print(arr));
 
   const arr2 = [];
-  useEffectWrapper(() => arr2.push(propVal), [arr2, propVal]);
+  useEffectWrapper(() => arr2.push(propVal));
   arr2.push(2);
+
   return { arr, arr2 };
 }
 

filename: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-bailout-nopanic-shouldnt-outline.expect.md
patch: @@ -20,9 +20,6 @@ function Foo() {
 function Foo() {
   return <button onClick={() => alert("hello!")}>Click me!</button>;
 }
-function _temp() {
-  return alert("hello!");
-}
 
 ```
       

filename: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-noemit.expect.md
patch: @@ -19,22 +19,11 @@ export const FIXTURE_ENTRYPOINT = {
 ## Code
 
 ```javascript
-import { c as _c } from "react/compiler-runtime"; // @noEmit
+// @noEmit
 
 function Foo() {
   "use memo";
-  const $ = _c(1);
-  let t0;
-  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
-    t0 = <button onClick={_temp}>Click me!</button>;
-    $[0] = t0;
-  } else {
-    t0 = $[0];
-  }
-  return t0;
-}
-function _temp() {
-  return alert("hello!");
+  return <button onClick={() => alert("hello!")}>Click me!</button>;
 }
 
 export const FIXTURE_ENTRYPOINT = {