Repository: facebook/react
Pull request: [compiler] Ignore ESLint suppressions when ValidateMemoDeps enabled


With `ValidateExhaustiveMemoDependencies` we can now check exhaustive dependencies for useMemo and useCallback within the compiler, without relying on the separate exhaustive-deps rule. Until now we've bailed out of any component/hook that suppresses this rule, since the suppression _might_ affect a memoization value. Compiling code with incorrect memo deps can change behavior so this wasn't safe. The downside was that a suppression within a useEffect could prevent memoization, even though non-exhaustive deps for effects do not cause problems for memoization specifically.

So here, we change to ignore ESLint suppressions if we have both the compiler's hooks validation and memo deps validations enabled.

Now we just have to test out the new validation and refine before we can enable this by default.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35184).
* #35201
* #35202
* #35192
* #35190
* #35186
* #35185
* __->__ #35184

Changed files:
filename: compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
patch: @@ -400,7 +400,15 @@ export function compileProgram(
    */
   const suppressions = findProgramSuppressions(
     pass.comments,
-    pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS,
+    /*
+     * If the compiler is validating hooks rules and exhaustive memo dependencies, we don't need to check
+     * for React ESLint suppressions
+     */
+    pass.opts.environment.validateExhaustiveMemoizationDependencies &&
+      pass.opts.environment.validateHooksUsage
+      ? null
+      : (pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS),
+    // Always bail on Flow suppressions
     pass.opts.flowSuppressions,
   );
 

filename: compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts
patch: @@ -78,7 +78,7 @@ export function filterSuppressionsThatAffectFunction(
 
 export function findProgramSuppressions(
   programComments: Array<t.Comment>,
-  ruleNames: Array<string>,
+  ruleNames: Array<string> | null,
   flowSuppressions: boolean,
 ): Array<SuppressionRange> {
   const suppressionRanges: Array<SuppressionRange> = [];
@@ -89,7 +89,7 @@ export function findProgramSuppressions(
   let disableNextLinePattern: RegExp | null = null;
   let disablePattern: RegExp | null = null;
   let enablePattern: RegExp | null = null;
-  if (ruleNames.length !== 0) {
+  if (ruleNames != null && ruleNames.length !== 0) {
     const rulePattern = `(${ruleNames.join('|')})`;
     disableNextLinePattern = new RegExp(
       `eslint-disable-next-line ${rulePattern}`,

filename: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-files-with-exhaustive-deps-violation-in-effects.expect.md
patch: @@ -0,0 +1,91 @@
+
+## Input
+
+```javascript
+// @validateExhaustiveMemoizationDependencies
+
+import {useMemo} from 'react';
+import {ValidateMemoization} from 'shared-runtime';
+
+function Component({x}) {
+  useEffect(
+    () => {
+      console.log(x);
+      // eslint-disable-next-line react-hooks/exhaustive-deps
+    },
+    [
+      /* intentionally missing deps */
+    ]
+  );
+
+  const memo = useMemo(() => {
+    return [x];
+  }, [x]);
+
+  return <ValidateMemoization inputs={[x]} output={memo} />;
+}
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
+
+import { useMemo } from "react";
+import { ValidateMemoization } from "shared-runtime";
+
+function Component(t0) {
+  const $ = _c(10);
+  const { x } = t0;
+  let t1;
+  if ($[0] !== x) {
+    t1 = () => {
+      console.log(x);
+    };
+    $[0] = x;
+    $[1] = t1;
+  } else {
+    t1 = $[1];
+  }
+  let t2;
+  if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
+    t2 = [];
+    $[2] = t2;
+  } else {
+    t2 = $[2];
+  }
+  useEffect(t1, t2);
+  let t3;
+  if ($[3] !== x) {
+    t3 = [x];
+    $[3] = x;
+    $[4] = t3;
+  } else {
+    t3 = $[4];
+  }
+  const memo = t3;
+  let t4;
+  if ($[5] !== x) {
+    t4 = [x];
+    $[5] = x;
+    $[6] = t4;
+  } else {
+    t4 = $[6];
+  }
+  let t5;
+  if ($[7] !== memo || $[8] !== t4) {
+    t5 = <ValidateMemoization inputs={t4} output={memo} />;
+    $[7] = memo;
+    $[8] = t4;
+    $[9] = t5;
+  } else {
+    t5 = $[9];
+  }
+  return t5;
+}
+
+```
+      
+### Eval output
+(kind: exception) Fixture not implemented
\ No newline at end of file

filename: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-files-with-exhaustive-deps-violation-in-effects.js
patch: @@ -0,0 +1,22 @@
+// @validateExhaustiveMemoizationDependencies
+
+import {useMemo} from 'react';
+import {ValidateMemoization} from 'shared-runtime';
+
+function Component({x}) {
+  useEffect(
+    () => {
+      console.log(x);
+      // eslint-disable-next-line react-hooks/exhaustive-deps
+    },
+    [
+      /* intentionally missing deps */
+    ]
+  );
+
+  const memo = useMemo(() => {
+    return [x];
+  }, [x]);
+
+  return <ValidateMemoization inputs={[x]} output={memo} />;
+}

filename: compiler/packages/snap/src/runner.ts
patch: @@ -53,8 +53,10 @@ const opts: RunnerOptions = yargs
   .default('worker-threads', true)
   .boolean('watch')
   .describe('watch', 'Run compiler in watch mode, re-running after changes')
+  .alias('w', 'watch')
   .default('watch', false)
   .boolean('update')
+  .alias('u', 'update')
   .describe('update', 'Update fixtures')
   .default('update', false)
   .boolean('filter')