Compare commits

...

1 Commits

Author SHA1 Message Date
Joe Savona
12d3951727 [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.
2025-11-20 22:02:46 -08:00
5 changed files with 126 additions and 3 deletions

View File

@@ -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,
);

View File

@@ -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}`,

View File

@@ -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

View File

@@ -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} />;
}

View File

@@ -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')