Compare commits

..

1 Commits

Author SHA1 Message Date
Joe Savona
ab6d14b823 [compiler][poc] Quick experiment with SSR-optimization pass
Just a quick poc:
* Inline useState when the initializer is known to not be a function. The heuristic could be improved but will handle a large number of cases already.
* Prune effects
* Prune useRef if the ref is unused, by pruning 'ref' props on primitive components. Then DCE does the rest of the work - with a small change to allow `useRef()` calls to be dropped since function calls aren't normally eligible for dropping.
* Prune event handlers, by pruning props whose names start w "on" from primitive components. Then DCE removes the functions themselves.

Per the fixture, this gets pretty far.
2025-11-20 14:47:51 -08:00
47 changed files with 273 additions and 2267 deletions

View File

@@ -304,30 +304,6 @@ 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'>,
@@ -600,8 +576,7 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
case ErrorCategory.Suppression:
case ErrorCategory.Syntax:
case ErrorCategory.UseMemo:
case ErrorCategory.VoidUseMemo:
case ErrorCategory.MemoDependencies: {
case ErrorCategory.VoidUseMemo: {
heading = 'Error';
break;
}
@@ -659,10 +634,6 @@ 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
*/
@@ -1060,16 +1031,6 @@ 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.',
preset: LintRulePreset.RecommendedLatest,
};
}
case ErrorCategory.IncompatibleLibrary: {
return {
category,

View File

@@ -102,25 +102,14 @@ export type PluginOptions = Partial<{
panicThreshold: PanicThresholdOptions;
/**
* @deprecated
*
/*
* When enabled, Forget will continue statically analyzing and linting code, but skip over codegen
* passes.
*
* NOTE: ignored if `outputMode` is specified
*
* Defaults to false
*/
noEmit: boolean;
/**
* If specified, overrides `noEmit` and controls the output mode of the compiler.
*
* Defaults to null
*/
outputMode: CompilerOutputMode | null;
/*
* Determines the strategy for determining which functions to compile. Note that regardless of
* which mode is enabled, a component can be opted out by adding the string literal
@@ -223,19 +212,6 @@ const CompilationModeSchema = z.enum([
export type CompilationMode = z.infer<typeof CompilationModeSchema>;
const CompilerOutputModeSchema = z.enum([
// Build optimized for SSR, with client features removed
'ssr',
// Build optimized for the client, with auto memoization
'client',
// Build optimized for the client without auto memo
'client-no-memo',
// Lint mode, the output is unused but validations should run
'lint',
]);
export type CompilerOutputMode = z.infer<typeof CompilerOutputModeSchema>;
/**
* Represents 'events' that may occur during compilation. Events are only
* recorded when a logger is set (through the config).
@@ -317,7 +293,6 @@ export const defaultOptions: ParsedPluginOptions = {
logger: null,
gating: null,
noEmit: false,
outputMode: null,
dynamicGating: null,
eslintSuppressionRules: null,
flowSuppressions: true,

View File

@@ -8,7 +8,7 @@
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import prettyFormat from 'pretty-format';
import {CompilerOutputMode, Logger, ProgramContext} from '.';
import {Logger, ProgramContext} from '.';
import {
HIRFunction,
ReactiveFunction,
@@ -24,6 +24,7 @@ import {
pruneUnusedLabelsHIR,
} from '../HIR';
import {
CompilerMode,
Environment,
EnvironmentConfig,
ReactFunctionType,
@@ -105,7 +106,6 @@ 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 =
@@ -120,7 +120,7 @@ function run(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerOutputMode,
mode: CompilerMode,
programContext: ProgramContext,
logger: Logger | null,
filename: string | null,
@@ -170,7 +170,7 @@ function runWithEnvironment(
validateUseMemo(hir).unwrap();
if (
env.enableDropManualMemoization &&
env.isInferredMemoEnabled &&
!env.config.enablePreserveExistingManualUseMemo &&
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging
@@ -206,7 +206,7 @@ function runWithEnvironment(
inferTypes(hir);
log({kind: 'hir', name: 'InferTypes', value: hir});
if (env.enableValidations) {
if (env.isInferredMemoEnabled) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir).unwrap();
}
@@ -232,13 +232,13 @@ function runWithEnvironment(
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.enableValidations) {
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
}
if (env.outputMode === 'ssr') {
if (env.config.enableOptimizeForSSR) {
optimizeForSSR(hir);
log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
}
@@ -259,14 +259,14 @@ function runWithEnvironment(
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.enableValidations) {
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingRangeErrors.isErr()) {
throw mutabilityAliasingRangeErrors.unwrapErr();
}
validateLocalsNotReassignedAfterRender(hir);
}
if (env.enableValidations) {
if (env.isInferredMemoEnabled) {
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
@@ -303,11 +303,6 @@ function runWithEnvironment(
inferReactivePlaces(hir);
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
if (env.config.validateExhaustiveMemoizationDependencies) {
// NOTE: this relies on reactivity inference running first
validateExhaustiveDependencies(hir).unwrap();
}
rewriteInstructionKindsBasedOnReassignment(hir);
log({
kind: 'hir',
@@ -315,18 +310,20 @@ function runWithEnvironment(
value: hir,
});
if (env.enableValidations && env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));
}
if (env.isInferredMemoEnabled) {
if (env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));
}
if (env.enableMemoization) {
/**
* Only create reactive scopes (which directly map to generated memo blocks)
* if inferred memoization is enabled. This makes all later passes which
* transform reactive-scope labeled instructions no-ops.
*/
inferReactiveScopeVariables(hir);
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
if (!env.config.enableOptimizeForSSR) {
inferReactiveScopeVariables(hir);
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
}
}
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
@@ -591,7 +588,7 @@ export function compileFn(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerOutputMode,
mode: CompilerMode,
programContext: ProgramContext,
logger: Logger | null,
filename: string | null,

View File

@@ -24,7 +24,6 @@ import {
validateRestrictedImports,
} from './Imports';
import {
CompilerOutputMode,
CompilerReactTarget,
ParsedPluginOptions,
PluginOptions,
@@ -400,15 +399,7 @@ export function compileProgram(
*/
const suppressions = findProgramSuppressions(
pass.comments,
/*
* 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.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS,
pass.opts.flowSuppressions,
);
@@ -430,17 +421,9 @@ export function compileProgram(
);
const compiledFns: Array<CompileResult> = [];
// outputMode takes precedence if specified
const outputMode: CompilerOutputMode =
pass.opts.outputMode ?? (pass.opts.noEmit ? 'lint' : 'client');
while (queue.length !== 0) {
const current = queue.shift()!;
const compiled = processFn(
current.fn,
current.fnType,
programContext,
outputMode,
);
const compiled = processFn(current.fn, current.fnType, programContext);
if (compiled != null) {
for (const outlined of compiled.outlined) {
@@ -598,7 +581,6 @@ function processFn(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
outputMode: CompilerOutputMode,
): null | CodegenFunction {
let directives: {
optIn: t.Directive | null;
@@ -634,27 +616,18 @@ function processFn(
}
let compiledFn: CodegenFunction;
const compileResult = tryCompileFunction(
fn,
fnType,
programContext,
outputMode,
);
const compileResult = tryCompileFunction(fn, fnType, programContext);
if (compileResult.kind === 'error') {
if (directives.optOut != null) {
logError(compileResult.error, programContext, fn.node.loc ?? null);
} else {
handleError(compileResult.error, programContext, fn.node.loc ?? null);
}
if (outputMode === 'client') {
const retryResult = retryCompileFunction(fn, fnType, programContext);
if (retryResult == null) {
return null;
}
compiledFn = retryResult;
} else {
const retryResult = retryCompileFunction(fn, fnType, programContext);
if (retryResult == null) {
return null;
}
compiledFn = retryResult;
} else {
compiledFn = compileResult.compiledFn;
}
@@ -690,7 +663,7 @@ function processFn(
if (programContext.hasModuleScopeOptOut) {
return null;
} else if (programContext.opts.outputMode === 'lint') {
} else if (programContext.opts.noEmit) {
/**
* inferEffectDependencies + noEmit is currently only used for linting. In
* this mode, add source locations for where the compiler *can* infer effect
@@ -720,7 +693,6 @@ function tryCompileFunction(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
outputMode: CompilerOutputMode,
):
| {kind: 'compile'; compiledFn: CodegenFunction}
| {kind: 'error'; error: unknown} {
@@ -747,7 +719,7 @@ function tryCompileFunction(
fn,
programContext.opts.environment,
fnType,
outputMode,
'all_features',
programContext,
programContext.opts.logger,
programContext.filename,
@@ -785,7 +757,7 @@ function retryCompileFunction(
fn,
environment,
fnType,
'client-no-memo',
'no_inferred_memo',
programContext,
programContext.opts.logger,
programContext.filename,

View File

@@ -78,7 +78,7 @@ export function filterSuppressionsThatAffectFunction(
export function findProgramSuppressions(
programComments: Array<t.Comment>,
ruleNames: Array<string> | null,
ruleNames: Array<string>,
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 != null && ruleNames.length !== 0) {
if (ruleNames.length !== 0) {
const rulePattern = `(${ruleNames.join('|')})`;
disableNextLinePattern = new RegExp(
`eslint-disable-next-line ${rulePattern}`,

View File

@@ -9,7 +9,7 @@ import * as t from '@babel/types';
import {ZodError, z} from 'zod/v4';
import {fromZodError} from 'zod-validation-error/v4';
import {CompilerError} from '../CompilerError';
import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint';
import {Logger, ProgramContext} from '../Entrypoint';
import {Err, Ok, Result} from '../Utils/Result';
import {
DEFAULT_GLOBALS,
@@ -51,7 +51,6 @@ import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
import {FlowTypeEnv} from '../Flood/Types';
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
import {assertExhaustive} from '../Utils/utils';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -218,11 +217,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validatePreserveExistingMemoizationGuarantees: z.boolean().default(true),
/**
* Validate that dependencies supplied to manual memoization calls are exhaustive.
*/
validateExhaustiveMemoizationDependencies: z.boolean().default(false),
/**
* 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
@@ -697,6 +691,8 @@ export const EnvironmentConfigSchema = z.object({
* by React to only execute in response to events, not during render.
*/
enableInferEventHandlers: z.boolean().default(false),
enableOptimizeForSSR: z.boolean().default(false),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -736,7 +732,7 @@ export class Environment {
code: string | null;
config: EnvironmentConfig;
fnType: ReactFunctionType;
outputMode: CompilerOutputMode;
compilerMode: CompilerMode;
programContext: ProgramContext;
hasFireRewrite: boolean;
hasInferredEffect: boolean;
@@ -751,7 +747,7 @@ export class Environment {
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
outputMode: CompilerOutputMode,
compilerMode: CompilerMode,
config: EnvironmentConfig,
contextIdentifiers: Set<t.Identifier>,
parentFunction: NodePath<t.Function>, // the outermost function being compiled
@@ -762,7 +758,7 @@ export class Environment {
) {
this.#scope = scope;
this.fnType = fnType;
this.outputMode = outputMode;
this.compilerMode = compilerMode;
this.config = config;
this.filename = filename;
this.code = code;
@@ -858,65 +854,8 @@ export class Environment {
return this.#flowTypeEnvironment;
}
get enableDropManualMemoization(): boolean {
switch (this.outputMode) {
case 'lint': {
// linting drops to be more compatible with compiler analysis
return true;
}
case 'client':
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
}
get enableMemoization(): boolean {
switch (this.outputMode) {
case 'client':
case 'lint': {
// linting also enables memoization so that we can check if manual memoization is preserved
return true;
}
case 'ssr':
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
}
get enableValidations(): boolean {
switch (this.outputMode) {
case 'client':
case 'lint':
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
get isInferredMemoEnabled(): boolean {
return this.compilerMode !== 'no_inferred_memo';
}
get nextIdentifierId(): IdentifierId {

View File

@@ -817,11 +817,6 @@ 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 = {
@@ -1685,28 +1680,6 @@ 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, {loc: SourceLocation; deps: Array<Place>}>;
maybeDepsLists: Map<IdentifierId, Array<Place>>;
maybeDeps: Map<IdentifierId, ManualMemoDependency>;
optionals: Set<IdentifierId>;
};
@@ -159,10 +159,10 @@ function collectTemporaries(
}
case 'ArrayExpression': {
if (value.elements.every(e => e.kind === 'Identifier')) {
sidemap.maybeDepsLists.set(instr.lvalue.identifier.id, {
loc: value.loc,
deps: value.elements as Array<Place>,
});
sidemap.maybeDepsLists.set(
instr.lvalue.identifier.id,
value.elements as Array<Place>,
);
}
break;
}
@@ -182,7 +182,6 @@ function makeManualMemoizationMarkers(
fnExpr: Place,
env: Environment,
depsList: Array<ManualMemoDependency> | null,
depsLoc: SourceLocation | null,
memoDecl: Place,
manualMemoId: number,
): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
@@ -198,7 +197,6 @@ function makeManualMemoizationMarkers(
* as dependencies
*/
deps: depsList,
depsLoc,
loc: fnExpr.loc,
},
effects: null,
@@ -289,85 +287,86 @@ function extractManualMemoizationArgs(
sidemap: IdentifierSidemap,
errors: CompilerError,
): {
fnPlace: Place;
fnPlace: Place | null;
depsList: Array<ManualMemoDependency> | null;
depsLoc: SourceLocation | null;
} | null {
} {
const [fnPlace, depsListPlace] = instr.value.args as Array<
Place | SpreadPattern | undefined
>;
if (fnPlace == null || fnPlace.kind !== 'Identifier') {
if (fnPlace == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `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',
description: `Expected a callback function to be passed to ${kind}`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: instr.value.loc,
message:
kind === 'useCallback'
? `Expected a callback function`
: `Expected a memoization function`,
message: `Expected a callback function to be passed to ${kind}`,
}),
);
return null;
return {fnPlace: null, depsList: null};
}
if (depsListPlace == null) {
return {
fnPlace,
depsList: null,
depsLoc: null,
};
}
const maybeDepsList =
depsListPlace.kind === 'Identifier'
? sidemap.maybeDepsLists.get(depsListPlace.identifier.id)
: null;
if (maybeDepsList == null) {
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
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: `Unexpected spread argument to ${kind}`,
description: `Unexpected spread argument to ${kind}`,
suggestions: null,
}).withDetails({
kind: 'error',
loc:
depsListPlace?.kind === 'Identifier' ? depsListPlace.loc : instr.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
loc: instr.value.loc,
message: `Unexpected spread argument to ${kind}`,
}),
);
return null;
return {fnPlace: null, depsList: null};
}
const depsList: Array<ManualMemoDependency> = [];
for (const dep of maybeDepsList.deps) {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
let depsList: Array<ManualMemoDependency> | null = null;
if (depsListPlace != null) {
const maybeDepsList = sidemap.maybeDepsLists.get(
depsListPlace.identifier.id,
);
if (maybeDepsList == 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\`)`,
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: dep.loc,
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
loc: depsListPlace.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
}),
);
} else {
depsList.push(maybeDep);
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);
}
}
}
return {
fnPlace,
depsList,
depsLoc: maybeDepsList.loc,
};
}
@@ -428,17 +427,16 @@ export function dropManualMemoization(
const manualMemo = sidemap.manualMemos.get(id);
if (manualMemo != null) {
const memoDetails = extractManualMemoizationArgs(
const {fnPlace, depsList} = extractManualMemoizationArgs(
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
manualMemo.kind,
sidemap,
errors,
);
if (memoDetails == null) {
if (fnPlace == null) {
continue;
}
const {fnPlace, depsList, depsLoc} = memoDetails;
instr.value = getManualMemoizationReplacement(
fnPlace,
@@ -489,7 +487,6 @@ export function dropManualMemoization(
fnPlace,
func.env,
depsList,
depsLoc,
memoDecl,
nextManualMemoId++,
);

View File

@@ -2452,7 +2452,7 @@ function computeEffectsForLegacySignature(
}),
});
}
if (signature.knownIncompatible != null && state.env.enableValidations) {
if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) {
const errors = new CompilerError();
errors.pushDiagnostic(
CompilerDiagnostic.create({

View File

@@ -319,7 +319,7 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
}
case 'CallExpression':
case 'MethodCall': {
if (state.env.outputMode === 'ssr') {
if (state.env.config.enableOptimizeForSSR) {
const calleee =
value.kind === 'CallExpression' ? value.callee : value.property;
const hookKind = getHookKind(state.env, calleee.identifier);

View File

@@ -159,7 +159,7 @@ export function codegenFunction(
const compiled = compileResult.unwrap();
const hookGuard = fn.env.config.enableEmitHookGuards;
if (hookGuard != null && fn.env.outputMode === 'client') {
if (hookGuard != null && fn.env.isInferredMemoEnabled) {
compiled.body = t.blockStatement([
createHookGuard(
hookGuard,
@@ -259,7 +259,7 @@ export function codegenFunction(
if (
emitInstrumentForget != null &&
fn.id != null &&
fn.env.outputMode === 'client'
fn.env.isInferredMemoEnabled
) {
/*
* Technically, this is a conditional hook call. However, we expect
@@ -591,10 +591,7 @@ function codegenBlockNoReset(
}
function wrapCacheDep(cx: Context, value: t.Expression): t.Expression {
if (
cx.env.config.enableEmitFreeze != null &&
cx.env.outputMode === 'client'
) {
if (cx.env.config.enableEmitFreeze != null && cx.env.isInferredMemoEnabled) {
const emitFreezeIdentifier = cx.env.programContext.addImportSpecifier(
cx.env.config.enableEmitFreeze,
).name;
@@ -1775,7 +1772,7 @@ function createCallExpression(
}
const hookGuard = env.config.enableEmitHookGuards;
if (hookGuard != null && isHook && env.outputMode === 'client') {
if (hookGuard != null && isHook && env.isInferredMemoEnabled) {
const iife = t.functionExpression(
null,
[],

View File

@@ -1,824 +0,0 @@
/**
* 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,
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.
*
* We consider a value V as missing if ALL of the following conditions are met:
* - V is reactive
* - There is no manual dependency path P such that whenever V would change,
* P would also change. If V is `x.y.z`, this means there must be some
* path P that is either `x.y.z`, `x.y`, or `x`. Note that we assume no
* interior mutability, such that a shorter path "covers" changes to longer
* more precise paths.
*
* We consider a value V extraneous if either of the folowing are true:
* - V is a reactive local that is unreferenced
* - V is a global that is unreferenced
*
* In other words, we allow extraneous non-reactive values since we know they cannot
* impact how often the memoization would run.
*
* ## 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,
});
const isRequiredDependency = reactive.has(
inferredDependency.identifier.id,
);
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 (!isRequiredDependency) {
extra.push(manualDependency);
}
}
}
if (isRequiredDependency && !hasMatchingManualDependency) {
missing.push(inferredDependency);
}
}
for (const dep of startMemo.deps ?? []) {
if (matched.has(dep)) {
continue;
}
extra.push(dep);
}
/**
* Per docblock, we only consider dependencies as extraneous if
* they are unused globals or reactive locals. Notably, this allows
* non-reactive locals.
*/
retainWhere(extra, dep => {
return dep.root.kind === 'Global' || dep.root.value.reactive;
});
if (missing.length !== 0 || extra.length !== 0) {
let suggestions: Array<CompilerSuggestion> | null = null;
if (startMemo.depsLoc != null && typeof startMemo.depsLoc !== 'symbol') {
suggestions = [
{
description: 'Update dependencies',
range: [startMemo.depsLoc.start.index, startMemo.depsLoc.end.index],
op: CompilerSuggestionOperation.Replace,
text: `[${inferred.map(printInferredDependency).join(', ')}]`,
},
];
}
if (missing.length !== 0) {
const diagnostic = CompilerDiagnostic.create({
category: ErrorCategory.MemoDependencies,
reason: 'Found non-exhaustive dependencies',
description:
'Missing dependencies can cause a value not to update when those inputs change, ' +
'resulting in stale UI',
suggestions,
});
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,
});
}
error.pushDiagnostic(diagnostic);
} else if (extra.length !== 0) {
const diagnostic = CompilerDiagnostic.create({
category: ErrorCategory.MemoDependencies,
reason: 'Found unnecessary memoization dependencies',
description:
'Unnecessary dependencies can cause a value to update more often than necessary, ' +
'which can cause effects to run more than expected',
});
diagnostic.withDetails({
kind: 'error',
message: `Unnecessary dependencies ${extra.map(dep => `\`${printManualMemoDependency(dep)}\``).join(', ')}`,
loc: startMemo.depsLoc ?? value.loc,
});
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;
}

View File

@@ -1,91 +0,0 @@
## 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

@@ -1,22 +0,0 @@
// @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

@@ -1,109 +0,0 @@
## 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 4 errors:
Error: Found non-exhaustive dependencies
Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
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: Found non-exhaustive dependencies
Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
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: Found unnecessary memoization dependencies
Unnecessary dependencies can cause a value to update more often than necessary, which can cause effects to run more than expected.
error.invalid-exhaustive-deps.ts:31:5
29 | return [];
30 | // error: unnecessary
> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unnecessary dependencies `x`, `y.z`, `z?.y?.a`, `UNUSED_GLOBAL`
32 | const ref1 = useRef(null);
33 | const ref2 = useRef(null);
34 | const ref = z ? ref1 : ref2;
Error: Found non-exhaustive dependencies
Missing dependencies can cause a value not to update when those inputs change, 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 | }, []);
```

View File

@@ -1,42 +0,0 @@
// @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

@@ -1,100 +0,0 @@
## 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

@@ -1,42 +0,0 @@
// @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

@@ -1,201 +0,0 @@
## 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

@@ -1,65 +0,0 @@
// @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
// @dynamicGating:{"source":"shared-runtime"} @outputMode:"lint"
// @dynamicGating:{"source":"shared-runtime"} @noEmit
function Foo() {
'use memo if(getTrue)';
@@ -19,7 +19,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @dynamicGating:{"source":"shared-runtime"} @outputMode:"lint"
// @dynamicGating:{"source":"shared-runtime"} @noEmit
function Foo() {
"use memo if(getTrue)";

View File

@@ -1,4 +1,4 @@
// @dynamicGating:{"source":"shared-runtime"} @outputMode:"lint"
// @dynamicGating:{"source":"shared-runtime"} @noEmit
function Foo() {
'use memo if(getTrue)';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @inferEffectDependencies @outputMode:"lint"
// @inferEffectDependencies @noEmit
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
@@ -17,7 +17,7 @@ function ReactiveVariable({propVal}) {
## Code
```javascript
// @inferEffectDependencies @outputMode:"lint"
// @inferEffectDependencies @noEmit
import { print } from "shared-runtime";
import useEffectWrapper from "useEffectWrapper";
import { AUTODEPS } from "react";

View File

@@ -1,4 +1,4 @@
// @inferEffectDependencies @outputMode:"lint"
// @inferEffectDependencies @noEmit
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';

View File

@@ -0,0 +1,66 @@
## Input
```javascript
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
function Foo({propVal}) {
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};
```
## Code
```javascript
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly
import { print } from "shared-runtime";
import useEffectWrapper from "useEffectWrapper";
import { AUTODEPS } from "react";
function Foo({ propVal }) {
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return { arr, arr2 };
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ propVal: 1 }],
sequentialRenders: [{ propVal: 1 }, { propVal: 2 }],
};
```
## Logs
```
{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":195},"end":{"line":14,"column":1,"index":409},"filename":"retry-no-emit.ts"},"detail":{"options":{"category":"Immutability","reason":"This value cannot be modified","description":"Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook","details":[{"kind":"error","loc":{"start":{"line":12,"column":2,"index":372},"end":{"line":12,"column":6,"index":376},"filename":"retry-no-emit.ts","identifierName":"arr2"},"message":"value cannot be modified"}]}}}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":248},"end":{"line":8,"column":46,"index":292},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":8,"column":31,"index":277},"end":{"line":8,"column":34,"index":280},"filename":"retry-no-emit.ts","identifierName":"arr"}]}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":11,"column":2,"index":316},"end":{"line":11,"column":54,"index":368},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":11,"column":25,"index":339},"end":{"line":11,"column":29,"index":343},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":11,"column":25,"index":339},"end":{"line":11,"column":29,"index":343},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":11,"column":35,"index":349},"end":{"line":11,"column":42,"index":356},"filename":"retry-no-emit.ts","identifierName":"propVal"}]}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":195},"end":{"line":14,"column":1,"index":409},"filename":"retry-no-emit.ts"},"fnName":"Foo","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: ok) {"arr":[1],"arr2":[2]}
{"arr":[2],"arr2":[2]}
logs: [[ 1 ],[ 2 ]]

View File

@@ -1,10 +1,9 @@
// @inferEffectDependencies @panicThreshold:"none"
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
function Foo({propVal}) {
'use memo';
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);

View File

@@ -2,10 +2,10 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold:"none"
// @compilationMode:"all" @inferEffectDependencies @panicThreshold:"none" @noEmit
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
import useEffectWrapper from 'useEffectWrapper';
function Foo({propVal}) {
'use memo';
@@ -15,6 +15,7 @@ function Foo({propVal}) {
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}
@@ -29,21 +30,20 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @inferEffectDependencies @panicThreshold:"none"
// @compilationMode:"all" @inferEffectDependencies @panicThreshold:"none" @noEmit
import { print } from "shared-runtime";
import useEffectWrapper from "useEffectWrapper";
import { AUTODEPS } from "react";
import useEffectWrapper from "useEffectWrapper";
function Foo(t0) {
function Foo({ propVal }) {
"use memo";
const { propVal } = t0;
const arr = [propVal];
useEffectWrapper(() => print(arr), [arr]);
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), [arr2, propVal]);
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return { arr, arr2 };
}

View File

@@ -1,7 +1,7 @@
// @inferEffectDependencies @outputMode:"lint" @panicThreshold:"none"
// @compilationMode:"all" @inferEffectDependencies @panicThreshold:"none" @noEmit
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
import useEffectWrapper from 'useEffectWrapper';
function Foo({propVal}) {
'use memo';
@@ -11,6 +11,7 @@ function Foo({propVal}) {
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}

View File

@@ -1,48 +0,0 @@
## Input
```javascript
// @inferEffectDependencies @outputMode:"lint" @panicThreshold:"none"
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
function Foo({propVal}) {
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};
```
## Error
```
Found 1 error:
Error: Cannot infer dependencies of this effect. This will break your build!
To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.
error.infer-effect-deps-with-rule-violation--lint.ts:8:2
6 | function Foo({propVal}) {
7 | const arr = [propVal];
> 8 | useEffectWrapper(() => print(arr), AUTODEPS);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot infer dependencies
9 |
10 | const arr2 = [];
11 | useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
```

View File

@@ -1,49 +0,0 @@
## Input
```javascript
// @inferEffectDependencies @outputMode:"lint" @panicThreshold:"none"
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
function Foo({propVal}) {
'use memo';
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};
```
## Error
```
Found 1 error:
Error: Cannot infer dependencies of this effect. This will break your build!
To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.
error.infer-effect-deps-with-rule-violation-use-memo-opt-in--lint.ts:9:2
7 | 'use memo';
8 | const arr = [propVal];
> 9 | useEffectWrapper(() => print(arr), AUTODEPS);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot infer dependencies
10 |
11 | const arr2 = [];
12 | useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
```

View File

@@ -1,58 +0,0 @@
## Input
```javascript
// @inferEffectDependencies @panicThreshold:"none"
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
function Foo({propVal}) {
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};
```
## Code
```javascript
// @inferEffectDependencies @panicThreshold:"none"
import { print } from "shared-runtime";
import useEffectWrapper from "useEffectWrapper";
import { AUTODEPS } from "react";
function Foo(t0) {
const { propVal } = t0;
const arr = [propVal];
useEffectWrapper(() => print(arr), [arr]);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), [arr2, propVal]);
arr2.push(2);
return { arr, arr2 };
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ propVal: 1 }],
sequentialRenders: [{ propVal: 1 }, { propVal: 2 }],
};
```
### Eval output
(kind: ok) {"arr":[1],"arr2":[2]}
{"arr":[2],"arr2":[2]}
logs: [[ 1 ],[ 2 ]]

View File

@@ -1,20 +0,0 @@
// @inferEffectDependencies @panicThreshold:"none"
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
function Foo({propVal}) {
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};

View File

@@ -0,0 +1,66 @@
## Input
```javascript
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';
function Foo({propVal}) {
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return {arr, arr2};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{propVal: 1}],
sequentialRenders: [{propVal: 1}, {propVal: 2}],
};
```
## Code
```javascript
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel
import { print } from "shared-runtime";
import useEffectWrapper from "useEffectWrapper";
import { AUTODEPS } from "react";
function Foo({ propVal }) {
const arr = [propVal];
useEffectWrapper(() => print(arr), AUTODEPS);
const arr2 = [];
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
arr2.push(2);
return { arr, arr2 };
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ propVal: 1 }],
sequentialRenders: [{ propVal: 1 }, { propVal: 2 }],
};
```
## Logs
```
{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":227},"end":{"line":14,"column":1,"index":441},"filename":"retry-no-emit.ts"},"detail":{"options":{"category":"Immutability","reason":"This value cannot be modified","description":"Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook","details":[{"kind":"error","loc":{"start":{"line":12,"column":2,"index":404},"end":{"line":12,"column":6,"index":408},"filename":"retry-no-emit.ts","identifierName":"arr2"},"message":"value cannot be modified"}]}}}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":280},"end":{"line":8,"column":46,"index":324},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":8,"column":31,"index":309},"end":{"line":8,"column":34,"index":312},"filename":"retry-no-emit.ts","identifierName":"arr"}]}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":11,"column":2,"index":348},"end":{"line":11,"column":54,"index":400},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":11,"column":25,"index":371},"end":{"line":11,"column":29,"index":375},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":11,"column":25,"index":371},"end":{"line":11,"column":29,"index":375},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":11,"column":35,"index":381},"end":{"line":11,"column":42,"index":388},"filename":"retry-no-emit.ts","identifierName":"propVal"}]}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":227},"end":{"line":14,"column":1,"index":441},"filename":"retry-no-emit.ts"},"fnName":"Foo","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: ok) {"arr":[1],"arr2":[2]}
{"arr":[2],"arr2":[2]}
logs: [[ 1 ],[ 2 ]]

View File

@@ -1,4 +1,4 @@
// @inferEffectDependencies @outputMode:"lint" @panicThreshold:"none"
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';

View File

@@ -20,40 +20,10 @@ function Component() {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR
// @enableOptimizeForSSR
function Component() {
const $ = _c(4);
const [state, setState] = useState(0);
const ref = useRef(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (e) => {
setState(e.target.value);
};
$[0] = t0;
} else {
t0 = $[0];
}
const onChange = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
log(ref.current.value);
};
$[1] = t1;
} else {
t1 = $[1];
}
useEffect(t1);
let t2;
if ($[2] !== state) {
t2 = <input value={state} onChange={onChange} ref={ref} />;
$[2] = state;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
const state = 0;
return <input value={state} />;
}
```

View File

@@ -22,40 +22,12 @@ function Component() {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR
// @enableOptimizeForSSR
function Component() {
const $ = _c(4);
const [state, setState] = useState(0);
const state = 0;
const ref = useRef(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (e) => {
setState(e.target.value);
};
$[0] = t0;
} else {
t0 = $[0];
}
const onChange = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
log(ref.current.value);
};
$[1] = t1;
} else {
t1 = $[1];
}
useEffect(t1);
let t2;
if ($[2] !== state) {
t2 = <CustomInput value={state} onChange={onChange} ref={ref} />;
$[2] = state;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
const onChange = undefined;
return <CustomInput value={state} onChange={onChange} ref={ref} />;
}
```

View File

@@ -25,43 +25,13 @@ function Component() {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR
// @enableOptimizeForSSR
function Component() {
const $ = _c(4);
const [, startTransition] = useTransition();
const [state, setState] = useState(0);
useTransition();
const state = 0;
const ref = useRef(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (e) => {
startTransition(() => {
setState.call(null, e.target.value);
});
};
$[0] = t0;
} else {
t0 = $[0];
}
const onChange = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
log(ref.current.value);
};
$[1] = t1;
} else {
t1 = $[1];
}
useEffect(t1);
let t2;
if ($[2] !== state) {
t2 = <CustomInput value={state} onChange={onChange} ref={ref} />;
$[2] = state;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
const onChange = undefined;
return <CustomInput value={state} onChange={onChange} ref={ref} />;
}
```

View File

@@ -25,7 +25,7 @@ function Component() {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR
// @enableOptimizeForSSR
import { useReducer } from "react";
@@ -34,41 +34,8 @@ const initializer = (x) => {
};
function Component() {
const $ = _c(4);
const [state, dispatch] = useReducer(_temp, 0, initializer);
const ref = useRef(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (e) => {
dispatch(e.target.value);
};
$[0] = t0;
} else {
t0 = $[0];
}
const onChange = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
log(ref.current.value);
};
$[1] = t1;
} else {
t1 = $[1];
}
useEffect(t1);
let t2;
if ($[2] !== state) {
t2 = <input value={state} onChange={onChange} ref={ref} />;
$[2] = state;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
function _temp(_, next) {
return next;
const state = initializer(0);
return <input value={state} />;
}
```

View File

@@ -23,46 +23,13 @@ function Component() {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR
// @enableOptimizeForSSR
import { useReducer } from "react";
function Component() {
const $ = _c(4);
const [state, dispatch] = useReducer(_temp, 0);
const ref = useRef(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (e) => {
dispatch(e.target.value);
};
$[0] = t0;
} else {
t0 = $[0];
}
const onChange = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
log(ref.current.value);
};
$[1] = t1;
} else {
t1 = $[1];
}
useEffect(t1);
let t2;
if ($[2] !== state) {
t2 = <input value={state} onChange={onChange} ref={ref} />;
$[2] = state;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
function _temp(_, next) {
return next;
const state = 0;
return <input value={state} />;
}
```

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @outputMode:"lint"
// @noEmit
function Foo() {
'use memo';
@@ -19,7 +19,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @outputMode:"lint"
// @noEmit
function Foo() {
"use memo";

View File

@@ -1,4 +1,4 @@
// @outputMode:"lint"
// @noEmit
function Foo() {
'use memo';

View File

@@ -21,7 +21,7 @@ import {isDeepStrictEqual} from 'util';
import type {ParseResult} from '@babel/parser';
const COMPILER_OPTIONS: PluginOptions = {
outputMode: 'lint',
noEmit: true,
panicThreshold: 'none',
// Don't emit errors on Flow suppressions--Flow already gave a signal
flowSuppressions: false,

View File

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

View File

@@ -22,7 +22,7 @@ import {isDeepStrictEqual} from 'util';
import type {ParseResult} from '@babel/parser';
const COMPILER_OPTIONS: PluginOptions = {
outputMode: 'lint',
noEmit: true,
panicThreshold: 'none',
// Don't emit errors on Flow suppressions--Flow already gave a signal
flowSuppressions: false,

View File

@@ -584,75 +584,4 @@ describe('ProfilerContext', () => {
await utils.actAsync(() => context.selectFiber(childID, 'Child'));
expect(inspectedElementID).toBe(parentID);
});
it('should toggle profiling when the keyboard shortcut is pressed', async () => {
// Context providers
const Profiler =
require('react-devtools-shared/src/devtools/views/Profiler/Profiler').default;
const {
TimelineContextController,
} = require('react-devtools-timeline/src/TimelineContext');
const {
SettingsContextController,
} = require('react-devtools-shared/src/devtools/views/Settings/SettingsContext');
const {
ModalDialogContextController,
} = require('react-devtools-shared/src/devtools/views/ModalDialog');
// Dom component for profiling to be enabled
const Component = () => null;
utils.act(() => render(<Component />));
const profilerContainer = document.createElement('div');
document.body.appendChild(profilerContainer);
// Create a root for the profiler
const profilerRoot = ReactDOMClient.createRoot(profilerContainer);
// Render the profiler
utils.act(() => {
profilerRoot.render(
<Contexts>
<SettingsContextController browserTheme="light">
<ModalDialogContextController>
<TimelineContextController>
<Profiler />
</TimelineContextController>
</ModalDialogContextController>
</SettingsContextController>
</Contexts>,
);
});
// Verify that the profiler is not profiling.
expect(store.profilerStore.isProfilingBasedOnUserInput).toBe(false);
// Trigger the keyboard shortcut.
const ownerWindow = profilerContainer.ownerDocument.defaultView;
const isMac =
typeof navigator !== 'undefined' &&
navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const keyEvent = new KeyboardEvent('keydown', {
key: 'e',
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
});
// Dispatch keyboard event to toggle profiling on
// Try utils.actAsync with recursivelyFlush=false
await utils.actAsync(() => {
ownerWindow.dispatchEvent(keyEvent);
}, false);
expect(store.profilerStore.isProfilingBasedOnUserInput).toBe(true);
// Dispatch keyboard event to toggle profiling off
await utils.actAsync(() => {
ownerWindow.dispatchEvent(keyEvent);
}, false);
expect(store.profilerStore.isProfilingBasedOnUserInput).toBe(false);
document.body.removeChild(profilerContainer);
});
});

View File

@@ -8,7 +8,7 @@
*/
import * as React from 'react';
import {Fragment, useContext, useEffect, useRef, useEffectEvent} from 'react';
import {Fragment, useContext} from 'react';
import {ModalDialog} from '../ModalDialog';
import {ProfilerContext} from './ProfilerContext';
import TabBar from '../TabBar';
@@ -38,11 +38,6 @@ import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
import styles from './Profiler.css';
function Profiler(_: {}) {
const profilerRef = useRef<HTMLDivElement | null>(null);
const isMac =
typeof navigator !== 'undefined' &&
navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const {
didRecordCommits,
isProcessingData,
@@ -52,8 +47,6 @@ function Profiler(_: {}) {
selectedTabID,
selectTab,
supportsProfiling,
startProfiling,
stopProfiling,
} = useContext(ProfilerContext);
const {file: timelineTraceEventData, searchInputContainerRef} =
@@ -63,32 +56,6 @@ function Profiler(_: {}) {
const isLegacyProfilerSelected = selectedTabID !== 'timeline';
// Cmd+E to start/stop profiler recording
const handleKeyDown = useEffectEvent((event: KeyboardEvent) => {
const correctModifier = isMac ? event.metaKey : event.ctrlKey;
if (correctModifier && event.key === 'e') {
if (isProfiling) {
stopProfiling();
} else {
startProfiling();
}
event.preventDefault();
event.stopPropagation();
}
});
useEffect(() => {
const div = profilerRef.current;
if (!div) {
return;
}
const ownerWindow = div.ownerDocument.defaultView;
ownerWindow.addEventListener('keydown', handleKeyDown);
return () => {
ownerWindow.removeEventListener('keydown', handleKeyDown);
};
}, []);
let view = null;
if (didRecordCommits || selectedTabID === 'timeline') {
switch (selectedTabID) {
@@ -145,7 +112,7 @@ function Profiler(_: {}) {
return (
<SettingsModalContextController>
<div ref={profilerRef} className={styles.Profiler}>
<div className={styles.Profiler}>
<div className={styles.LeftColumn}>
<div className={styles.Toolbar}>
<RecordToggle disabled={!supportsProfiling} />

View File

@@ -30,19 +30,13 @@ export default function RecordToggle({disabled}: Props): React.Node {
className = styles.ActiveRecordToggle;
}
const isMac =
typeof navigator !== 'undefined' &&
navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const shortcut = isMac ? '⌘E' : 'Ctrl+E';
const title = `${isProfiling ? 'Stop' : 'Start'} profiling - ${shortcut}`;
return (
<Button
className={className}
disabled={disabled}
onClick={isProfiling ? stopProfiling : startProfiling}
testName="ProfilerToggleButton"
title={title}>
title={isProfiling ? 'Stop profiling' : 'Start profiling'}>
<ButtonIcon type="record" />
</Button>
);