diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 05105ae5973..d6c0e245b84 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -225,8 +225,15 @@ export const EnvironmentConfigSchema = z.object({ /** * Validate that dependencies supplied to effect hooks are exhaustive. + * Can be: + * - 'off': No validation (default) + * - 'all': Validate and report both missing and extra dependencies + * - 'missing-only': Only report missing dependencies + * - 'extra-only': Only report extra/unnecessary dependencies */ - validateExhaustiveEffectDependencies: z.boolean().default(false), + validateExhaustiveEffectDependencies: z + .enum(['off', 'all', 'missing-only', 'extra-only']) + .default('off'), /** * When this is true, rather than pruning existing manual memoization but ensuring or validating diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts index e364c025f38..e72de6c3a3e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts @@ -141,6 +141,7 @@ export function validateExhaustiveDependencies( reactive, startMemo.depsLoc, ErrorCategory.MemoDependencies, + 'all', ); if (diagnostic != null) { error.pushDiagnostic(diagnostic); @@ -159,7 +160,7 @@ export function validateExhaustiveDependencies( onStartMemoize, onFinishMemoize, onEffect: (inferred, manual, manualMemoLoc) => { - if (env.config.validateExhaustiveEffectDependencies === false) { + if (env.config.validateExhaustiveEffectDependencies === 'off') { return; } if (DEBUG) { @@ -195,12 +196,17 @@ export function validateExhaustiveDependencies( }); } } + const effectReportMode = + typeof env.config.validateExhaustiveEffectDependencies === 'string' + ? env.config.validateExhaustiveEffectDependencies + : 'all'; const diagnostic = validateDependencies( Array.from(inferred), manualDeps, reactive, manualMemoLoc, ErrorCategory.EffectExhaustiveDependencies, + effectReportMode, ); if (diagnostic != null) { error.pushDiagnostic(diagnostic); @@ -220,6 +226,7 @@ function validateDependencies( category: | ErrorCategory.MemoDependencies | ErrorCategory.EffectExhaustiveDependencies, + exhaustiveDepsReportMode: 'all' | 'missing-only' | 'extra-only', ): CompilerDiagnostic | null { // Sort dependencies by name and path, with shorter/non-optional paths first inferred.sort((a, b) => { @@ -370,9 +377,20 @@ function validateDependencies( extra.push(dep); } - if (missing.length !== 0 || extra.length !== 0) { + // Filter based on report mode + const filteredMissing = + exhaustiveDepsReportMode === 'extra-only' ? [] : missing; + const filteredExtra = + exhaustiveDepsReportMode === 'missing-only' ? [] : extra; + + if (filteredMissing.length !== 0 || filteredExtra.length !== 0) { let suggestion: CompilerSuggestion | null = null; - if (manualMemoLoc != null && typeof manualMemoLoc !== 'symbol') { + if ( + manualMemoLoc != null && + typeof manualMemoLoc !== 'symbol' && + manualMemoLoc.start.index != null && + manualMemoLoc.end.index != null + ) { suggestion = { description: 'Update dependencies', range: [manualMemoLoc.start.index, manualMemoLoc.end.index], @@ -388,8 +406,13 @@ function validateDependencies( .join(', ')}]`, }; } - const diagnostic = createDiagnostic(category, missing, extra, suggestion); - for (const dep of missing) { + const diagnostic = createDiagnostic( + category, + filteredMissing, + filteredExtra, + suggestion, + ); + for (const dep of filteredMissing) { let reactiveStableValueHint = ''; if (isStableType(dep.identifier)) { reactiveStableValueHint = @@ -402,7 +425,7 @@ function validateDependencies( loc: dep.loc, }); } - for (const dep of extra) { + for (const dep of filteredExtra) { if (dep.root.kind === 'Global') { diagnostic.withDetails({ kind: 'error', diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.exhaustive-deps-effect-events.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.exhaustive-deps-effect-events.expect.md index e2ab53dd05f..d0a626e50ea 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.exhaustive-deps-effect-events.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.exhaustive-deps-effect-events.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @validateExhaustiveEffectDependencies +// @validateExhaustiveEffectDependencies:"all" import {useEffect, useEffectEvent} from 'react'; function Component({x, y, z}) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.exhaustive-deps-effect-events.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.exhaustive-deps-effect-events.js index f8e9c8d700e..03e4a326f95 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.exhaustive-deps-effect-events.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.exhaustive-deps-effect-events.js @@ -1,4 +1,4 @@ -// @validateExhaustiveEffectDependencies +// @validateExhaustiveEffectDependencies:"all" import {useEffect, useEffectEvent} from 'react'; function Component({x, y, z}) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-extra-only.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-extra-only.expect.md new file mode 100644 index 00000000000..f9aac96434a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-extra-only.expect.md @@ -0,0 +1,83 @@ + +## Input + +```javascript +// @validateExhaustiveEffectDependencies:"extra-only" +import {useEffect} from 'react'; + +function Component({x, y, z}) { + // no error: missing dep not reported in extra-only mode + useEffect(() => { + log(x); + }, []); + + // error: extra dep - y + useEffect(() => { + log(x); + }, [x, y]); + + // error: extra dep - y (missing dep - z not reported) + useEffect(() => { + log(x, z); + }, [x, y]); + + // error: extra dep - x.y + useEffect(() => { + log(x); + }, [x.y]); +} + +``` + + +## Error + +``` +Found 3 errors: + +Error: Found extra effect dependencies + +Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects. + +error.invalid-exhaustive-effect-deps-extra-only.ts:13:9 + 11 | useEffect(() => { + 12 | log(x); +> 13 | }, [x, y]); + | ^ Unnecessary dependency `y` + 14 | + 15 | // error: extra dep - y (missing dep - z not reported) + 16 | useEffect(() => { + +Inferred dependencies: `[x]` + +Error: Found extra effect dependencies + +Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects. + +error.invalid-exhaustive-effect-deps-extra-only.ts:18:9 + 16 | useEffect(() => { + 17 | log(x, z); +> 18 | }, [x, y]); + | ^ Unnecessary dependency `y` + 19 | + 20 | // error: extra dep - x.y + 21 | useEffect(() => { + +Inferred dependencies: `[x, z]` + +Error: Found extra effect dependencies + +Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects. + +error.invalid-exhaustive-effect-deps-extra-only.ts:23:6 + 21 | useEffect(() => { + 22 | log(x); +> 23 | }, [x.y]); + | ^^^ Overly precise dependency `x.y`, use `x` instead + 24 | } + 25 | + +Inferred dependencies: `[x]` +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-extra-only.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-extra-only.js new file mode 100644 index 00000000000..fcdc4c1a75e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-extra-only.js @@ -0,0 +1,24 @@ +// @validateExhaustiveEffectDependencies:"extra-only" +import {useEffect} from 'react'; + +function Component({x, y, z}) { + // no error: missing dep not reported in extra-only mode + useEffect(() => { + log(x); + }, []); + + // error: extra dep - y + useEffect(() => { + log(x); + }, [x, y]); + + // error: extra dep - y (missing dep - z not reported) + useEffect(() => { + log(x, z); + }, [x, y]); + + // error: extra dep - x.y + useEffect(() => { + log(x); + }, [x.y]); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-missing-only.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-missing-only.expect.md new file mode 100644 index 00000000000..5a104f529ee --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-missing-only.expect.md @@ -0,0 +1,84 @@ + +## Input + +```javascript +// @validateExhaustiveEffectDependencies:"missing-only" +import {useEffect} from 'react'; + +function Component({x, y, z}) { + // error: missing dep - x + useEffect(() => { + log(x); + }, []); + + // no error: extra dep not reported in missing-only mode + useEffect(() => { + log(x); + }, [x, y]); + + // error: missing dep - z (extra dep - y not reported) + useEffect(() => { + log(x, z); + }, [x, y]); + + // error: missing dep x + useEffect(() => { + log(x); + }, [x.y]); +} + +``` + + +## Error + +``` +Found 3 errors: + +Error: Found missing effect dependencies + +Missing dependencies can cause an effect to fire less often than it should. + +error.invalid-exhaustive-effect-deps-missing-only.ts:7:8 + 5 | // error: missing dep - x + 6 | useEffect(() => { +> 7 | log(x); + | ^ Missing dependency `x` + 8 | }, []); + 9 | + 10 | // no error: extra dep not reported in missing-only mode + +Inferred dependencies: `[x]` + +Error: Found missing effect dependencies + +Missing dependencies can cause an effect to fire less often than it should. + +error.invalid-exhaustive-effect-deps-missing-only.ts:17:11 + 15 | // error: missing dep - z (extra dep - y not reported) + 16 | useEffect(() => { +> 17 | log(x, z); + | ^ Missing dependency `z` + 18 | }, [x, y]); + 19 | + 20 | // error: missing dep x + +Inferred dependencies: `[x, z]` + +Error: Found missing effect dependencies + +Missing dependencies can cause an effect to fire less often than it should. + +error.invalid-exhaustive-effect-deps-missing-only.ts:22:8 + 20 | // error: missing dep x + 21 | useEffect(() => { +> 22 | log(x); + | ^ Missing dependency `x` + 23 | }, [x.y]); + 24 | } + 25 | + +Inferred dependencies: `[x]` +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-missing-only.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-missing-only.js new file mode 100644 index 00000000000..7b333f97a85 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-missing-only.js @@ -0,0 +1,24 @@ +// @validateExhaustiveEffectDependencies:"missing-only" +import {useEffect} from 'react'; + +function Component({x, y, z}) { + // error: missing dep - x + useEffect(() => { + log(x); + }, []); + + // no error: extra dep not reported in missing-only mode + useEffect(() => { + log(x); + }, [x, y]); + + // error: missing dep - z (extra dep - y not reported) + useEffect(() => { + log(x, z); + }, [x, y]); + + // error: missing dep x + useEffect(() => { + log(x); + }, [x.y]); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps.expect.md index 78332c7071b..55dcd921a20 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @validateExhaustiveEffectDependencies +// @validateExhaustiveEffectDependencies:"all" import {useEffect} from 'react'; function Component({x, y, z}) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps.js index b508117acb0..e810fd3f7c8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps.js @@ -1,4 +1,4 @@ -// @validateExhaustiveEffectDependencies +// @validateExhaustiveEffectDependencies:"all" import {useEffect} from 'react'; function Component({x, y, z}) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.expect.md index 2c449ddc5e5..a5b500ddcf5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies +// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies:"all" import { useCallback, useTransition, @@ -69,7 +69,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies +import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies:"all" import { useCallback, useTransition, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.js index 721832eac1b..75ea6edbb34 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.js @@ -1,4 +1,4 @@ -// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies +// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies:"all" import { useCallback, useTransition, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-effect-events.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-effect-events.expect.md index 3b0e696911a..a633db2d8da 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-effect-events.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-effect-events.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @validateExhaustiveEffectDependencies +// @validateExhaustiveEffectDependencies:"all" import {useEffect, useEffectEvent} from 'react'; function Component({x, y, z}) { @@ -30,7 +30,7 @@ function Component({x, y, z}) { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveEffectDependencies +import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveEffectDependencies:"all" import { useEffect, useEffectEvent } from "react"; function Component(t0) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-effect-events.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-effect-events.js index 1c8c710d6cb..ef3853b6e4d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-effect-events.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-effect-events.js @@ -1,4 +1,4 @@ -// @validateExhaustiveEffectDependencies +// @validateExhaustiveEffectDependencies:"all" import {useEffect, useEffectEvent} from 'react'; function Component({x, y, z}) { diff --git a/fixtures/eslint-v9/index.js b/fixtures/eslint-v9/index.js index 5a43235df97..601b68a0328 100644 --- a/fixtures/eslint-v9/index.js +++ b/fixtures/eslint-v9/index.js @@ -167,3 +167,16 @@ function InvalidUseMemo({items}) { const sorted = useMemo(() => [...items].sort(), []); return