Compare commits

..

15 Commits

Author SHA1 Message Date
Joe Savona
28a2150dc7 [compiler][snap] Fix for filter mode with nested files, 'error.' prefix
Fixes some issues i ran into w my recent snap changes:
* Correctly match against patterns that contain subdirectories, eg `fbt/fbt-call`
* When checking if the input pattern has an extension, only prune known supported extensions. Our convention of `error.<name>` for fixtures that error makes the rest of the test name look like an extension to `path.extname()`.

Tested with lots of different patterns including `error.` examples at the top level and in nested directories, etc.
2025-11-25 15:32:46 -08:00
Joseph Savona
fb18ad3fd3 [compiler] Exhaustive deps: extra tests, improve diagnostic (#35213)
First, this adds some more tests and organizes them into an
`exhaustive-deps/` subdirectory.

Second, the diagnostics are overhauled. For each memo block we now
report a single diagnostic which summarizes the issue, plus individual
errors for each missing/extra dependency. Within the extra deps, we
distinguish whether it's truly extra vs whether its just a more (too)
precise version of an inferred dep. For example, if you depend on
`x.y.z` but the inferred dep was `x.y`. Finally, we print the full
inferred deps at the end as a hint (it's also a suggestion, but this
makes it more clear what would be suggested).
2025-11-25 12:09:09 -08:00
Joseph Savona
ddff35441a [compiler] Enable validateExhaustiveMemoizationDependencies by default (#35201)
Enables `@validateExhaustiveMemoizationDependencies` feature flag by
default, and disables it in select tests that failed due to the change.
Some of our tests intentionally use incorrect memo dependencies in order
to test edge cases.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35201).
* #35213
* __->__ #35201
2025-11-25 12:07:55 -08:00
Joseph Savona
d39a1d6b63 [compiler] Distingush optional/extraneous deps (#35204)
In ValidateExhaustiveDependencies, I previously changed to allow
extraneous dependencies as long as they were non-reactive. Here we make
that more precise, and distinguish between values that are definitely
referenced in the memo function but optional as dependencies vs values
that are not even referenced in the memo function. The latter now error
as extraneous even if they're non-reactive. This also turned up a case
where constant-folded primitives could show up as false positives of the
latter category, so now we track manual deps which quality for constant
folding and don't error on them.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35204).
* #35213
* #35201
* __->__ #35204
2025-11-25 12:06:25 -08:00
Joseph Savona
16e16ec6ff [compiler] Script to enable a feature by default and update tests (#35202)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35202).
* #35201
* __->__ #35202
2025-11-24 12:21:35 -08:00
Joseph Savona
9599e7a787 [compiler] Adjustments to exhaustive deps messages, disable the lint rule (#35192)
Similar to ValidateHookUsage, we implement this check in the compiler
for safety but (for now) continue to rely on the existing rule for
actually reporting errors to users.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35192).
* #35201
* #35202
* __->__ #35192
2025-11-24 12:20:12 -08:00
Joseph Savona
67c1487ffd [compiler] Allow extraneous non-reactive locals (#35190)
The existing exhaustive-deps rule allows omitting non-reactive
dependencies, even if they're not memoized. Conceptually, if a value is
non-reactive then it cannot semantically change. Even if the value is a
new object, that object represents the exact same value and doesn't
necessitate redoing downstream computation. Thus its fine to exclude
nonreactive dependencies, whether they're a stable type or not.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35190).
* #35201
* #35202
* #35192
* __->__ #35190
2025-11-24 12:18:49 -08:00
Joseph Savona
454e01e603 [compiler] Allow manual dependencies to have different optionality than inferred deps (#35186)
Since adding this validation we've already changed our inference to use
knowledge from manual memoization to inform when values are frozen and
which values are non-nullable. To align with that, if the user chooses
to use different optionality btw the deps and the memo block/callback,
that's fine. The key is that eg `x?.y` will invalidate whenever `x.y`
would, so from a memoization correctness perspective its fine. It's not
our job to be a type checker: if a value is potentially nullable, it
should likely use a nullable property access in both places but
TypeScript/Flow can check that.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35186).
* #35201
* #35202
* #35192
* #35190
* __->__ #35186
2025-11-24 12:17:03 -08:00
Joseph Savona
c9a8cf3411 [compiler] Allow nonreactive stable types as extraneous deps (#35185)
When checking ValidateExhaustiveDeps internally, this seems to be the
most common case that it flags. The current exhaustive-deps rule allows
extraneous deps if they are a set of stable types. So here we reuse our
existing isStableType() util in the compiler to allow this case.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35185).
* #35201
* #35202
* #35192
* #35190
* #35186
* __->__ #35185
2025-11-24 12:15:06 -08:00
Joseph Savona
fca172e3f3 [compiler] Ignore ESLint suppressions when ValidateMemoDeps enabled (#35184)
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
2025-11-24 12:12:49 -08:00
emily8rown
fd524fe02a [DevTools] hotkey to start/stop profiling (#35160)
## Summary

The built-in browser profiler supports starting/stopping with Cmd+E. For
Symmetry this adds the same hotkey for react devtools profiler.

## How did you test this change?
yarn build:\<browser name\> 
yarn run test:\<browser name\>

<img width="483" height="135" alt="Screenshot 2025-11-17 at 14 30 34"
src="https://github.com/user-attachments/assets/426939aa-15da-4c21-87a4-e949e6949482"
/>

firefox:

https://github.com/user-attachments/assets/6f225b90-828f-4e79-a364-59d6bc942f83

edge:

https://github.com/user-attachments/assets/5b2e9242-f0e8-481b-99a2-2dd78099f3ac

chrome:

https://github.com/user-attachments/assets/790aab02-2867-4499-aec1-e32e38c763f9

---------

Co-authored-by: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com>
2025-11-21 11:37:10 -05:00
Joseph Savona
40b4a5bf71 [compiler] ValidateExhaustiveDeps disallows unnecessary non-reactive deps (#34472)
Just to be consistent, we disallow unnecessary deps even if they're
known to be non-reactive.
2025-11-20 19:30:35 -08:00
Joseph Savona
df75af4edc [compiler] Auto-fix for non-exhaustive deps (#34471)
Records more information in DropManualMemoization so that we know the
full span of the manual dependencies array (if present). This allows
ValidateExhaustiveDeps to include a suggestion with the correct deps.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34471).
* #34472
* __->__ #34471
2025-11-20 19:28:08 -08:00
Joseph Savona
bcc3fd8b05 [compiler] Implement exhaustive dependency checking for manual memoization (#34394)
The compiler currently drops manual memoization and rewrites it using
its own inference. If the existing manual memo dependencies has missing
or extra dependencies, compilation can change behavior by running the
computation more often (if deps were missing) or less often (if there
were extra deps). We currently address this by relying on the developer
to use the ESLint plugin and have `eslint-disable-next-line
react-hooks/exhaustive-deps` suppressions in their code. If a
suppression exists, we skip compilation.

But not everyone is using the linter! Relying on the linter is also
imprecise since it forces us to bail out on exhaustive-deps checks that
only effect (ahem) effects — and while it isn't good to have incorrect
deps on effects, it isn't a problem for compilation.

So this PR is a rough sketch of validating manual memoization
dependencies in the compiler. Long-term we could use this to also check
effect deps and replace the ExhaustiveDeps lint rule, but for now I'm
focused specifically on manual memoization use-cases. If this works, we
can stop bailing out on ESLint suppressions, since the compiler will
implement all the appropriate checks (we already check rules of hooks).

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34394).
* #34472
* #34471
* __->__ #34394
2025-11-20 19:26:26 -08:00
Joseph Savona
50e7ec8a69 [compiler] Deprecate noEmit, add outputMode (#35112)
This deprecates the `noEmit: boolean` flag and adds `outputMode:
'client' | 'client-no-memo' | 'ssr' | 'lint'` as the replacement.
OutputMode defaults to null and takes precedence if specified, otherwise
we use 'client' mode for noEmit=false and 'lint' mode for noEmit=true.

Key points:
* Retrying failed compilation switches from 'client' mode to
'client-no-memo'
* Validations are enabled behind
Environment.proto.shouldEnableValidations, enabled for all modes except
'client-no-memo'. Similar for dropping manual memoization.
* OptimizeSSR is now gated by the outputMode==='ssr', not a feature flag
* Creation of reactive scopes, and related codegen logic, is now gated
by outputMode==='client'
2025-11-20 15:12:40 -08:00
171 changed files with 2903 additions and 295 deletions

View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(node scripts/enable-feature-flag.js:*)"
],
"deny": [],
"ask": []
}
}

View File

@@ -304,6 +304,30 @@ export class CompilerError extends Error {
disabledDetails: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
printedMessage: string | null = null;
static simpleInvariant(
condition: unknown,
options: {
reason: CompilerDiagnosticOptions['reason'];
description?: CompilerDiagnosticOptions['description'];
loc: SourceLocation;
},
): asserts condition {
if (!condition) {
const errors = new CompilerError();
errors.pushDiagnostic(
CompilerDiagnostic.create({
reason: options.reason,
description: options.description ?? null,
category: ErrorCategory.Invariant,
}).withDetails({
kind: 'error',
loc: options.loc,
message: options.reason,
}),
);
throw errors;
}
}
static invariant(
condition: unknown,
options: Omit<CompilerDiagnosticOptions, 'category'>,
@@ -576,7 +600,8 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
case ErrorCategory.Suppression:
case ErrorCategory.Syntax:
case ErrorCategory.UseMemo:
case ErrorCategory.VoidUseMemo: {
case ErrorCategory.VoidUseMemo:
case ErrorCategory.MemoDependencies: {
heading = 'Error';
break;
}
@@ -634,6 +659,10 @@ export enum ErrorCategory {
* Checks that manual memoization is preserved
*/
PreserveManualMemo = 'PreserveManualMemo',
/**
* Checks for exhaustive useMemo/useCallback dependencies without extraneous values
*/
MemoDependencies = 'MemoDependencies',
/**
* Checks for known incompatible libraries
*/
@@ -1031,6 +1060,24 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
preset: LintRulePreset.RecommendedLatest,
};
}
case ErrorCategory.MemoDependencies: {
return {
category,
severity: ErrorSeverity.Error,
name: 'memo-dependencies',
description:
'Validates that useMemo() and useCallback() specify comprehensive dependencies without extraneous values. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
/**
* TODO: the "MemoDependencies" rule largely reimplements the "exhaustive-deps" non-compiler rule,
* allowing the compiler to ensure it does not regress change behavior due to different dependencies.
* We previously relied on the source having ESLint suppressions for any exhaustive-deps violations,
* but it's more reliable to verify it within the compiler.
*
* Long-term we should de-duplicate these implementations.
*/
preset: LintRulePreset.Off,
};
}
case ErrorCategory.IncompatibleLibrary: {
return {
category,

View File

@@ -105,6 +105,7 @@ import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDe
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
import {optimizeForSSR} from '../Optimization/OptimizeForSSR';
import {validateExhaustiveDependencies} from '../Validation/ValidateExhaustiveDependencies';
import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
export type CompilerPipelineValue =
@@ -302,6 +303,13 @@ function runWithEnvironment(
inferReactivePlaces(hir);
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
if (env.enableValidations) {
if (env.config.validateExhaustiveMemoizationDependencies) {
// NOTE: this relies on reactivity inference running first
validateExhaustiveDependencies(hir).unwrap();
}
}
rewriteInstructionKindsBasedOnReassignment(hir);
log({
kind: 'hir',

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

@@ -218,6 +218,11 @@ export const EnvironmentConfigSchema = z.object({
*/
validatePreserveExistingMemoizationGuarantees: z.boolean().default(true),
/**
* Validate that dependencies supplied to manual memoization calls are exhaustive.
*/
validateExhaustiveMemoizationDependencies: z.boolean().default(true),
/**
* When this is true, rather than pruning existing manual memoization but ensuring or validating
* that the memoized values remain memoized, the compiler will simply not prune existing calls to

View File

@@ -803,9 +803,11 @@ export type ManualMemoDependency = {
| {
kind: 'NamedLocal';
value: Place;
constant: boolean;
}
| {kind: 'Global'; identifierName: string};
path: DependencyPath;
loc: SourceLocation;
};
export type StartMemoize = {
@@ -817,6 +819,11 @@ export type StartMemoize = {
* (e.g. useMemo without a second arg)
*/
deps: Array<ManualMemoDependency> | null;
/**
* The source location of the dependencies argument. Used for
* emitting diagnostics with a suggested replacement
*/
depsLoc: SourceLocation | null;
loc: SourceLocation;
};
export type FinishMemoize = {
@@ -1680,6 +1687,28 @@ export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
)
);
}
export function isSubPath(
subpath: DependencyPath,
path: DependencyPath,
): boolean {
return (
subpath.length <= path.length &&
subpath.every(
(item, ix) =>
item.property === path[ix].property &&
item.optional === path[ix].optional,
)
);
}
export function isSubPathIgnoringOptionals(
subpath: DependencyPath,
path: DependencyPath,
): boolean {
return (
subpath.length <= path.length &&
subpath.every((item, ix) => item.property === path[ix].property)
);
}
export function getPlaceScope(
id: InstructionId,

View File

@@ -42,7 +42,7 @@ type IdentifierSidemap = {
functions: Map<IdentifierId, TInstruction<FunctionExpression>>;
manualMemos: Map<IdentifierId, ManualMemoCallee>;
react: Set<IdentifierId>;
maybeDepsLists: Map<IdentifierId, Array<Place>>;
maybeDepsLists: Map<IdentifierId, {loc: SourceLocation; deps: Array<Place>}>;
maybeDeps: Map<IdentifierId, ManualMemoDependency>;
optionals: Set<IdentifierId>;
};
@@ -65,6 +65,7 @@ export function collectMaybeMemoDependencies(
identifierName: value.binding.name,
},
path: [],
loc: value.loc,
};
}
case 'PropertyLoad': {
@@ -74,6 +75,7 @@ export function collectMaybeMemoDependencies(
root: object.root,
// TODO: determine if the access is optional
path: [...object.path, {property: value.property, optional}],
loc: value.loc,
};
}
break;
@@ -92,8 +94,10 @@ export function collectMaybeMemoDependencies(
root: {
kind: 'NamedLocal',
value: {...value.place},
constant: false,
},
path: [],
loc: value.place.loc,
};
}
break;
@@ -159,10 +163,10 @@ function collectTemporaries(
}
case 'ArrayExpression': {
if (value.elements.every(e => e.kind === 'Identifier')) {
sidemap.maybeDepsLists.set(
instr.lvalue.identifier.id,
value.elements as Array<Place>,
);
sidemap.maybeDepsLists.set(instr.lvalue.identifier.id, {
loc: value.loc,
deps: value.elements as Array<Place>,
});
}
break;
}
@@ -182,6 +186,7 @@ function makeManualMemoizationMarkers(
fnExpr: Place,
env: Environment,
depsList: Array<ManualMemoDependency> | null,
depsLoc: SourceLocation | null,
memoDecl: Place,
manualMemoId: number,
): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
@@ -197,6 +202,7 @@ function makeManualMemoizationMarkers(
* as dependencies
*/
deps: depsList,
depsLoc,
loc: fnExpr.loc,
},
effects: null,
@@ -287,86 +293,85 @@ function extractManualMemoizationArgs(
sidemap: IdentifierSidemap,
errors: CompilerError,
): {
fnPlace: Place | null;
fnPlace: Place;
depsList: Array<ManualMemoDependency> | null;
} {
depsLoc: SourceLocation | null;
} | null {
const [fnPlace, depsListPlace] = instr.value.args as Array<
Place | SpreadPattern | undefined
>;
if (fnPlace == null) {
if (fnPlace == null || fnPlace.kind !== 'Identifier') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected a callback function to be passed to ${kind}`,
description: `Expected a callback function to be passed to ${kind}`,
description:
kind === 'useCallback'
? 'The first argument to useCallback() must be a function to cache'
: 'The first argument to useMemo() must be a function that calculates a result to cache',
suggestions: null,
}).withDetails({
kind: 'error',
loc: instr.value.loc,
message: `Expected a callback function to be passed to ${kind}`,
message:
kind === 'useCallback'
? `Expected a callback function`
: `Expected a memoization function`,
}),
);
return {fnPlace: null, depsList: null};
return null;
}
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
if (depsListPlace == null) {
return {
fnPlace,
depsList: null,
depsLoc: null,
};
}
const maybeDepsList =
depsListPlace.kind === 'Identifier'
? sidemap.maybeDepsLists.get(depsListPlace.identifier.id)
: null;
if (maybeDepsList == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Unexpected spread argument to ${kind}`,
description: `Unexpected spread argument to ${kind}`,
reason: `Expected the dependency list for ${kind} to be an array literal`,
description: `Expected the dependency list for ${kind} to be an array literal`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: instr.value.loc,
message: `Unexpected spread argument to ${kind}`,
loc:
depsListPlace?.kind === 'Identifier' ? depsListPlace.loc : instr.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
}),
);
return {fnPlace: null, depsList: null};
return null;
}
let depsList: Array<ManualMemoDependency> | null = null;
if (depsListPlace != null) {
const maybeDepsList = sidemap.maybeDepsLists.get(
depsListPlace.identifier.id,
);
if (maybeDepsList == null) {
const depsList: Array<ManualMemoDependency> = [];
for (const dep of maybeDepsList.deps) {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the dependency list for ${kind} to be an array literal`,
description: `Expected the dependency list for ${kind} to be an array literal`,
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: depsListPlace.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
loc: dep.loc,
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
}),
);
return {fnPlace, depsList: null};
}
depsList = [];
for (const dep of maybeDepsList) {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: dep.loc,
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
}),
);
} else {
depsList.push(maybeDep);
}
} else {
depsList.push(maybeDep);
}
}
return {
fnPlace,
depsList,
depsLoc: maybeDepsList.loc,
};
}
@@ -427,16 +432,17 @@ export function dropManualMemoization(
const manualMemo = sidemap.manualMemos.get(id);
if (manualMemo != null) {
const {fnPlace, depsList} = extractManualMemoizationArgs(
const memoDetails = extractManualMemoizationArgs(
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
manualMemo.kind,
sidemap,
errors,
);
if (fnPlace == null) {
if (memoDetails == null) {
continue;
}
const {fnPlace, depsList, depsLoc} = memoDetails;
instr.value = getManualMemoizationReplacement(
fnPlace,
@@ -487,6 +493,7 @@ export function dropManualMemoization(
fnPlace,
func.env,
depsList,
depsLoc,
memoDecl,
nextManualMemoId++,
);

View File

@@ -609,6 +609,19 @@ function evaluateInstruction(
constantPropagationImpl(value.loweredFunc.func, constants);
return null;
}
case 'StartMemoize': {
if (value.deps != null) {
for (const dep of value.deps) {
if (dep.root.kind === 'NamedLocal') {
const placeValue = read(constants, dep.root.value);
if (placeValue != null && placeValue.kind === 'Primitive') {
dep.root.constant = true;
}
}
}
}
return null;
}
default: {
// TODO: handle more cases
return null;

View File

@@ -0,0 +1,890 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import prettyFormat from 'pretty-format';
import {
CompilerDiagnostic,
CompilerError,
CompilerSuggestionOperation,
SourceLocation,
} from '..';
import {CompilerSuggestion, ErrorCategory} from '../CompilerError';
import {
areEqualPaths,
BlockId,
DependencyPath,
FinishMemoize,
HIRFunction,
Identifier,
IdentifierId,
InstructionKind,
isPrimitiveType,
isStableType,
isSubPath,
isSubPathIgnoringOptionals,
isUseRefType,
LoadGlobal,
ManualMemoDependency,
Place,
StartMemoize,
} from '../HIR';
import {
eachInstructionLValue,
eachInstructionValueLValue,
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
const DEBUG = false;
/**
* Validates that existing manual memoization is exhaustive and does not
* have extraneous dependencies. The goal of the validation is to ensure
* that auto-memoization will not substantially change the behavior of
* the program:
* - If the manual dependencies were non-exhaustive (missing important deps)
* then auto-memoization will include those dependencies, and cause the
* value to update *more* frequently.
* - If the manual dependencies had extraneous deps, then auto memoization
* will remove them and cause the value to update *less* frequently.
*
* The implementation compares the manual dependencies against the values
* actually used within the memoization function
* - For each value V referenced in the memo function, either:
* - If the value is non-reactive *and* a known stable type, then the
* value may optionally be specified as an exact dependency.
* - Otherwise, report an error unless there is a manual dependency that will
* invalidate whenever V invalidates. If `x.y.z` is referenced, there must
* be a manual dependency for `x.y.z`, `x.y`, or `x`. Note that we assume
* no interior mutability, ie we assume that any changes to inner paths must
* always cause the other path to change as well.
* - Any dependencies that do not correspond to a value referenced in the memo
* function are considered extraneous and throw an error
*
* ## TODO: Invalid, Complex Deps
*
* Handle cases where the user deps were not simple identifiers + property chains.
* We try to detect this in ValidateUseMemo but we miss some cases. The problem
* is that invalid forms can be value blocks or function calls that don't get
* removed by DCE, leaving a structure like:
*
* StartMemoize
* t0 = <value to memoize>
* ...non-DCE'd code for manual deps...
* FinishMemoize decl=t0
*
* When we go to compute the dependencies, we then think that the user's manual dep
* logic is part of what the memo computation logic.
*/
export function validateExhaustiveDependencies(
fn: HIRFunction,
): Result<void, CompilerError> {
const reactive = collectReactiveIdentifiersHIR(fn);
const temporaries: Map<IdentifierId, Temporary> = new Map();
for (const param of fn.params) {
const place = param.kind === 'Identifier' ? param : param.place;
temporaries.set(place.identifier.id, {
kind: 'Local',
identifier: place.identifier,
path: [],
context: false,
loc: place.loc,
});
}
const error = new CompilerError();
let startMemo: StartMemoize | null = null;
function onStartMemoize(
value: StartMemoize,
dependencies: Set<InferredDependency>,
locals: Set<IdentifierId>,
): void {
CompilerError.simpleInvariant(startMemo == null, {
reason: 'Unexpected nested memo calls',
loc: value.loc,
});
startMemo = value;
dependencies.clear();
locals.clear();
}
function onFinishMemoize(
value: FinishMemoize,
dependencies: Set<InferredDependency>,
locals: Set<IdentifierId>,
): void {
CompilerError.simpleInvariant(
startMemo != null && startMemo.manualMemoId === value.manualMemoId,
{
reason: 'Found FinishMemoize without corresponding StartMemoize',
loc: value.loc,
},
);
visitCandidateDependency(value.decl, temporaries, dependencies, locals);
const inferred: Array<InferredDependency> = Array.from(dependencies);
// Sort dependencies by name and path, with shorter/non-optional paths first
inferred.sort((a, b) => {
if (a.kind === 'Global' && b.kind == 'Global') {
return a.binding.name.localeCompare(b.binding.name);
} else if (a.kind == 'Local' && b.kind == 'Local') {
CompilerError.simpleInvariant(
a.identifier.name != null &&
a.identifier.name.kind === 'named' &&
b.identifier.name != null &&
b.identifier.name.kind === 'named',
{
reason: 'Expected dependencies to be named variables',
loc: a.loc,
},
);
if (a.identifier.id !== b.identifier.id) {
return a.identifier.name.value.localeCompare(b.identifier.name.value);
}
if (a.path.length !== b.path.length) {
// if a's path is shorter this returns a negative, sorting a first
return a.path.length - b.path.length;
}
for (let i = 0; i < a.path.length; i++) {
const aProperty = a.path[i];
const bProperty = b.path[i];
const aOptional = aProperty.optional ? 0 : 1;
const bOptional = bProperty.optional ? 0 : 1;
if (aOptional !== bOptional) {
// sort non-optionals first
return aOptional - bOptional;
} else if (aProperty.property !== bProperty.property) {
return String(aProperty.property).localeCompare(
String(bProperty.property),
);
}
}
return 0;
} else {
const aName =
a.kind === 'Global' ? a.binding.name : a.identifier.name?.value;
const bName =
b.kind === 'Global' ? b.binding.name : b.identifier.name?.value;
if (aName != null && bName != null) {
return aName.localeCompare(bName);
}
return 0;
}
});
// remove redundant inferred dependencies
retainWhere(inferred, (dep, ix) => {
const match = inferred.findIndex(prevDep => {
return (
isEqualTemporary(prevDep, dep) ||
(prevDep.kind === 'Local' &&
dep.kind === 'Local' &&
prevDep.identifier.id === dep.identifier.id &&
isSubPath(prevDep.path, dep.path))
);
});
// only retain entries that don't have a prior match
return match === -1 || match >= ix;
});
// Validate that all manual dependencies belong there
if (DEBUG) {
console.log('manual');
console.log(
(startMemo.deps ?? [])
.map(x => ' ' + printManualMemoDependency(x))
.join('\n'),
);
console.log('inferred');
console.log(
inferred.map(x => ' ' + printInferredDependency(x)).join('\n'),
);
}
const manualDependencies = startMemo.deps ?? [];
const matched: Set<ManualMemoDependency> = new Set();
const missing: Array<Extract<InferredDependency, {kind: 'Local'}>> = [];
const extra: Array<ManualMemoDependency> = [];
for (const inferredDependency of inferred) {
if (inferredDependency.kind === 'Global') {
for (const manualDependency of manualDependencies) {
if (
manualDependency.root.kind === 'Global' &&
manualDependency.root.identifierName ===
inferredDependency.binding.name
) {
matched.add(manualDependency);
extra.push(manualDependency);
}
}
continue;
}
CompilerError.simpleInvariant(inferredDependency.kind === 'Local', {
reason: 'Unexpected function dependency',
loc: value.loc,
});
let hasMatchingManualDependency = false;
for (const manualDependency of manualDependencies) {
if (
manualDependency.root.kind === 'NamedLocal' &&
manualDependency.root.value.identifier.id ===
inferredDependency.identifier.id &&
(areEqualPaths(manualDependency.path, inferredDependency.path) ||
isSubPathIgnoringOptionals(
manualDependency.path,
inferredDependency.path,
))
) {
hasMatchingManualDependency = true;
matched.add(manualDependency);
}
}
if (
hasMatchingManualDependency ||
isOptionalDependency(inferredDependency, reactive)
) {
continue;
}
missing.push(inferredDependency);
}
for (const dep of startMemo.deps ?? []) {
if (matched.has(dep)) {
continue;
}
if (dep.root.kind === 'NamedLocal' && dep.root.constant) {
CompilerError.simpleInvariant(
!dep.root.value.reactive &&
isPrimitiveType(dep.root.value.identifier),
{
reason: 'Expected constant-folded dependency to be non-reactive',
loc: dep.root.value.loc,
},
);
/*
* Constant primitives can get constant-folded, which means we won't
* see a LoadLocal for the value within the memo function.
*/
continue;
}
extra.push(dep);
}
if (missing.length !== 0 || extra.length !== 0) {
let suggestion: CompilerSuggestion | null = null;
if (startMemo.depsLoc != null && typeof startMemo.depsLoc !== 'symbol') {
suggestion = {
description: 'Update dependencies',
range: [startMemo.depsLoc.start.index, startMemo.depsLoc.end.index],
op: CompilerSuggestionOperation.Replace,
text: `[${inferred
.filter(
dep =>
dep.kind === 'Local' && !isOptionalDependency(dep, reactive),
)
.map(printInferredDependency)
.join(', ')}]`,
};
}
const diagnostic = CompilerDiagnostic.create({
category: ErrorCategory.MemoDependencies,
reason: 'Found missing/extra memoization dependencies',
description: [
missing.length !== 0
? 'Missing dependencies can cause a value to update less often than it should, ' +
'resulting in stale UI'
: null,
extra.length !== 0
? 'Extra dependencies can cause a value to update more often than it should, ' +
'resulting in performance problems such as excessive renders or effects firing too often'
: null,
]
.filter(Boolean)
.join('. '),
suggestions: suggestion != null ? [suggestion] : null,
});
for (const dep of missing) {
let reactiveStableValueHint = '';
if (isStableType(dep.identifier)) {
reactiveStableValueHint =
'. Refs, setState functions, and other "stable" values generally do not need to be added ' +
'as dependencies, but this variable may change over time to point to different values';
}
diagnostic.withDetails({
kind: 'error',
message: `Missing dependency \`${printInferredDependency(dep)}\`${reactiveStableValueHint}`,
loc: dep.loc,
});
}
for (const dep of extra) {
if (dep.root.kind === 'Global') {
diagnostic.withDetails({
kind: 'error',
message:
`Unnecessary dependency \`${printManualMemoDependency(dep)}\`. ` +
'Values declared outside of a component/hook should not be listed as ' +
'dependencies as the component will not re-render if they change',
loc: dep.loc ?? startMemo.depsLoc ?? value.loc,
});
error.pushDiagnostic(diagnostic);
} else {
const root = dep.root.value;
const matchingInferred = inferred.find(
(
inferredDep,
): inferredDep is Extract<InferredDependency, {kind: 'Local'}> => {
return (
inferredDep.kind === 'Local' &&
inferredDep.identifier.id === root.identifier.id &&
isSubPathIgnoringOptionals(inferredDep.path, dep.path)
);
},
);
if (
matchingInferred != null &&
!isOptionalDependency(matchingInferred, reactive)
) {
diagnostic.withDetails({
kind: 'error',
message:
`Overly precise dependency \`${printManualMemoDependency(dep)}\`, ` +
`use \`${printInferredDependency(matchingInferred)}\` instead`,
loc: dep.loc ?? startMemo.depsLoc ?? value.loc,
});
} else {
/**
* Else this dependency doesn't correspond to anything referenced in the memo function,
* or is an optional dependency so we don't want to suggest adding it
*/
diagnostic.withDetails({
kind: 'error',
message: `Unnecessary dependency \`${printManualMemoDependency(dep)}\``,
loc: dep.loc ?? startMemo.depsLoc ?? value.loc,
});
}
}
}
if (suggestion != null) {
diagnostic.withDetails({
kind: 'hint',
message: `Inferred dependencies: \`${suggestion.text}\``,
});
}
error.pushDiagnostic(diagnostic);
}
dependencies.clear();
locals.clear();
startMemo = null;
}
collectDependencies(
fn,
temporaries,
{
onStartMemoize,
onFinishMemoize,
},
false, // isFunctionExpression
);
return error.asResult();
}
function addDependency(
dep: Temporary,
dependencies: Set<InferredDependency>,
locals: Set<IdentifierId>,
): void {
if (dep.kind === 'Function') {
for (const x of dep.dependencies) {
addDependency(x, dependencies, locals);
}
} else if (dep.kind === 'Global') {
dependencies.add(dep);
} else if (!locals.has(dep.identifier.id)) {
dependencies.add(dep);
}
}
function visitCandidateDependency(
place: Place,
temporaries: Map<IdentifierId, Temporary>,
dependencies: Set<InferredDependency>,
locals: Set<IdentifierId>,
): void {
const dep = temporaries.get(place.identifier.id);
if (dep != null) {
addDependency(dep, dependencies, locals);
}
}
/**
* This function determines the dependencies of the given function relative to
* its external context. Dependencies are collected eagerly, the first time an
* external variable is referenced, as opposed to trying to delay or aggregate
* calculation of dependencies until they are later "used".
*
* For example, in
*
* ```
* function f() {
* let x = y; // we record a dependency on `y` here
* ...
* use(x); // as opposed to trying to delay that dependency until here
* }
* ```
*
* That said, LoadLocal/LoadContext does not immediately take a dependency,
* we store the dependency in a temporary and set it as used when that temporary
* is referenced as an operand.
*
* As we proceed through the function we track local variables that it creates
* and don't consider later references to these variables as dependencies.
*
* For function expressions we first collect the function's dependencies by
* calling this function recursively, _without_ taking into account whether
* the "external" variables it accesses are actually external or just locals
* in the parent. We then prune any locals and immediately consider any
* remaining externals that it accesses as a dependency:
*
* ```
* function Component() {
* const local = ...;
* const f = () => { return [external, local] };
* }
* ```
*
* Here we calculate `f` as having dependencies `external, `local` and save
* this into `temporaries`. We then also immediately take these as dependencies
* at the Component scope, at which point we filter out `local` as a local variable,
* leaving just a dependency on `external`.
*
* When calling this function on a top-level component or hook, the collected dependencies
* will only contain the globals that it accesses which isn't useful. Instead, passing
* onStartMemoize/onFinishMemoize callbacks allows looking at the dependencies within
* blocks of manual memoization.
*/
function collectDependencies(
fn: HIRFunction,
temporaries: Map<IdentifierId, Temporary>,
callbacks: {
onStartMemoize: (
startMemo: StartMemoize,
dependencies: Set<InferredDependency>,
locals: Set<IdentifierId>,
) => void;
onFinishMemoize: (
finishMemo: FinishMemoize,
dependencies: Set<InferredDependency>,
locals: Set<IdentifierId>,
) => void;
} | null,
isFunctionExpression: boolean,
): Extract<Temporary, {kind: 'Function'}> {
const optionals = findOptionalPlaces(fn);
if (DEBUG) {
console.log(prettyFormat(optionals));
}
const locals: Set<IdentifierId> = new Set();
if (isFunctionExpression) {
for (const param of fn.params) {
const place = param.kind === 'Identifier' ? param : param.place;
locals.add(place.identifier.id);
}
}
const dependencies: Set<InferredDependency> = new Set();
function visit(place: Place): void {
visitCandidateDependency(place, temporaries, dependencies, locals);
}
for (const block of fn.body.blocks.values()) {
for (const phi of block.phis) {
let deps: Array<Temporary> | null = null;
for (const operand of phi.operands.values()) {
const dep = temporaries.get(operand.identifier.id);
if (dep == null) {
continue;
}
if (deps == null) {
deps = [dep];
} else {
deps.push(dep);
}
}
if (deps == null) {
continue;
} else if (deps.length === 1) {
temporaries.set(phi.place.identifier.id, deps[0]!);
} else {
temporaries.set(phi.place.identifier.id, {
kind: 'Function',
dependencies: new Set(deps),
});
}
}
for (const instr of block.instructions) {
const {lvalue, value} = instr;
switch (value.kind) {
case 'LoadGlobal': {
temporaries.set(lvalue.identifier.id, {
kind: 'Global',
binding: value.binding,
});
break;
}
case 'LoadContext':
case 'LoadLocal': {
if (locals.has(value.place.identifier.id)) {
break;
}
const temp = temporaries.get(value.place.identifier.id);
if (temp != null) {
if (temp.kind === 'Local') {
const local: Temporary = {...temp, loc: value.place.loc};
temporaries.set(lvalue.identifier.id, local);
} else {
temporaries.set(lvalue.identifier.id, temp);
}
}
break;
}
case 'DeclareLocal': {
const local: Temporary = {
kind: 'Local',
identifier: value.lvalue.place.identifier,
path: [],
context: false,
loc: value.lvalue.place.loc,
};
temporaries.set(value.lvalue.place.identifier.id, local);
locals.add(value.lvalue.place.identifier.id);
break;
}
case 'StoreLocal': {
if (value.lvalue.place.identifier.name == null) {
const temp = temporaries.get(value.value.identifier.id);
if (temp != null) {
temporaries.set(value.lvalue.place.identifier.id, temp);
}
break;
}
visit(value.value);
if (value.lvalue.kind !== InstructionKind.Reassign) {
const local: Temporary = {
kind: 'Local',
identifier: value.lvalue.place.identifier,
path: [],
context: false,
loc: value.lvalue.place.loc,
};
temporaries.set(value.lvalue.place.identifier.id, local);
locals.add(value.lvalue.place.identifier.id);
}
break;
}
case 'DeclareContext': {
const local: Temporary = {
kind: 'Local',
identifier: value.lvalue.place.identifier,
path: [],
context: true,
loc: value.lvalue.place.loc,
};
temporaries.set(value.lvalue.place.identifier.id, local);
break;
}
case 'StoreContext': {
visit(value.value);
if (value.lvalue.kind !== InstructionKind.Reassign) {
const local: Temporary = {
kind: 'Local',
identifier: value.lvalue.place.identifier,
path: [],
context: true,
loc: value.lvalue.place.loc,
};
temporaries.set(value.lvalue.place.identifier.id, local);
locals.add(value.lvalue.place.identifier.id);
}
break;
}
case 'Destructure': {
visit(value.value);
if (value.lvalue.kind !== InstructionKind.Reassign) {
for (const lvalue of eachInstructionValueLValue(value)) {
const local: Temporary = {
kind: 'Local',
identifier: lvalue.identifier,
path: [],
context: false,
loc: lvalue.loc,
};
temporaries.set(lvalue.identifier.id, local);
locals.add(lvalue.identifier.id);
}
}
break;
}
case 'PropertyLoad': {
if (
typeof value.property === 'number' ||
(isUseRefType(value.object.identifier) &&
value.property === 'current')
) {
visit(value.object);
break;
}
const object = temporaries.get(value.object.identifier.id);
if (object != null && object.kind === 'Local') {
const optional = optionals.get(value.object.identifier.id) ?? false;
const local: Temporary = {
kind: 'Local',
identifier: object.identifier,
context: object.context,
path: [
...object.path,
{
optional,
property: value.property,
},
],
loc: value.loc,
};
temporaries.set(lvalue.identifier.id, local);
}
break;
}
case 'FunctionExpression':
case 'ObjectMethod': {
const functionDeps = collectDependencies(
value.loweredFunc.func,
temporaries,
null,
true, // isFunctionExpression
);
temporaries.set(lvalue.identifier.id, functionDeps);
addDependency(functionDeps, dependencies, locals);
break;
}
case 'StartMemoize': {
const onStartMemoize = callbacks?.onStartMemoize;
if (onStartMemoize != null) {
onStartMemoize(value, dependencies, locals);
}
break;
}
case 'FinishMemoize': {
const onFinishMemoize = callbacks?.onFinishMemoize;
if (onFinishMemoize != null) {
onFinishMemoize(value, dependencies, locals);
}
break;
}
case 'MethodCall': {
// Ignore the method itself
for (const operand of eachInstructionValueOperand(value)) {
if (operand.identifier.id === value.property.identifier.id) {
continue;
}
visit(operand);
}
break;
}
default: {
for (const operand of eachInstructionValueOperand(value)) {
visit(operand);
}
for (const lvalue of eachInstructionLValue(instr)) {
locals.add(lvalue.identifier.id);
}
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (optionals.has(operand.identifier.id)) {
continue;
}
visit(operand);
}
}
return {kind: 'Function', dependencies};
}
function printInferredDependency(dep: InferredDependency): string {
switch (dep.kind) {
case 'Global': {
return dep.binding.name;
}
case 'Local': {
CompilerError.simpleInvariant(
dep.identifier.name != null && dep.identifier.name.kind === 'named',
{
reason: 'Expected dependencies to be named variables',
loc: dep.loc,
},
);
return `${dep.identifier.name.value}${dep.path.map(p => (p.optional ? '?' : '') + '.' + p.property).join('')}`;
}
}
}
function printManualMemoDependency(dep: ManualMemoDependency): string {
let identifierName: string;
if (dep.root.kind === 'Global') {
identifierName = dep.root.identifierName;
} else {
const name = dep.root.value.identifier.name;
CompilerError.simpleInvariant(name != null && name.kind === 'named', {
reason: 'Expected manual dependencies to be named variables',
loc: dep.root.value.loc,
});
identifierName = name.value;
}
return `${identifierName}${dep.path.map(p => (p.optional ? '?' : '') + '.' + p.property).join('')}`;
}
function isEqualTemporary(a: Temporary, b: Temporary): boolean {
switch (a.kind) {
case 'Function': {
return false;
}
case 'Global': {
return b.kind === 'Global' && a.binding.name === b.binding.name;
}
case 'Local': {
return (
b.kind === 'Local' &&
a.identifier.id === b.identifier.id &&
areEqualPaths(a.path, b.path)
);
}
}
}
type Temporary =
| {kind: 'Global'; binding: LoadGlobal['binding']}
| {
kind: 'Local';
identifier: Identifier;
path: DependencyPath;
context: boolean;
loc: SourceLocation;
}
| {kind: 'Function'; dependencies: Set<Temporary>};
type InferredDependency = Extract<Temporary, {kind: 'Local' | 'Global'}>;
function collectReactiveIdentifiersHIR(fn: HIRFunction): Set<IdentifierId> {
const reactive = new Set<IdentifierId>();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
for (const lvalue of eachInstructionLValue(instr)) {
if (lvalue.reactive) {
reactive.add(lvalue.identifier.id);
}
}
for (const operand of eachInstructionValueOperand(instr.value)) {
if (operand.reactive) {
reactive.add(operand.identifier.id);
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (operand.reactive) {
reactive.add(operand.identifier.id);
}
}
}
return reactive;
}
export function findOptionalPlaces(
fn: HIRFunction,
): Map<IdentifierId, boolean> {
const optionals = new Map<IdentifierId, boolean>();
const visited: Set<BlockId> = new Set();
for (const [, block] of fn.body.blocks) {
if (visited.has(block.id)) {
continue;
}
if (block.terminal.kind === 'optional') {
visited.add(block.id);
const optionalTerminal = block.terminal;
let testBlock = fn.body.blocks.get(block.terminal.test)!;
const queue: Array<boolean | null> = [block.terminal.optional];
loop: while (true) {
visited.add(testBlock.id);
const terminal = testBlock.terminal;
switch (terminal.kind) {
case 'branch': {
const isOptional = queue.pop();
CompilerError.simpleInvariant(isOptional !== undefined, {
reason:
'Expected an optional value for each optional test condition',
loc: terminal.test.loc,
});
if (isOptional != null) {
optionals.set(terminal.test.identifier.id, isOptional);
}
if (terminal.fallthrough === optionalTerminal.fallthrough) {
// found it
const consequent = fn.body.blocks.get(terminal.consequent)!;
const last = consequent.instructions.at(-1);
if (last !== undefined && last.value.kind === 'StoreLocal') {
if (isOptional != null) {
optionals.set(last.value.value.identifier.id, isOptional);
}
}
break loop;
} else {
testBlock = fn.body.blocks.get(terminal.fallthrough)!;
}
break;
}
case 'optional': {
queue.push(terminal.optional);
testBlock = fn.body.blocks.get(terminal.test)!;
break;
}
case 'logical':
case 'ternary': {
queue.push(null);
testBlock = fn.body.blocks.get(terminal.test)!;
break;
}
case 'sequence': {
// Do we need sequence?? In any case, don't push to queue bc there is no corresponding branch terminal
testBlock = fn.body.blocks.get(terminal.block)!;
break;
}
default: {
CompilerError.simpleInvariant(false, {
reason: `Unexpected terminal in optional`,
loc: terminal.loc,
});
}
}
}
CompilerError.simpleInvariant(queue.length === 0, {
reason:
'Expected a matching number of conditional blocks and branch points',
loc: block.terminal.loc,
});
}
}
return optionals;
}
function isOptionalDependency(
inferredDependency: Extract<InferredDependency, {kind: 'Local'}>,
reactive: Set<IdentifierId>,
): boolean {
return (
!reactive.has(inferredDependency.identifier.id) &&
(isStableType(inferredDependency.identifier) ||
isPrimitiveType(inferredDependency.identifier))
);
}

View File

@@ -242,6 +242,7 @@ function validateInferredDep(
normalizedDep = {
root: maybeNormalizedRoot.root,
path: [...maybeNormalizedRoot.path, ...dep.path],
loc: maybeNormalizedRoot.loc,
};
} else {
CompilerError.invariant(dep.identifier.name?.kind === 'named', {
@@ -267,8 +268,10 @@ function validateInferredDep(
effect: Effect.Read,
reactive: false,
},
constant: false,
},
path: [...dep.path],
loc: GeneratedSource,
};
}
for (const decl of declsWithinMemoBlock) {
@@ -379,8 +382,10 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
root: {
kind: 'NamedLocal',
value: storeTarget,
constant: false,
},
path: [],
loc: storeTarget.loc,
});
}
}
@@ -408,8 +413,10 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
root: {
kind: 'NamedLocal',
value: {...lvalue},
constant: false,
},
path: [],
loc: lvalue.loc,
});
}
}

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
const someGlobal = {value: 0};
@@ -33,7 +33,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import { useMemo } from "react";
const someGlobal = { value: 0 };

View File

@@ -1,4 +1,4 @@
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
const someGlobal = {value: 0};

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
@@ -36,7 +36,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import { useMemo } from "react";
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";

View File

@@ -1,4 +1,4 @@
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';

View File

@@ -2,6 +2,7 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
function Component(props) {
@@ -30,7 +31,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies:false
import { useMemo } from "react";
function Component(props) {

View File

@@ -1,3 +1,4 @@
// @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
function Component(props) {

View File

@@ -11,7 +11,7 @@ function Component(props) {
Component = useMemo(() => {
return Component;
});
}, [Component]);
return <Component {...props} />;
}
@@ -36,6 +36,7 @@ function Component(props) {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
Component = Stringify;
Component;
Component = Component;
$[0] = Component;
} else {

View File

@@ -7,7 +7,7 @@ function Component(props) {
Component = useMemo(() => {
return Component;
});
}, [Component]);
return <Component {...props} />;
}

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @eslintSuppressionRules:["my-app","react-rule"]
// @eslintSuppressionRules:["my-app","react-rule"] @validateExhaustiveMemoizationDependencies:false
/* eslint-disable my-app/react-rule */
function lowercasecomponent() {
@@ -26,7 +26,7 @@ Error: React Compiler has skipped optimizing this component because one or more
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable my-app/react-rule`.
error.bailout-on-suppression-of-custom-rule.ts:3:0
1 | // @eslintSuppressionRules:["my-app","react-rule"]
1 | // @eslintSuppressionRules:["my-app","react-rule"] @validateExhaustiveMemoizationDependencies:false
2 |
> 3 | /* eslint-disable my-app/react-rule */
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression

View File

@@ -1,4 +1,4 @@
// @eslintSuppressionRules:["my-app","react-rule"]
// @eslintSuppressionRules:["my-app","react-rule"] @validateExhaustiveMemoizationDependencies:false
/* eslint-disable my-app/react-rule */
function lowercasecomponent() {

View File

@@ -2,6 +2,7 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies:false
/* eslint-disable react-hooks/rules-of-hooks */
function lowercasecomponent() {
'use forget';
@@ -23,25 +24,26 @@ Error: React Compiler has skipped optimizing this component because one or more
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable react-hooks/rules-of-hooks`.
error.invalid-sketchy-code-use-forget.ts:1:0
> 1 | /* eslint-disable react-hooks/rules-of-hooks */
error.invalid-sketchy-code-use-forget.ts:2:0
1 | // @validateExhaustiveMemoizationDependencies:false
> 2 | /* eslint-disable react-hooks/rules-of-hooks */
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
2 | function lowercasecomponent() {
3 | 'use forget';
4 | const x = [];
3 | function lowercasecomponent() {
4 | 'use forget';
5 | const x = [];
Error: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable-next-line react-hooks/rules-of-hooks`.
error.invalid-sketchy-code-use-forget.ts:5:2
3 | 'use forget';
4 | const x = [];
> 5 | // eslint-disable-next-line react-hooks/rules-of-hooks
error.invalid-sketchy-code-use-forget.ts:6:2
4 | 'use forget';
5 | const x = [];
> 6 | // eslint-disable-next-line react-hooks/rules-of-hooks
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
6 | return <div>{x}</div>;
7 | }
8 | /* eslint-enable react-hooks/rules-of-hooks */
7 | return <div>{x}</div>;
8 | }
9 | /* eslint-enable react-hooks/rules-of-hooks */
```

View File

@@ -1,3 +1,4 @@
// @validateExhaustiveMemoizationDependencies:false
/* eslint-disable react-hooks/rules-of-hooks */
function lowercasecomponent() {
'use forget';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// Note: Everything below this is sketchy
// Note: Everything below this is sketchy @validateExhaustiveMemoizationDependencies:false
/* eslint-disable react-hooks/rules-of-hooks */
function lowercasecomponent() {
'use forget';
@@ -43,7 +43,7 @@ Error: React Compiler has skipped optimizing this component because one or more
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable react-hooks/rules-of-hooks`.
error.invalid-unclosed-eslint-suppression.ts:2:0
1 | // Note: Everything below this is sketchy
1 | // Note: Everything below this is sketchy @validateExhaustiveMemoizationDependencies:false
> 2 | /* eslint-disable react-hooks/rules-of-hooks */
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
3 | function lowercasecomponent() {

View File

@@ -1,4 +1,4 @@
// Note: Everything below this is sketchy
// Note: Everything below this is sketchy @validateExhaustiveMemoizationDependencies:false
/* eslint-disable react-hooks/rules-of-hooks */
function lowercasecomponent() {
'use forget';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useCallback, useRef} from 'react';
function useCustomRef() {

View File

@@ -1,4 +1,4 @@
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useCallback, useRef} from 'react';
function useCustomRef() {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useCallback, useRef} from 'react';
function useCustomRef() {

View File

@@ -1,4 +1,4 @@
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useCallback, useRef} from 'react';
function useCustomRef() {

View File

@@ -1,39 +0,0 @@
## Input
```javascript
function Component() {
const item = [];
const foo = useCallback(
() => {
item.push(1);
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return <Button foo={foo} />;
}
```
## Error
```
Found 1 error:
Error: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable-next-line react-hooks/exhaustive-deps`.
error.sketchy-code-exhaustive-deps.ts:6:7
4 | () => {
5 | item.push(1);
> 6 | }, // eslint-disable-next-line react-hooks/exhaustive-deps
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
7 | []
8 | );
9 |
```

View File

@@ -2,6 +2,7 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies:false
/* eslint-disable react-hooks/rules-of-hooks */
function lowercasecomponent() {
const x = [];
@@ -27,12 +28,13 @@ Error: React Compiler has skipped optimizing this component because one or more
React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable react-hooks/rules-of-hooks`.
error.sketchy-code-rules-of-hooks.ts:1:0
> 1 | /* eslint-disable react-hooks/rules-of-hooks */
error.sketchy-code-rules-of-hooks.ts:2:0
1 | // @validateExhaustiveMemoizationDependencies:false
> 2 | /* eslint-disable react-hooks/rules-of-hooks */
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression
2 | function lowercasecomponent() {
3 | const x = [];
4 | return <div>{x}</div>;
3 | function lowercasecomponent() {
4 | const x = [];
5 | return <div>{x}</div>;
```

View File

@@ -1,3 +1,4 @@
// @validateExhaustiveMemoizationDependencies:false
/* eslint-disable react-hooks/rules-of-hooks */
function lowercasecomponent() {
const x = [];

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @flow @validatePreserveExistingMemoizationGuarantees @enablePreserveExistingMemoizationGuarantees:false
// @flow @validatePreserveExistingMemoizationGuarantees @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useFragment} from 'react-relay';
import LogEvent from 'LogEvent';
import {useCallback, useMemo} from 'react';

View File

@@ -1,4 +1,4 @@
// @flow @validatePreserveExistingMemoizationGuarantees @enablePreserveExistingMemoizationGuarantees:false
// @flow @validatePreserveExistingMemoizationGuarantees @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useFragment} from 'react-relay';
import LogEvent from 'LogEvent';
import {useCallback, useMemo} from 'react';

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

@@ -0,0 +1,40 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies
function Component() {
const ref = useRef(null);
const onChange = useCallback(() => {
return ref.current.value;
}, [ref.current.value]);
return <input ref={ref} onChange={onChange} />;
}
```
## Error
```
Found 1 error:
Error: Found missing/extra memoization dependencies
Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
error.invalid-dep-on-ref-current-value.ts:7:6
5 | const onChange = useCallback(() => {
6 | return ref.current.value;
> 7 | }, [ref.current.value]);
| ^^^^^^^^^^^^^^^^^ Unnecessary dependency `ref.current.value`
8 |
9 | return <input ref={ref} onChange={onChange} />;
10 | }
Inferred dependencies: `[]`
```

View File

@@ -0,0 +1,10 @@
// @validateExhaustiveMemoizationDependencies
function Component() {
const ref = useRef(null);
const onChange = useCallback(() => {
return ref.current.value;
}, [ref.current.value]);
return <input ref={ref} onChange={onChange} />;
}

View File

@@ -0,0 +1,44 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies
import {useState} from 'react';
import {Stringify} from 'shared-runtime';
function Component() {
const [state, setState] = useState(0);
const x = useMemo(() => {
return [state];
// error: `setState` is a stable type, but not actually referenced
}, [state, setState]);
return 'oops';
}
```
## Error
```
Found 1 error:
Error: Found missing/extra memoization dependencies
Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
error.invalid-exhaustive-deps-disallow-unused-stable-types.ts:11:13
9 | return [state];
10 | // error: `setState` is a stable type, but not actually referenced
> 11 | }, [state, setState]);
| ^^^^^^^^ Unnecessary dependency `setState`
12 |
13 | return 'oops';
14 | }
Inferred dependencies: `[state]`
```

View File

@@ -0,0 +1,14 @@
// @validateExhaustiveMemoizationDependencies
import {useState} from 'react';
import {Stringify} from 'shared-runtime';
function Component() {
const [state, setState] = useState(0);
const x = useMemo(() => {
return [state];
// error: `setState` is a stable type, but not actually referenced
}, [state, setState]);
return 'oops';
}

View File

@@ -0,0 +1,204 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies
import {useMemo} from 'react';
import {Stringify} from 'shared-runtime';
function Component({x, y, z}) {
const a = useMemo(() => {
return x?.y.z?.a;
// error: too precise
}, [x?.y.z?.a.b]);
const b = useMemo(() => {
return x.y.z?.a;
// ok, not our job to type check nullability
}, [x.y.z.a]);
const c = useMemo(() => {
return x?.y.z.a?.b;
// error: too precise
}, [x?.y.z.a?.b.z]);
const d = useMemo(() => {
return x?.y?.[(console.log(y), z?.b)];
// ok
}, [x?.y, y, z?.b]);
const e = useMemo(() => {
const e = [];
e.push(x);
return e;
// ok
}, [x]);
const f = useMemo(() => {
return [];
// error: unnecessary
}, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
const ref1 = useRef(null);
const ref2 = useRef(null);
const ref = z ? ref1 : ref2;
const cb = useMemo(() => {
return () => {
return ref.current;
};
// error: ref is a stable type but reactive
}, []);
return <Stringify results={[a, b, c, d, e, f, cb]} />;
}
```
## Error
```
Found 5 errors:
Error: Found missing/extra memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI. Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
error.invalid-exhaustive-deps.ts:7:11
5 | function Component({x, y, z}) {
6 | const a = useMemo(() => {
> 7 | return x?.y.z?.a;
| ^^^^^^^^^ Missing dependency `x?.y.z?.a`
8 | // error: too precise
9 | }, [x?.y.z?.a.b]);
10 | const b = useMemo(() => {
error.invalid-exhaustive-deps.ts:9:6
7 | return x?.y.z?.a;
8 | // error: too precise
> 9 | }, [x?.y.z?.a.b]);
| ^^^^^^^^^^^ Overly precise dependency `x?.y.z?.a.b`, use `x?.y.z?.a` instead
10 | const b = useMemo(() => {
11 | return x.y.z?.a;
12 | // ok, not our job to type check nullability
Inferred dependencies: `[x?.y.z?.a]`
Error: Found missing/extra memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI. Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
error.invalid-exhaustive-deps.ts:15:11
13 | }, [x.y.z.a]);
14 | const c = useMemo(() => {
> 15 | return x?.y.z.a?.b;
| ^^^^^^^^^^^ Missing dependency `x?.y.z.a?.b`
16 | // error: too precise
17 | }, [x?.y.z.a?.b.z]);
18 | const d = useMemo(() => {
error.invalid-exhaustive-deps.ts:17:6
15 | return x?.y.z.a?.b;
16 | // error: too precise
> 17 | }, [x?.y.z.a?.b.z]);
| ^^^^^^^^^^^^^ Overly precise dependency `x?.y.z.a?.b.z`, use `x?.y.z.a?.b` instead
18 | const d = useMemo(() => {
19 | return x?.y?.[(console.log(y), z?.b)];
20 | // ok
Inferred dependencies: `[x?.y.z.a?.b]`
Error: Found missing/extra memoization dependencies
Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
error.invalid-exhaustive-deps.ts:31:6
29 | return [];
30 | // error: unnecessary
> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
| ^ Unnecessary dependency `x`
32 | const ref1 = useRef(null);
33 | const ref2 = useRef(null);
34 | const ref = z ? ref1 : ref2;
error.invalid-exhaustive-deps.ts:31:9
29 | return [];
30 | // error: unnecessary
> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
| ^^^ Unnecessary dependency `y.z`
32 | const ref1 = useRef(null);
33 | const ref2 = useRef(null);
34 | const ref = z ? ref1 : ref2;
error.invalid-exhaustive-deps.ts:31:14
29 | return [];
30 | // error: unnecessary
> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
| ^^^^^^^ Unnecessary dependency `z?.y?.a`
32 | const ref1 = useRef(null);
33 | const ref2 = useRef(null);
34 | const ref = z ? ref1 : ref2;
error.invalid-exhaustive-deps.ts:31:23
29 | return [];
30 | // error: unnecessary
> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
| ^^^^^^^^^^^^^ Unnecessary dependency `UNUSED_GLOBAL`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change
32 | const ref1 = useRef(null);
33 | const ref2 = useRef(null);
34 | const ref = z ? ref1 : ref2;
Inferred dependencies: `[]`
Error: Found missing/extra memoization dependencies
Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
error.invalid-exhaustive-deps.ts:31:6
29 | return [];
30 | // error: unnecessary
> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
| ^ Unnecessary dependency `x`
32 | const ref1 = useRef(null);
33 | const ref2 = useRef(null);
34 | const ref = z ? ref1 : ref2;
error.invalid-exhaustive-deps.ts:31:9
29 | return [];
30 | // error: unnecessary
> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
| ^^^ Unnecessary dependency `y.z`
32 | const ref1 = useRef(null);
33 | const ref2 = useRef(null);
34 | const ref = z ? ref1 : ref2;
error.invalid-exhaustive-deps.ts:31:14
29 | return [];
30 | // error: unnecessary
> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
| ^^^^^^^ Unnecessary dependency `z?.y?.a`
32 | const ref1 = useRef(null);
33 | const ref2 = useRef(null);
34 | const ref = z ? ref1 : ref2;
error.invalid-exhaustive-deps.ts:31:23
29 | return [];
30 | // error: unnecessary
> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
| ^^^^^^^^^^^^^ Unnecessary dependency `UNUSED_GLOBAL`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change
32 | const ref1 = useRef(null);
33 | const ref2 = useRef(null);
34 | const ref = z ? ref1 : ref2;
Inferred dependencies: `[]`
Error: Found missing/extra memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-exhaustive-deps.ts:37:13
35 | const cb = useMemo(() => {
36 | return () => {
> 37 | return ref.current;
| ^^^ Missing dependency `ref`. Refs, setState functions, and other "stable" values generally do not need to be added as dependencies, but this variable may change over time to point to different values
38 | };
39 | // error: ref is a stable type but reactive
40 | }, []);
Inferred dependencies: `[ref]`
```

View File

@@ -0,0 +1,42 @@
// @validateExhaustiveMemoizationDependencies
import {useMemo} from 'react';
import {Stringify} from 'shared-runtime';
function Component({x, y, z}) {
const a = useMemo(() => {
return x?.y.z?.a;
// error: too precise
}, [x?.y.z?.a.b]);
const b = useMemo(() => {
return x.y.z?.a;
// ok, not our job to type check nullability
}, [x.y.z.a]);
const c = useMemo(() => {
return x?.y.z.a?.b;
// error: too precise
}, [x?.y.z.a?.b.z]);
const d = useMemo(() => {
return x?.y?.[(console.log(y), z?.b)];
// ok
}, [x?.y, y, z?.b]);
const e = useMemo(() => {
const e = [];
e.push(x);
return e;
// ok
}, [x]);
const f = useMemo(() => {
return [];
// error: unnecessary
}, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
const ref1 = useRef(null);
const ref2 = useRef(null);
const ref = z ? ref1 : ref2;
const cb = useMemo(() => {
return () => {
return ref.current;
};
// error: ref is a stable type but reactive
}, []);
return <Stringify results={[a, b, c, d, e, f, cb]} />;
}

View File

@@ -0,0 +1,43 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies
import {useMemo} from 'react';
import {makeObject_Primitives} from 'shared-runtime';
function useHook() {
const object = makeObject_Primitives();
const fn = useCallback(() => {
const g = () => {
return [object];
};
return g;
});
return fn;
}
```
## Error
```
Found 1 error:
Error: Found missing/extra memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-missing-nonreactive-dep-inner-function.ts:10:14
8 | const fn = useCallback(() => {
9 | const g = () => {
> 10 | return [object];
| ^^^^^^ Missing dependency `object`
11 | };
12 | return g;
13 | });
```

View File

@@ -0,0 +1,15 @@
// @validateExhaustiveMemoizationDependencies
import {useMemo} from 'react';
import {makeObject_Primitives} from 'shared-runtime';
function useHook() {
const object = makeObject_Primitives();
const fn = useCallback(() => {
const g = () => {
return [object];
};
return g;
});
return fn;
}

View File

@@ -0,0 +1,43 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies
import {useMemo} from 'react';
import {makeObject_Primitives, useIdentity} from 'shared-runtime';
function useHook() {
// object is non-reactive but not memoized bc the mutation surrounds a hook
const object = makeObject_Primitives();
useIdentity();
object.x = 0;
const array = useMemo(() => [object], []);
return array;
}
```
## Error
```
Found 1 error:
Error: Found missing/extra memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-missing-nonreactive-dep-unmemoized.ts:11:31
9 | useIdentity();
10 | object.x = 0;
> 11 | const array = useMemo(() => [object], []);
| ^^^^^^ Missing dependency `object`
12 | return array;
13 | }
14 |
Inferred dependencies: `[object]`
```

View File

@@ -0,0 +1,13 @@
// @validateExhaustiveMemoizationDependencies
import {useMemo} from 'react';
import {makeObject_Primitives, useIdentity} from 'shared-runtime';
function useHook() {
// object is non-reactive but not memoized bc the mutation surrounds a hook
const object = makeObject_Primitives();
useIdentity();
object.x = 0;
const array = useMemo(() => [object], []);
return array;
}

View File

@@ -0,0 +1,40 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies
import {useMemo} from 'react';
import {makeObject_Primitives} from 'shared-runtime';
function useHook() {
const object = makeObject_Primitives();
const array = useMemo(() => [object], []);
return array;
}
```
## Error
```
Found 1 error:
Error: Found missing/extra memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-missing-nonreactive-dep.ts:8:31
6 | function useHook() {
7 | const object = makeObject_Primitives();
> 8 | const array = useMemo(() => [object], []);
| ^^^^^^ Missing dependency `object`
9 | return array;
10 | }
11 |
Inferred dependencies: `[object]`
```

View File

@@ -0,0 +1,10 @@
// @validateExhaustiveMemoizationDependencies
import {useMemo} from 'react';
import {makeObject_Primitives} from 'shared-runtime';
function useHook() {
const object = makeObject_Primitives();
const array = useMemo(() => [object], []);
return array;
}

View File

@@ -0,0 +1,44 @@
## Input
```javascript
import {arrayPush} from 'shared-runtime';
// @validateExhaustiveMemoizationDependencies
function Component() {
const item = [];
const foo = useCallback(
() => {
arrayPush(item, 1);
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return <Button foo={foo} />;
}
```
## Error
```
Found 1 error:
Error: Found missing/extra memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.sketchy-code-exhaustive-deps.ts:8:16
6 | const foo = useCallback(
7 | () => {
> 8 | arrayPush(item, 1);
| ^^^^ Missing dependency `item`
9 | }, // eslint-disable-next-line react-hooks/exhaustive-deps
10 | []
11 | );
Inferred dependencies: `[item]`
```

View File

@@ -1,8 +1,11 @@
import {arrayPush} from 'shared-runtime';
// @validateExhaustiveMemoizationDependencies
function Component() {
const item = [];
const foo = useCallback(
() => {
item.push(1);
arrayPush(item, 1);
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[]
);

View File

@@ -0,0 +1,41 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies
function Component() {
const x = 0;
const y = useMemo(() => {
return [x];
// x gets constant-folded but shouldn't count as extraneous,
// it was referenced in the memo block
}, [x]);
return y;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
function Component() {
const $ = _c(1);
const x = 0;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [0];
$[0] = t0;
} else {
t0 = $[0];
}
const y = t0;
return y;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,11 @@
// @validateExhaustiveMemoizationDependencies
function Component() {
const x = 0;
const y = useMemo(() => {
return [x];
// x gets constant-folded but shouldn't count as extraneous,
// it was referenced in the memo block
}, [x]);
return y;
}

View File

@@ -0,0 +1,100 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies
import {
useCallback,
useTransition,
useState,
useOptimistic,
useActionState,
useRef,
useReducer,
} from 'react';
function useFoo() {
const [s, setState] = useState();
const ref = useRef(null);
const [t, startTransition] = useTransition();
const [u, addOptimistic] = useOptimistic();
const [v, dispatch] = useReducer(() => {}, null);
const [isPending, dispatchAction] = useActionState(() => {}, null);
return useCallback(() => {
dispatch();
startTransition(() => {});
addOptimistic();
setState(null);
dispatchAction();
ref.current = true;
}, [
// intentionally adding unnecessary deps on nonreactive stable values
// to check that they're allowed
dispatch,
startTransition,
addOptimistic,
setState,
dispatchAction,
ref,
]);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
import {
useCallback,
useTransition,
useState,
useOptimistic,
useActionState,
useRef,
useReducer,
} from "react";
function useFoo() {
const $ = _c(1);
const [, setState] = useState();
const ref = useRef(null);
const [, startTransition] = useTransition();
const [, addOptimistic] = useOptimistic();
const [, dispatch] = useReducer(_temp, null);
const [, dispatchAction] = useActionState(_temp2, null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => {
dispatch();
startTransition(_temp3);
addOptimistic();
setState(null);
dispatchAction();
ref.current = true;
};
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function _temp3() {}
function _temp2() {}
function _temp() {}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};
```
### Eval output
(kind: ok) "[[ function params=0 ]]"

View File

@@ -0,0 +1,42 @@
// @validateExhaustiveMemoizationDependencies
import {
useCallback,
useTransition,
useState,
useOptimistic,
useActionState,
useRef,
useReducer,
} from 'react';
function useFoo() {
const [s, setState] = useState();
const ref = useRef(null);
const [t, startTransition] = useTransition();
const [u, addOptimistic] = useOptimistic();
const [v, dispatch] = useReducer(() => {}, null);
const [isPending, dispatchAction] = useActionState(() => {}, null);
return useCallback(() => {
dispatch();
startTransition(() => {});
addOptimistic();
setState(null);
dispatchAction();
ref.current = true;
}, [
// intentionally adding unnecessary deps on nonreactive stable values
// to check that they're allowed
dispatch,
startTransition,
addOptimistic,
setState,
dispatchAction,
ref,
]);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};

View File

@@ -0,0 +1,201 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies
import {useCallback, useMemo} from 'react';
import {makeObject_Primitives, Stringify} from 'shared-runtime';
function useHook1(x) {
return useMemo(() => {
return x?.y.z?.a;
}, [x?.y.z?.a]);
}
function useHook2(x) {
useMemo(() => {
return x.y.z?.a;
}, [x.y.z?.a]);
}
function useHook3(x) {
return useMemo(() => {
return x?.y.z.a?.b;
}, [x?.y.z.a?.b]);
}
function useHook4(x, y, z) {
return useMemo(() => {
return x?.y?.[(console.log(y), z?.b)];
}, [x?.y, y, z?.b]);
}
function useHook5(x) {
return useMemo(() => {
const e = [];
const local = makeObject_Primitives(x);
const fn = () => {
e.push(local);
};
fn();
return e;
}, [x]);
}
function useHook6(x) {
return useMemo(() => {
const f = [];
f.push(x.y.z);
f.push(x.y);
f.push(x);
return f;
}, [x]);
}
function useHook7(x) {
const [state, setState] = useState(true);
const f = () => {
setState(x => !x);
};
return useCallback(() => {
f();
}, [f]);
}
function Component({x, y, z}) {
const a = useHook1(x);
const b = useHook2(x);
const c = useHook3(x);
const d = useHook4(x, y, z);
const e = useHook5(x);
const f = useHook6(x);
const g = useHook7(x);
return <Stringify results={[a, b, c, d, e, f, g]} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
import { useCallback, useMemo } from "react";
import { makeObject_Primitives, Stringify } from "shared-runtime";
function useHook1(x) {
x?.y.z?.a;
return x?.y.z?.a;
}
function useHook2(x) {
x.y.z?.a;
}
function useHook3(x) {
x?.y.z.a?.b;
return x?.y.z.a?.b;
}
function useHook4(x, y, z) {
x?.y;
z?.b;
return x?.y?.[(console.log(y), z?.b)];
}
function useHook5(x) {
const $ = _c(2);
let e;
if ($[0] !== x) {
e = [];
const local = makeObject_Primitives(x);
const fn = () => {
e.push(local);
};
fn();
$[0] = x;
$[1] = e;
} else {
e = $[1];
}
return e;
}
function useHook6(x) {
const $ = _c(2);
let f;
if ($[0] !== x) {
f = [];
f.push(x.y.z);
f.push(x.y);
f.push(x);
$[0] = x;
$[1] = f;
} else {
f = $[1];
}
return f;
}
function useHook7(x) {
const $ = _c(2);
const [, setState] = useState(true);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => {
setState(_temp);
};
$[0] = t0;
} else {
t0 = $[0];
}
const f = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
f();
};
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
function _temp(x_0) {
return !x_0;
}
function Component(t0) {
const $ = _c(8);
const { x, y, z } = t0;
const a = useHook1(x);
const b = useHook2(x);
const c = useHook3(x);
const d = useHook4(x, y, z);
const e = useHook5(x);
const f = useHook6(x);
const g = useHook7(x);
let t1;
if (
$[0] !== a ||
$[1] !== b ||
$[2] !== c ||
$[3] !== d ||
$[4] !== e ||
$[5] !== f ||
$[6] !== g
) {
t1 = <Stringify results={[a, b, c, d, e, f, g]} />;
$[0] = a;
$[1] = b;
$[2] = c;
$[3] = d;
$[4] = e;
$[5] = f;
$[6] = g;
$[7] = t1;
} else {
t1 = $[7];
}
return t1;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,65 @@
// @validateExhaustiveMemoizationDependencies
import {useCallback, useMemo} from 'react';
import {makeObject_Primitives, Stringify} from 'shared-runtime';
function useHook1(x) {
return useMemo(() => {
return x?.y.z?.a;
}, [x?.y.z?.a]);
}
function useHook2(x) {
useMemo(() => {
return x.y.z?.a;
}, [x.y.z?.a]);
}
function useHook3(x) {
return useMemo(() => {
return x?.y.z.a?.b;
}, [x?.y.z.a?.b]);
}
function useHook4(x, y, z) {
return useMemo(() => {
return x?.y?.[(console.log(y), z?.b)];
}, [x?.y, y, z?.b]);
}
function useHook5(x) {
return useMemo(() => {
const e = [];
const local = makeObject_Primitives(x);
const fn = () => {
e.push(local);
};
fn();
return e;
}, [x]);
}
function useHook6(x) {
return useMemo(() => {
const f = [];
f.push(x.y.z);
f.push(x.y);
f.push(x);
return f;
}, [x]);
}
function useHook7(x) {
const [state, setState] = useState(true);
const f = () => {
setState(x => !x);
};
return useCallback(() => {
f();
}, [f]);
}
function Component({x, y, z}) {
const a = useHook1(x);
const b = useHook2(x);
const c = useHook3(x);
const d = useHook4(x, y, z);
const e = useHook5(x);
const f = useHook6(x);
const g = useHook7(x);
return <Stringify results={[a, b, c, d, e, f, g]} />;
}

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo, useState} from 'react';
import {ValidateMemoization} from 'shared-runtime';
@@ -29,7 +29,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c2 } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
import { c as _c2 } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import { useMemo, useState } from "react";
import { ValidateMemoization } from "shared-runtime";

View File

@@ -1,4 +1,4 @@
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo, useState} from 'react';
import {ValidateMemoization} from 'shared-runtime';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @compilationMode:"infer" @enableResetCacheOnSourceFileChanges
// @compilationMode:"infer" @enableResetCacheOnSourceFileChanges @validateExhaustiveMemoizationDependencies:false
import {useEffect, useMemo, useState} from 'react';
import {ValidateMemoization} from 'shared-runtime';
@@ -46,7 +46,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer" @enableResetCacheOnSourceFileChanges
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer" @enableResetCacheOnSourceFileChanges @validateExhaustiveMemoizationDependencies:false
import { useEffect, useMemo, useState } from "react";
import { ValidateMemoization } from "shared-runtime";
@@ -63,12 +63,12 @@ function unsafeUpdateConst() {
function Component() {
const $ = _c(3);
if (
$[0] !== "a585d27423c1181e7b6305ff909458183d284658c3c3d2e3764e1128be302fd7"
$[0] !== "36c02976ff5bc474b7510128ea8220ffe31d92cd5d245148ed0a43146d18ded4"
) {
for (let $i = 0; $i < 3; $i += 1) {
$[$i] = Symbol.for("react.memo_cache_sentinel");
}
$[0] = "a585d27423c1181e7b6305ff909458183d284658c3c3d2e3764e1128be302fd7";
$[0] = "36c02976ff5bc474b7510128ea8220ffe31d92cd5d245148ed0a43146d18ded4";
}
useState(_temp);

View File

@@ -1,4 +1,4 @@
// @compilationMode:"infer" @enableResetCacheOnSourceFileChanges
// @compilationMode:"infer" @enableResetCacheOnSourceFileChanges @validateExhaustiveMemoizationDependencies:false
import {useEffect, useMemo, useState} from 'react';
import {ValidateMemoization} from 'shared-runtime';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly
// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {identity} from 'shared-runtime';
@@ -30,7 +30,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly
// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly @validateExhaustiveMemoizationDependencies:false
import { useMemo } from "react";
import { identity } from "shared-runtime";
@@ -58,7 +58,7 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":206},"end":{"line":16,"column":1,"index":433},"filename":"dynamic-gating-bailout-nopanic.ts"},"detail":{"options":{"category":"PreserveManualMemo","reason":"Existing memoization could not be preserved","description":"React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `value`, but the source dependencies were []. Inferred dependency not present in source","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":9,"column":31,"index":288},"end":{"line":9,"column":52,"index":309},"filename":"dynamic-gating-bailout-nopanic.ts"},"message":"Could not preserve existing manual memoization"}]}}}
{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":255},"end":{"line":16,"column":1,"index":482},"filename":"dynamic-gating-bailout-nopanic.ts"},"detail":{"options":{"category":"PreserveManualMemo","reason":"Existing memoization could not be preserved","description":"React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `value`, but the source dependencies were []. Inferred dependency not present in source","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":9,"column":31,"index":337},"end":{"line":9,"column":52,"index":358},"filename":"dynamic-gating-bailout-nopanic.ts"},"message":"Could not preserve existing manual memoization"}]}}}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly
// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {identity} from 'shared-runtime';

View File

@@ -2,6 +2,7 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies:false
import {useState, useMemo} from 'react';
import {useIdentity} from 'shared-runtime';
@@ -36,7 +37,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies:false
import { useState, useMemo } from "react";
import { useIdentity } from "shared-runtime";

View File

@@ -1,3 +1,4 @@
// @validateExhaustiveMemoizationDependencies:false
import {useState, useMemo} from 'react';
import {useIdentity} from 'shared-runtime';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @loggerTestOnly
// @loggerTestOnly @validateExhaustiveMemoizationDependencies:false
function component(a) {
let x = useMemo(() => {
mutate(a);
@@ -15,7 +15,7 @@ function component(a) {
## Code
```javascript
// @loggerTestOnly
// @loggerTestOnly @validateExhaustiveMemoizationDependencies:false
function component(a) {
mutate(a);
}
@@ -25,8 +25,8 @@ function component(a) {
## Logs
```
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":18,"index":61},"end":{"line":5,"column":3,"index":87},"filename":"invalid-useMemo-return-empty.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":19},"end":{"line":7,"column":1,"index":107},"filename":"invalid-useMemo-return-empty.ts"},"fnName":"component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":1,"prunedMemoValues":0}
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":18,"index":110},"end":{"line":5,"column":3,"index":136},"filename":"invalid-useMemo-return-empty.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":68},"end":{"line":7,"column":1,"index":156},"filename":"invalid-useMemo-return-empty.ts"},"fnName":"component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":1,"prunedMemoValues":0}
```
### Eval output

View File

@@ -1,4 +1,4 @@
// @loggerTestOnly
// @loggerTestOnly @validateExhaustiveMemoizationDependencies:false
function component(a) {
let x = useMemo(() => {
mutate(a);

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {identity, ValidateMemoization} from 'shared-runtime';
@@ -33,7 +33,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import { useMemo } from "react";
import { identity, ValidateMemoization } from "shared-runtime";

View File

@@ -1,4 +1,4 @@
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {identity, ValidateMemoization} from 'shared-runtime';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {identity, ValidateMemoization} from 'shared-runtime';
@@ -30,7 +30,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import { useMemo } from "react";
import { identity, ValidateMemoization } from "shared-runtime";

View File

@@ -1,4 +1,4 @@
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {identity, ValidateMemoization} from 'shared-runtime';

View File

@@ -2,6 +2,7 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies:false
export function useFormatRelativeTime(opts = {}) {
const {timeZone, minimal} = opts;
const format = useCallback(function formatWithUnit() {}, [minimal]);
@@ -21,7 +22,7 @@ export function useFormatRelativeTime(opts = {}) {
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies:false
export function useFormatRelativeTime(t0) {
const $ = _c(1);
const opts = t0 === undefined ? {} : t0;

View File

@@ -1,3 +1,4 @@
// @validateExhaustiveMemoizationDependencies:false
export function useFormatRelativeTime(opts = {}) {
const {timeZone, minimal} = opts;
const format = useCallback(function formatWithUnit() {}, [minimal]);

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {
mutate,
@@ -50,7 +50,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import { useMemo } from "react";
import {
mutate,

View File

@@ -1,4 +1,4 @@
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {
mutate,

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {
typedCapture,
@@ -43,7 +43,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import { useMemo } from "react";
import {
typedCapture,

View File

@@ -1,4 +1,4 @@
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {
typedCapture,

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {
typedCapture,
@@ -41,7 +41,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import { useMemo } from "react";
import {
typedCapture,

View File

@@ -1,4 +1,4 @@
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {
typedCapture,

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {
typedCapture,
@@ -37,7 +37,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import { useMemo } from "react";
import {
typedCapture,

View File

@@ -1,4 +1,4 @@
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {
typedCapture,

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {
typedCapture,
@@ -41,7 +41,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import { useMemo } from "react";
import {
typedCapture,

View File

@@ -1,4 +1,4 @@
// @enablePreserveExistingMemoizationGuarantees:false
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
import {
typedCapture,

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false
// @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useCallback} from 'react';
import {Stringify} from 'shared-runtime';
@@ -36,7 +36,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import { useCallback } from "react";
import { Stringify } from "shared-runtime";

View File

@@ -1,4 +1,4 @@
// @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false
// @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useCallback} from 'react';
import {Stringify} from 'shared-runtime';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false
// @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useCallback} from 'react';
import {Stringify} from 'shared-runtime';
@@ -31,7 +31,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import { useCallback } from "react";
import { Stringify } from "shared-runtime";

View File

@@ -1,4 +1,4 @@
// @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false
// @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useCallback} from 'react';
import {Stringify} from 'shared-runtime';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false
// @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
function useFoo(arr1, arr2) {
@@ -27,7 +27,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import { useMemo } from "react";
function useFoo(arr1, arr2) {

View File

@@ -1,4 +1,4 @@
// @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false
// @enableNewMutationAliasingModel @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
function useFoo(arr1, arr2) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useCallback, useRef} from 'react';
function useFoo({cond}) {

View File

@@ -1,4 +1,4 @@
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useCallback, useRef} from 'react';
function useFoo({cond}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
// This is technically a false positive, but source is already breaking
// `exhaustive-deps` lint rule (and can be considered invalid).

View File

@@ -1,4 +1,4 @@
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
// This is technically a false positive, but source is already breaking
// `exhaustive-deps` lint rule (and can be considered invalid).

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useCallback} from 'react';
function Component({propA}) {

View File

@@ -1,4 +1,4 @@
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useCallback} from 'react';
function Component({propA}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
// This is technically a false positive, but source is already breaking
// `exhaustive-deps` lint rule (and can be considered invalid).

View File

@@ -1,4 +1,4 @@
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
// This is technically a false positive, but source is already breaking
// `exhaustive-deps` lint rule (and can be considered invalid).

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
function Component({propA}) {

View File

@@ -1,4 +1,4 @@
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
function Component({propA}) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
function Component({propA}) {

View File

@@ -1,4 +1,4 @@
// @validatePreserveExistingMemoizationGuarantees
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
function Component({propA}) {

Some files were not shown because too many files have changed in this diff Show More