Compare commits

..

2 Commits

Author SHA1 Message Date
Jorge Cabiedes
99d7519db3 Merge branch 'main' into pr35100 2025-11-11 09:55:40 -08:00
Jorge Cabiedes Acosta
2ac39db287 [compiler] Update test snap to include fixture comment
Summary:
I missed this test case failing and now having @loggerTestOnly after landing some other PRs good to know they're not land blocking
2025-11-11 09:19:40 -08:00
131 changed files with 784 additions and 5866 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'>,

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,
@@ -104,9 +105,6 @@ import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRan
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
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 =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -120,7 +118,7 @@ function run(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerOutputMode,
mode: CompilerMode,
programContext: ProgramContext,
logger: Logger | null,
filename: string | null,
@@ -170,7 +168,7 @@ function runWithEnvironment(
validateUseMemo(hir).unwrap();
if (
env.enableDropManualMemoization &&
env.isInferredMemoEnabled &&
!env.config.enablePreserveExistingManualUseMemo &&
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging
@@ -206,7 +204,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,17 +230,12 @@ 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') {
optimizeForSSR(hir);
log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
}
// Note: Has to come after infer reference effects because "dead" code may still affect inference
deadCodeElimination(hir);
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
@@ -259,14 +252,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);
}
@@ -279,10 +272,12 @@ function runWithEnvironment(
validateNoSetStateInRender(hir).unwrap();
}
if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoDerivedComputationsInEffects_exp) {
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
} else if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoSetStateInEffects) {
@@ -303,11 +298,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,11 +305,11 @@ 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
@@ -569,10 +559,6 @@ function runWithEnvironment(
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
}
if (env.config.validateSourceLocations) {
validateSourceLocations(func, ast).unwrap();
}
/**
* This flag should be only set for unit / fixture tests to check
* that Forget correctly handles unexpected errors (e.g. exceptions
@@ -591,7 +577,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,
@@ -422,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) {
@@ -590,7 +581,6 @@ function processFn(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
outputMode: CompilerOutputMode,
): null | CodegenFunction {
let directives: {
optIn: t.Directive | null;
@@ -626,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;
}
@@ -682,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
@@ -712,7 +693,6 @@ function tryCompileFunction(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
outputMode: CompilerOutputMode,
):
| {kind: 'compile'; compiledFn: CodegenFunction}
| {kind: 'error'; error: unknown} {
@@ -739,7 +719,7 @@ function tryCompileFunction(
fn,
programContext.opts.environment,
fnType,
outputMode,
'all_features',
programContext,
programContext.opts.logger,
programContext.filename,
@@ -777,7 +757,7 @@ function retryCompileFunction(
fn,
environment,
fnType,
'client-no-memo',
'no_inferred_memo',
programContext,
programContext.opts.logger,
programContext.filename,

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
@@ -370,13 +364,6 @@ export const EnvironmentConfigSchema = z.object({
validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),
validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),
/**
* Validates that AST nodes generated during codegen have proper source locations.
* This is useful for debugging issues with source maps and Istanbul coverage.
* When enabled, the compiler will error if important source locations are missing in the generated AST.
*/
validateSourceLocations: z.boolean().default(false),
/**
* Validate against impure functions called during render
*/
@@ -678,25 +665,11 @@ export const EnvironmentConfigSchema = z.object({
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
/**
* When enabled, allows setState calls in effects based on valid patterns involving refs:
* - Allow setState where the value being set is derived from a ref. This is useful where
* state needs to take into account layer information, and a layout effect reads layout
* data from a ref and sets state.
* - Allow conditionally calling setState after manually comparing previous/new values
* for changes via a ref. Relying on effect deps is insufficient for non-primitive values,
* so a ref is generally required to manually track previous values and compare prev/next
* for meaningful changes before setting state.
* When enabled, allows setState calls in effects when the value being set is
* derived from a ref. This is useful for patterns where initial layout measurements
* from refs need to be stored in state during mount.
*/
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
/**
* Enables inference of event handler types for JSX props on built-in DOM elements.
* When enabled, functions passed to event handler props (props starting with "on")
* on primitive JSX tags are inferred to have the BuiltinEventHandlerId type, which
* allows ref access within those functions since DOM event handlers are guaranteed
* by React to only execute in response to events, not during render.
*/
enableInferEventHandlers: z.boolean().default(false),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -736,7 +709,7 @@ export class Environment {
code: string | null;
config: EnvironmentConfig;
fnType: ReactFunctionType;
outputMode: CompilerOutputMode;
compilerMode: CompilerMode;
programContext: ProgramContext;
hasFireRewrite: boolean;
hasInferredEffect: boolean;
@@ -751,7 +724,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 +735,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 +831,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

@@ -23,14 +23,13 @@ import {
BuiltInUseInsertionEffectHookId,
BuiltInUseLayoutEffectHookId,
BuiltInUseOperatorId,
BuiltInUseOptimisticId,
BuiltInUseReducerId,
BuiltInUseRefId,
BuiltInUseStateId,
BuiltInUseTransitionId,
BuiltInWeakMapId,
BuiltInWeakSetId,
BuiltInEffectEventId,
BuiltinEffectEventId,
ReanimatedSharedValueId,
ShapeRegistry,
addFunction,
@@ -819,18 +818,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
returnValueKind: ValueKind.Frozen,
}),
],
[
'useOptimistic',
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Object', shapeId: BuiltInUseOptimisticId},
calleeEffect: Effect.Read,
hookKind: 'useOptimistic',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.State,
}),
],
[
'use',
addFunction(
@@ -876,7 +863,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
returnType: {
kind: 'Function',
return: {kind: 'Poly'},
shapeId: BuiltInEffectEventId,
shapeId: BuiltinEffectEventId,
isConstructor: false,
},
calleeEffect: Effect.Read,

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,
@@ -1850,10 +1823,6 @@ export function isPrimitiveType(id: Identifier): boolean {
return id.type.kind === 'Primitive';
}
export function isPlainObjectType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInObject';
}
export function isArrayType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInArray';
}
@@ -1918,18 +1887,6 @@ export function isStartTransitionType(id: Identifier): boolean {
);
}
export function isUseOptimisticType(id: Identifier): boolean {
return (
id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseOptimistic'
);
}
export function isSetOptimisticType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetOptimistic'
);
}
export function isSetActionStateType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetActionState'
@@ -1963,8 +1920,7 @@ export function isStableType(id: Identifier): boolean {
isSetActionStateType(id) ||
isDispatcherType(id) ||
isUseRefType(id) ||
isStartTransitionType(id) ||
isSetOptimisticType(id)
isStartTransitionType(id)
);
}
@@ -1975,9 +1931,8 @@ export function isStableTypeContainer(id: Identifier): boolean {
}
return (
isUseStateType(id) || // setState
isUseActionStateType(id) || // setActionState
type_.shapeId === 'BuiltInUseActionState' || // setActionState
isUseReducerType(id) || // dispatcher
isUseOptimisticType(id) || // setOptimistic
type_.shapeId === 'BuiltInUseTransition' // startTransition
);
}
@@ -1997,7 +1952,6 @@ export function evaluatesToStableTypeOrContainer(
case 'useActionState':
case 'useRef':
case 'useTransition':
case 'useOptimistic':
return true;
}
}

View File

@@ -304,7 +304,6 @@ export type HookKind =
| 'useTransition'
| 'useImperativeHandle'
| 'useEffectEvent'
| 'useOptimistic'
| 'Custom';
/*
@@ -400,15 +399,12 @@ export const BuiltInUseReducerId = 'BuiltInUseReducer';
export const BuiltInDispatchId = 'BuiltInDispatch';
export const BuiltInUseContextHookId = 'BuiltInUseContextHook';
export const BuiltInUseTransitionId = 'BuiltInUseTransition';
export const BuiltInUseOptimisticId = 'BuiltInUseOptimistic';
export const BuiltInSetOptimisticId = 'BuiltInSetOptimistic';
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
export const BuiltInFireId = 'BuiltInFire';
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
export const BuiltInEffectEventId = 'BuiltInEffectEventFunction';
export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
export const BuiltInAutodepsId = 'BuiltInAutoDepsId';
export const BuiltInEventHandlerId = 'BuiltInEventHandlerId';
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
@@ -1189,25 +1185,6 @@ addObject(BUILTIN_SHAPES, BuiltInUseTransitionId, [
],
]);
addObject(BUILTIN_SHAPES, BuiltInUseOptimisticId, [
['0', {kind: 'Poly'}],
[
'1',
addFunction(
BUILTIN_SHAPES,
[],
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
},
BuiltInSetOptimisticId,
),
],
]);
addObject(BUILTIN_SHAPES, BuiltInUseActionStateId, [
['0', {kind: 'Poly'}],
[
@@ -1266,20 +1243,7 @@ addFunction(
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
},
BuiltInEffectEventId,
);
addFunction(
BUILTIN_SHAPES,
[],
{
positionalParams: [],
restParam: Effect.ConditionallyMutate,
returnType: {kind: 'Poly'},
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
},
BuiltInEventHandlerId,
BuiltinEffectEventId,
);
/**

View File

@@ -11,7 +11,6 @@ import {
BasicBlock,
BlockId,
Instruction,
InstructionKind,
InstructionValue,
makeInstructionId,
Pattern,
@@ -33,32 +32,6 @@ export function* eachInstructionLValue(
yield* eachInstructionValueLValue(instr.value);
}
export function* eachInstructionLValueWithKind(
instr: ReactiveInstruction,
): Iterable<[Place, InstructionKind]> {
switch (instr.value.kind) {
case 'DeclareContext':
case 'StoreContext':
case 'DeclareLocal':
case 'StoreLocal': {
yield [instr.value.lvalue.place, instr.value.lvalue.kind];
break;
}
case 'Destructure': {
const kind = instr.value.lvalue.kind;
for (const place of eachPatternOperand(instr.value.lvalue.pattern)) {
yield [place, kind];
}
break;
}
case 'PostfixUpdate':
case 'PrefixUpdate': {
yield [instr.value.lvalue, InstructionKind.Reassign];
break;
}
}
}
export function* eachInstructionValueLValue(
value: ReactiveValue,
): Iterable<Place> {

View File

@@ -1,114 +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 {BlockId, computePostDominatorTree, HIRFunction, Place} from '../HIR';
import {PostDominator} from '../HIR/Dominator';
export type ControlDominators = (id: BlockId) => boolean;
/**
* Returns an object that lazily calculates whether particular blocks are controlled
* by values of interest. Which values matter are up to the caller.
*/
export function createControlDominators(
fn: HIRFunction,
isControlVariable: (place: Place) => boolean,
): ControlDominators {
const postDominators = computePostDominatorTree(fn, {
includeThrowsAsExitNode: false,
});
const postDominatorFrontierCache = new Map<BlockId, Set<BlockId>>();
function isControlledBlock(id: BlockId): boolean {
let controlBlocks = postDominatorFrontierCache.get(id);
if (controlBlocks === undefined) {
controlBlocks = postDominatorFrontier(fn, postDominators, id);
postDominatorFrontierCache.set(id, controlBlocks);
}
for (const blockId of controlBlocks) {
const controlBlock = fn.body.blocks.get(blockId)!;
switch (controlBlock.terminal.kind) {
case 'if':
case 'branch': {
if (isControlVariable(controlBlock.terminal.test)) {
return true;
}
break;
}
case 'switch': {
if (isControlVariable(controlBlock.terminal.test)) {
return true;
}
for (const case_ of controlBlock.terminal.cases) {
if (case_.test !== null && isControlVariable(case_.test)) {
return true;
}
}
break;
}
}
}
return false;
}
return isControlledBlock;
}
/*
* Computes the post-dominator frontier of @param block. These are immediate successors of nodes that
* post-dominate @param targetId and from which execution may not reach @param block. Intuitively, these
* are the earliest blocks from which execution branches such that it may or may not reach the target block.
*/
function postDominatorFrontier(
fn: HIRFunction,
postDominators: PostDominator<BlockId>,
targetId: BlockId,
): Set<BlockId> {
const visited = new Set<BlockId>();
const frontier = new Set<BlockId>();
const targetPostDominators = postDominatorsOf(fn, postDominators, targetId);
for (const blockId of [...targetPostDominators, targetId]) {
if (visited.has(blockId)) {
continue;
}
visited.add(blockId);
const block = fn.body.blocks.get(blockId)!;
for (const pred of block.preds) {
if (!targetPostDominators.has(pred)) {
// The predecessor does not always reach this block, we found an item on the frontier!
frontier.add(pred);
}
}
}
return frontier;
}
function postDominatorsOf(
fn: HIRFunction,
postDominators: PostDominator<BlockId>,
targetId: BlockId,
): Set<BlockId> {
const result = new Set<BlockId>();
const visited = new Set<BlockId>();
const queue = [targetId];
while (queue.length) {
const currentId = queue.shift()!;
if (visited.has(currentId)) {
continue;
}
visited.add(currentId);
const current = fn.body.blocks.get(currentId)!;
for (const pred of current.preds) {
const predPostDominator = postDominators.get(pred) ?? pred;
if (predPostDominator === targetId || result.has(predPostDominator)) {
result.add(pred);
}
queue.push(pred);
}
}
return result;
}

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

@@ -954,7 +954,6 @@ function applyEffect(
case ValueKind.Primitive: {
break;
}
case ValueKind.MaybeFrozen:
case ValueKind.Frozen: {
sourceType = 'frozen';
break;
@@ -2452,7 +2451,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

@@ -7,6 +7,7 @@
import {CompilerError} from '..';
import {
BlockId,
Effect,
Environment,
HIRFunction,
@@ -14,12 +15,14 @@ import {
IdentifierId,
Instruction,
Place,
computePostDominatorTree,
evaluatesToStableTypeOrContainer,
getHookKind,
isStableType,
isStableTypeContainer,
isUseOperator,
} from '../HIR';
import {PostDominator} from '../HIR/Dominator';
import {
eachInstructionLValue,
eachInstructionOperand,
@@ -32,7 +35,6 @@ import {
} from '../ReactiveScopes/InferReactiveScopeVariables';
import DisjointSet from '../Utils/DisjointSet';
import {assertExhaustive} from '../Utils/utils';
import {createControlDominators} from './ControlDominators';
/**
* Side map to track and propagate sources of stability (i.e. hook calls such as
@@ -210,9 +212,45 @@ export function inferReactivePlaces(fn: HIRFunction): void {
reactiveIdentifiers.markReactive(place);
}
const isReactiveControlledBlock = createControlDominators(fn, place =>
reactiveIdentifiers.isReactive(place),
);
const postDominators = computePostDominatorTree(fn, {
includeThrowsAsExitNode: false,
});
const postDominatorFrontierCache = new Map<BlockId, Set<BlockId>>();
function isReactiveControlledBlock(id: BlockId): boolean {
let controlBlocks = postDominatorFrontierCache.get(id);
if (controlBlocks === undefined) {
controlBlocks = postDominatorFrontier(fn, postDominators, id);
postDominatorFrontierCache.set(id, controlBlocks);
}
for (const blockId of controlBlocks) {
const controlBlock = fn.body.blocks.get(blockId)!;
switch (controlBlock.terminal.kind) {
case 'if':
case 'branch': {
if (reactiveIdentifiers.isReactive(controlBlock.terminal.test)) {
return true;
}
break;
}
case 'switch': {
if (reactiveIdentifiers.isReactive(controlBlock.terminal.test)) {
return true;
}
for (const case_ of controlBlock.terminal.cases) {
if (
case_.test !== null &&
reactiveIdentifiers.isReactive(case_.test)
) {
return true;
}
}
break;
}
}
}
return false;
}
do {
for (const [, block] of fn.body.blocks) {
@@ -373,6 +411,61 @@ export function inferReactivePlaces(fn: HIRFunction): void {
propagateReactivityToInnerFunctions(fn, true);
}
/*
* Computes the post-dominator frontier of @param block. These are immediate successors of nodes that
* post-dominate @param targetId and from which execution may not reach @param block. Intuitively, these
* are the earliest blocks from which execution branches such that it may or may not reach the target block.
*/
function postDominatorFrontier(
fn: HIRFunction,
postDominators: PostDominator<BlockId>,
targetId: BlockId,
): Set<BlockId> {
const visited = new Set<BlockId>();
const frontier = new Set<BlockId>();
const targetPostDominators = postDominatorsOf(fn, postDominators, targetId);
for (const blockId of [...targetPostDominators, targetId]) {
if (visited.has(blockId)) {
continue;
}
visited.add(blockId);
const block = fn.body.blocks.get(blockId)!;
for (const pred of block.preds) {
if (!targetPostDominators.has(pred)) {
// The predecessor does not always reach this block, we found an item on the frontier!
frontier.add(pred);
}
}
}
return frontier;
}
function postDominatorsOf(
fn: HIRFunction,
postDominators: PostDominator<BlockId>,
targetId: BlockId,
): Set<BlockId> {
const result = new Set<BlockId>();
const visited = new Set<BlockId>();
const queue = [targetId];
while (queue.length) {
const currentId = queue.shift()!;
if (visited.has(currentId)) {
continue;
}
visited.add(currentId);
const current = fn.body.blocks.get(currentId)!;
for (const pred of current.preds) {
const predPostDominator = postDominators.get(pred) ?? pred;
if (predPostDominator === targetId || result.has(predPostDominator)) {
result.add(pred);
}
queue.push(pred);
}
}
return result;
}
class ReactivityMap {
hasChanges: boolean = false;
reactive: Set<IdentifierId> = new Set();

View File

@@ -7,8 +7,6 @@
import {
BlockId,
Environment,
getHookKind,
HIRFunction,
Identifier,
IdentifierId,
@@ -70,14 +68,9 @@ export function deadCodeElimination(fn: HIRFunction): void {
}
class State {
env: Environment;
named: Set<string> = new Set();
identifiers: Set<IdentifierId> = new Set();
constructor(env: Environment) {
this.env = env;
}
// Mark the identifier as being referenced (not dead code)
reference(identifier: Identifier): void {
this.identifiers.add(identifier.id);
@@ -119,7 +112,7 @@ function findReferencedIdentifiers(fn: HIRFunction): State {
const hasLoop = hasBackEdge(fn);
const reversedBlocks = [...fn.body.blocks.values()].reverse();
const state = new State(fn.env);
const state = new State();
let size = state.count;
do {
size = state.count;
@@ -317,27 +310,12 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
// explicitly retain debugger statements to not break debugging workflows
return false;
}
case 'CallExpression':
case 'MethodCall': {
if (state.env.outputMode === 'ssr') {
const calleee =
value.kind === 'CallExpression' ? value.callee : value.property;
const hookKind = getHookKind(state.env, calleee.identifier);
switch (hookKind) {
case 'useState':
case 'useReducer':
case 'useRef': {
// unused refs can be removed
return true;
}
}
}
return false;
}
case 'Await':
case 'CallExpression':
case 'ComputedDelete':
case 'ComputedStore':
case 'PropertyDelete':
case 'MethodCall':
case 'PropertyStore':
case 'StoreGlobal': {
/*

View File

@@ -1,269 +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 {CompilerError} from '..';
import {
CallExpression,
getHookKind,
HIRFunction,
IdentifierId,
InstructionValue,
isArrayType,
isPlainObjectType,
isPrimitiveType,
isSetStateType,
isStartTransitionType,
LoadLocal,
StoreLocal,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {retainWhere} from '../Utils/utils';
/**
* Optimizes the code for running specifically in an SSR environment. This optimization
* asssumes that setState will not be called during render during initial mount, which
* allows inlining useState/useReducer.
*
* Optimizations:
* - Inline useState/useReducer
* - Remove effects
* - Remove refs where known to be unused during render (eg directly passed to a dom node)
* - Remove event handlers
*
* Note that an earlier pass already inlines useMemo/useCallback
*/
export function optimizeForSSR(fn: HIRFunction): void {
const inlinedState = new Map<IdentifierId, InstructionValue>();
/**
* First pass identifies useState/useReducer which can be safely inlined. Any use
* of the hook return other than destructuring (with a specific pattern) prevents
* inlining.
*
* Supported cases:
* - `const [state, ] = useState( <primitive-array-or-object> )`
* - `const [state, ] = useReducer(..., <value>)`
* - `const [state, ] = useReducer[..., <value>, <init>]`
*/
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {value} = instr;
switch (value.kind) {
case 'Destructure': {
if (
inlinedState.has(value.value.identifier.id) &&
value.lvalue.pattern.kind === 'ArrayPattern' &&
value.lvalue.pattern.items.length >= 1 &&
value.lvalue.pattern.items[0].kind === 'Identifier'
) {
// Allow destructuring of inlined states
continue;
}
break;
}
case 'MethodCall':
case 'CallExpression': {
const calleee =
value.kind === 'CallExpression' ? value.callee : value.property;
const hookKind = getHookKind(fn.env, calleee.identifier);
switch (hookKind) {
case 'useReducer': {
if (
value.args.length === 2 &&
value.args[1].kind === 'Identifier'
) {
const arg = value.args[1];
const replace: LoadLocal = {
kind: 'LoadLocal',
place: arg,
loc: arg.loc,
};
inlinedState.set(instr.lvalue.identifier.id, replace);
} else if (
value.args.length === 3 &&
value.args[1].kind === 'Identifier' &&
value.args[2].kind === 'Identifier'
) {
const arg = value.args[1];
const initializer = value.args[2];
const replace: CallExpression = {
kind: 'CallExpression',
callee: initializer,
args: [arg],
loc: value.loc,
};
inlinedState.set(instr.lvalue.identifier.id, replace);
}
break;
}
case 'useState': {
if (
value.args.length === 1 &&
value.args[0].kind === 'Identifier'
) {
const arg = value.args[0];
if (
isPrimitiveType(arg.identifier) ||
isPlainObjectType(arg.identifier) ||
isArrayType(arg.identifier)
) {
const replace: LoadLocal = {
kind: 'LoadLocal',
place: arg,
loc: arg.loc,
};
inlinedState.set(instr.lvalue.identifier.id, replace);
}
}
break;
}
}
}
}
// Any use of useState/useReducer return besides destructuring prevents inlining
if (inlinedState.size !== 0) {
for (const operand of eachInstructionValueOperand(value)) {
inlinedState.delete(operand.identifier.id);
}
}
}
if (inlinedState.size !== 0) {
for (const operand of eachTerminalOperand(block.terminal)) {
inlinedState.delete(operand.identifier.id);
}
}
}
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {value} = instr;
switch (value.kind) {
case 'FunctionExpression': {
if (hasKnownNonRenderCall(value.loweredFunc.func)) {
instr.value = {
kind: 'Primitive',
value: undefined,
loc: value.loc,
};
}
break;
}
case 'JsxExpression': {
if (
value.tag.kind === 'BuiltinTag' &&
value.tag.name.indexOf('-') === -1
) {
const tag = value.tag.name;
retainWhere(value.props, prop => {
return (
prop.kind === 'JsxSpreadAttribute' ||
(!isKnownEventHandler(tag, prop.name) && prop.name !== 'ref')
);
});
}
break;
}
case 'Destructure': {
if (inlinedState.has(value.value.identifier.id)) {
// Canonical check is part of determining if state can inline, this is for TS
CompilerError.invariant(
value.lvalue.pattern.kind === 'ArrayPattern' &&
value.lvalue.pattern.items.length >= 1 &&
value.lvalue.pattern.items[0].kind === 'Identifier',
{
reason:
'Expected a valid destructuring pattern for inlined state',
description: null,
details: [
{
kind: 'error',
message: 'Expected a valid destructuring pattern',
loc: value.loc,
},
],
},
);
const store: StoreLocal = {
kind: 'StoreLocal',
loc: value.loc,
type: null,
lvalue: {
kind: value.lvalue.kind,
place: value.lvalue.pattern.items[0],
},
value: value.value,
};
instr.value = store;
}
break;
}
case 'MethodCall':
case 'CallExpression': {
const calleee =
value.kind === 'CallExpression' ? value.callee : value.property;
const hookKind = getHookKind(fn.env, calleee.identifier);
switch (hookKind) {
case 'useEffectEvent': {
if (
value.args.length === 1 &&
value.args[0].kind === 'Identifier'
) {
const load: LoadLocal = {
kind: 'LoadLocal',
place: value.args[0],
loc: value.loc,
};
instr.value = load;
}
break;
}
case 'useEffect':
case 'useLayoutEffect':
case 'useInsertionEffect': {
// Drop effects
instr.value = {
kind: 'Primitive',
value: undefined,
loc: value.loc,
};
break;
}
case 'useReducer':
case 'useState': {
const replace = inlinedState.get(instr.lvalue.identifier.id);
if (replace != null) {
instr.value = replace;
}
break;
}
}
}
}
}
}
}
function hasKnownNonRenderCall(fn: HIRFunction): boolean {
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
if (
instr.value.kind === 'CallExpression' &&
(isSetStateType(instr.value.callee.identifier) ||
isStartTransitionType(instr.value.callee.identifier))
) {
return true;
}
}
}
return false;
}
const EVENT_HANDLER_PATTERN = /^on[A-Z]/;
function isKnownEventHandler(_tag: string, prop: string): boolean {
return EVENT_HANDLER_PATTERN.test(prop);
}

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;
@@ -1362,6 +1359,8 @@ function codegenInstructionNullable(
value = null;
} else {
lvalue = instr.value.lvalue.pattern;
let hasReassign = false;
let hasDeclaration = false;
for (const place of eachPatternOperand(lvalue)) {
if (
kind !== InstructionKind.Reassign &&
@@ -1369,6 +1368,26 @@ function codegenInstructionNullable(
) {
cx.temp.set(place.identifier.declarationId, null);
}
const isDeclared = cx.hasDeclared(place.identifier);
hasReassign ||= isDeclared;
hasDeclaration ||= !isDeclared;
}
if (hasReassign && hasDeclaration) {
CompilerError.invariant(false, {
reason:
'Encountered a destructuring operation where some identifiers are already declared (reassignments) but others are not (declarations)',
description: null,
details: [
{
kind: 'error',
loc: instr.loc,
message: null,
},
],
suggestions: null,
});
} else if (hasReassign) {
kind = InstructionKind.Reassign;
}
value = codegenPlaceToExpression(cx, instr.value.value);
}
@@ -1775,7 +1794,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

@@ -19,11 +19,7 @@ import {
promoteTemporary,
} from '../HIR';
import {clonePlaceToTemporary} from '../HIR/HIRBuilder';
import {
eachInstructionLValueWithKind,
eachPatternOperand,
mapPatternOperands,
} from '../HIR/visitors';
import {eachPatternOperand, mapPatternOperands} from '../HIR/visitors';
import {
ReactiveFunctionTransform,
Transformed,
@@ -117,9 +113,6 @@ class Visitor extends ReactiveFunctionTransform<State> {
): Transformed<ReactiveStatement> {
this.visitInstruction(instruction, state);
let instructionsToProcess: Array<ReactiveInstruction> = [instruction];
let result: Transformed<ReactiveStatement> = {kind: 'keep'};
if (instruction.value.kind === 'Destructure') {
const transformed = transformDestructuring(
state,
@@ -127,8 +120,7 @@ class Visitor extends ReactiveFunctionTransform<State> {
instruction.value,
);
if (transformed) {
instructionsToProcess = transformed;
result = {
return {
kind: 'replace-many',
value: transformed.map(instruction => ({
kind: 'instruction',
@@ -137,17 +129,7 @@ class Visitor extends ReactiveFunctionTransform<State> {
};
}
}
// Update state.declared with declarations from the instruction(s)
for (const instr of instructionsToProcess) {
for (const [place, kind] of eachInstructionLValueWithKind(instr)) {
if (kind !== InstructionKind.Reassign) {
state.declared.add(place.identifier.declarationId);
}
}
}
return result;
return {kind: 'keep'};
}
}
@@ -162,13 +144,10 @@ function transformDestructuring(
const isDeclared = state.declared.has(place.identifier.declarationId);
if (isDeclared) {
reassigned.add(place.identifier.id);
} else {
hasDeclaration = true;
}
hasDeclaration ||= !isDeclared;
}
if (!hasDeclaration) {
// all reassignments
destructure.lvalue.kind = InstructionKind.Reassign;
if (reassigned.size === 0 || !hasDeclaration) {
return null;
}
/*

View File

@@ -25,7 +25,6 @@ import {
} from '../HIR/HIR';
import {
BuiltInArrayId,
BuiltInEventHandlerId,
BuiltInFunctionId,
BuiltInJsxId,
BuiltInMixedReadonlyId,
@@ -472,41 +471,6 @@ function* generateInstructionTypes(
}
}
}
if (env.config.enableInferEventHandlers) {
if (
value.kind === 'JsxExpression' &&
value.tag.kind === 'BuiltinTag' &&
!value.tag.name.includes('-')
) {
/*
* Infer event handler types for built-in DOM elements.
* Props starting with "on" (e.g., onClick, onSubmit) on primitive tags
* are inferred as event handlers. This allows functions with ref access
* to be passed to these props, since DOM event handlers are guaranteed
* by React to only execute in response to events, never during render.
*
* We exclude tags with hyphens to avoid web components (custom elements),
* which are required by the HTML spec to contain a hyphen. Web components
* may call event handler props during their lifecycle methods (e.g.,
* connectedCallback), which would be unsafe for ref access.
*/
for (const prop of value.props) {
if (
prop.kind === 'JsxAttribute' &&
prop.name.startsWith('on') &&
prop.name.length > 2 &&
prop.name[2] === prop.name[2].toUpperCase()
) {
yield equation(prop.place.identifier.type, {
kind: 'Function',
shapeId: BuiltInEventHandlerId,
return: makeType(),
isConstructor: false,
});
}
}
}
}
yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId});
break;
}

View File

@@ -1,768 +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,
isSubPath,
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 had exhaustive dependencies.
* Memoization with missing or extra reactive dependencies is invalid React
* and compilation can change behavior, causing a value to be computed more
* or less times.
*
* TODOs:
* - Better handling of cases where we infer multiple dependencies related to a single
* variable. Eg if the user has dep `x` and we inferred `x.y, x.z`, the user's dep
* is sufficient.
* - 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) ||
isSubPath(manualDependency.path, inferredDependency.path))
) {
hasMatchingManualDependency = true;
matched.add(manualDependency);
}
}
if (!hasMatchingManualDependency) {
missing.push(inferredDependency);
}
}
for (const dep of startMemo.deps ?? []) {
if (
matched.has(dep) ||
(dep.root.kind === 'NamedLocal' &&
!reactive.has(dep.root.value.identifier.id))
) {
continue;
}
extra.push(dep);
}
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) {
// Error
const diagnostic = CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
reason: 'Found non-exhaustive dependencies',
description:
'Missing dependencies can cause a value not to update when those inputs change, ' +
'resulting in stale UI. This memoization cannot be safely rewritten by the compiler.',
suggestions,
});
for (const dep of missing) {
diagnostic.withDetails({
kind: 'error',
message: `Missing dependency \`${printInferredDependency(dep)}\``,
loc: dep.loc,
});
}
error.pushDiagnostic(diagnostic);
} else if (extra.length !== 0) {
const diagnostic = CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
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. This memoization cannot be safely ' +
'rewritten by the compiler',
});
diagnostic.withDetails({
kind: 'error',
message: `Unnecessary dependencies ${extra.map(dep => `\`${printManualMemoDependency(dep)}\``).join(', ')}`,
loc: value.loc,
});
error.pushDiagnostic(diagnostic);
}
}
dependencies.clear();
locals.clear();
startMemo = null;
}
collectDependencies(fn, temporaries, {
onStartMemoize,
onFinishMemoize,
});
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,
): Extract<Temporary, {kind: 'Function'}> {
const optionals = findOptionalPlaces(fn);
if (DEBUG) {
console.log(prettyFormat(optionals));
}
const locals: Set<IdentifierId> = new Set();
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') {
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,
);
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

@@ -22,7 +22,6 @@ import {
BasicBlock,
isUseRefType,
SourceLocation,
ArrayExpression,
} from '../HIR';
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
@@ -37,23 +36,15 @@ type DerivationMetadata = {
isStateSource: boolean;
};
type EffectMetadata = {
effect: HIRFunction;
dependencies: ArrayExpression;
};
type ValidationContext = {
readonly functions: Map<IdentifierId, FunctionExpression>;
readonly candidateDependencies: Map<IdentifierId, ArrayExpression>;
readonly errors: CompilerError;
readonly derivationCache: DerivationCache;
readonly effectsCache: Map<IdentifierId, EffectMetadata>;
readonly effects: Set<HIRFunction>;
readonly setStateLoads: Map<IdentifierId, IdentifierId | null>;
readonly setStateUsages: Map<IdentifierId, Set<SourceLocation>>;
};
const MAX_FIXPOINT_ITERATIONS = 100;
class DerivationCache {
hasChanges: boolean = false;
cache: Map<IdentifierId, DerivationMetadata> = new Map();
@@ -184,20 +175,18 @@ export function validateNoDerivedComputationsInEffects_exp(
fn: HIRFunction,
): Result<void, CompilerError> {
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
const derivationCache = new DerivationCache();
const errors = new CompilerError();
const effectsCache: Map<IdentifierId, EffectMetadata> = new Map();
const effects: Set<HIRFunction> = new Set();
const setStateLoads: Map<IdentifierId, IdentifierId> = new Map();
const setStateUsages: Map<IdentifierId, Set<SourceLocation>> = new Map();
const context: ValidationContext = {
functions,
candidateDependencies,
errors,
derivationCache,
effectsCache,
effects,
setStateLoads,
setStateUsages,
};
@@ -226,7 +215,6 @@ export function validateNoDerivedComputationsInEffects_exp(
}
let isFirstPass = true;
let iterationCount = 0;
do {
context.derivationCache.takeSnapshot();
@@ -239,23 +227,10 @@ export function validateNoDerivedComputationsInEffects_exp(
context.derivationCache.checkForChanges();
isFirstPass = false;
iterationCount++;
CompilerError.invariant(iterationCount < MAX_FIXPOINT_ITERATIONS, {
reason:
'[ValidateNoDerivedComputationsInEffects] Fixpoint iteration failed to converge.',
description: `Fixpoint iteration exceeded ${MAX_FIXPOINT_ITERATIONS} iterations while tracking derivations. This suggests a cyclic dependency in the derivation cache.`,
details: [
{
kind: 'error',
loc: fn.loc,
message: `Exceeded ${MAX_FIXPOINT_ITERATIONS} iterations in ValidateNoDerivedComputationsInEffects`,
},
],
});
} while (context.derivationCache.snapshot());
for (const [, effect] of effectsCache) {
validateEffect(effect.effect, effect.dependencies, context);
for (const effect of effects) {
validateEffect(effect, context);
}
return errors.asResult();
@@ -379,16 +354,10 @@ function recordInstructionDerivations(
value.args[1].kind === 'Identifier'
) {
const effectFunction = context.functions.get(value.args[0].identifier.id);
const deps = context.candidateDependencies.get(
value.args[1].identifier.id,
);
if (effectFunction != null && deps != null) {
context.effectsCache.set(value.args[0].identifier.id, {
effect: effectFunction.loweredFunc.func,
dependencies: deps,
});
if (effectFunction != null) {
context.effects.add(effectFunction.loweredFunc.func);
}
} else if (isUseStateType(lvalue.identifier)) {
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
typeOfValue = 'fromState';
context.derivationCache.addDerivationEntry(
lvalue,
@@ -398,8 +367,6 @@ function recordInstructionDerivations(
);
return;
}
} else if (value.kind === 'ArrayExpression') {
context.candidateDependencies.set(lvalue.identifier.id, value);
}
for (const operand of eachInstructionOperand(instr)) {
@@ -438,14 +405,6 @@ function recordInstructionDerivations(
);
}
if (value.kind === 'FunctionExpression') {
/*
* We don't want to record effect mutations of FunctionExpressions the mutations will happen in the
* function body and we will record them there.
*/
return;
}
for (const operand of eachInstructionOperand(instr)) {
switch (operand.effect) {
case Effect.Capture:
@@ -536,19 +495,6 @@ function buildTreeNode(
const namedSiblings: Set<string> = new Set();
for (const childId of sourceMetadata.sourcesIds) {
CompilerError.invariant(childId !== sourceId, {
reason:
'Unexpected self-reference: a value should not have itself as a source',
description: null,
details: [
{
kind: 'error',
loc: sourceMetadata.place.loc,
message: null,
},
],
});
const childNodes = buildTreeNode(
childId,
context,
@@ -650,7 +596,6 @@ function getFnLocalDeps(
function validateEffect(
effectFunction: HIRFunction,
dependencies: ArrayExpression,
context: ValidationContext,
): void {
const seenBlocks: Set<BlockId> = new Set();
@@ -667,16 +612,6 @@ function validateEffect(
Set<SourceLocation>
> = new Map();
// Consider setStates in the effect's dependency array as being part of effectSetStateUsages
for (const dep of dependencies.elements) {
if (dep.kind === 'Identifier') {
const root = getRootSetState(dep.identifier.id, context.setStateLoads);
if (root !== null) {
effectSetStateUsages.set(root, new Set([dep.loc]));
}
}
}
let cleanUpFunctionDeps: Set<IdentifierId> | undefined;
const globals: Set<IdentifierId> = new Set();
@@ -727,18 +662,6 @@ function validateEffect(
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier'
) {
const calleeMetadata = context.derivationCache.cache.get(
instr.value.callee.identifier.id,
);
/*
* If the setState comes from a source other than local state skip
* since the fix is not to calculate in render
*/
if (calleeMetadata?.typeOfValue != 'fromState') {
continue;
}
const argMetadata = context.derivationCache.cache.get(
instr.value.args[0].identifier.id,
);

View File

@@ -14,14 +14,12 @@ import {
BlockId,
HIRFunction,
IdentifierId,
Identifier,
Place,
SourceLocation,
getHookKindForType,
isRefValueType,
isUseRefType,
} from '../HIR';
import {BuiltInEventHandlerId} from '../HIR/ObjectShape';
import {
eachInstructionOperand,
eachInstructionValueOperand,
@@ -185,11 +183,6 @@ function refTypeOfType(place: Place): RefAccessType {
}
}
function isEventHandlerType(identifier: Identifier): boolean {
const type = identifier.type;
return type.kind === 'Function' && type.shapeId === BuiltInEventHandlerId;
}
function tyEqual(a: RefAccessType, b: RefAccessType): boolean {
if (a.kind !== b.kind) {
return false;
@@ -526,9 +519,6 @@ function validateNoRefAccessInRenderImpl(
*/
if (!didError) {
const isRefLValue = isUseRefType(instr.lvalue.identifier);
const isEventHandlerLValue = isEventHandlerType(
instr.lvalue.identifier,
);
for (const operand of eachInstructionValueOperand(instr.value)) {
/**
* By default we check that function call operands are not refs,
@@ -536,16 +526,29 @@ function validateNoRefAccessInRenderImpl(
*/
if (
isRefLValue ||
isEventHandlerLValue ||
(hookKind != null &&
hookKind !== 'useState' &&
hookKind !== 'useReducer')
) {
/**
* Allow passing refs or ref-accessing functions when:
* 1. lvalue is a ref (mergeRefs pattern: `mergeRefs(ref1, ref2)`)
* 2. lvalue is an event handler (DOM events execute outside render)
* 3. calling hooks (independently validated for ref safety)
* Special cases:
*
* 1. the lvalue is a ref
* In general passing a ref to a function may access that ref
* value during render, so we disallow it.
*
* The main exception is the "mergeRefs" pattern, ie a function
* that accepts multiple refs as arguments (or an array of refs)
* and returns a new, aggregated ref. If the lvalue is a ref,
* we assume that the user is doing this pattern and allow passing
* refs.
*
* Eg `const mergedRef = mergeRefs(ref1, ref2)`
*
* 2. calling hooks
*
* Hooks are independently checked to ensure they don't access refs
* during render.
*/
validateNoDirectRefValueAccess(errors, operand, env);
} else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {

View File

@@ -21,17 +21,13 @@ import {
isUseRefType,
isRefValueType,
Place,
Effect,
BlockId,
} from '../HIR';
import {
eachInstructionLValue,
eachInstructionValueOperand,
} from '../HIR/visitors';
import {createControlDominators} from '../Inference/ControlDominators';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {Result} from '../Utils/Result';
import {assertExhaustive, Iterable_some} from '../Utils/utils';
import {Iterable_some} from '../Utils/utils';
/**
* Validates against calling setState in the body of an effect (useEffect and friends),
@@ -144,8 +140,6 @@ function getSetStateCall(
setStateFunctions: Map<IdentifierId, Place>,
env: Environment,
): Place | null {
const enableAllowSetStateFromRefsInEffects =
env.config.enableAllowSetStateFromRefsInEffects;
const refDerivedValues: Set<IdentifierId> = new Set();
const isDerivedFromRef = (place: Place): boolean => {
@@ -156,38 +150,9 @@ function getSetStateCall(
);
};
const isRefControlledBlock: (id: BlockId) => boolean =
enableAllowSetStateFromRefsInEffects
? createControlDominators(fn, place => isDerivedFromRef(place))
: (): boolean => false;
for (const [, block] of fn.body.blocks) {
if (enableAllowSetStateFromRefsInEffects) {
for (const phi of block.phis) {
if (isDerivedFromRef(phi.place)) {
continue;
}
let isPhiDerivedFromRef = false;
for (const [, operand] of phi.operands) {
if (isDerivedFromRef(operand)) {
isPhiDerivedFromRef = true;
break;
}
}
if (isPhiDerivedFromRef) {
refDerivedValues.add(phi.place.identifier.id);
} else {
for (const [pred] of phi.operands) {
if (isRefControlledBlock(pred)) {
refDerivedValues.add(phi.place.identifier.id);
break;
}
}
}
}
}
for (const instr of block.instructions) {
if (enableAllowSetStateFromRefsInEffects) {
if (env.config.enableAllowSetStateFromRefsInEffects) {
const hasRefOperand = Iterable_some(
eachInstructionValueOperand(instr.value),
isDerivedFromRef,
@@ -197,46 +162,6 @@ function getSetStateCall(
for (const lvalue of eachInstructionLValue(instr)) {
refDerivedValues.add(lvalue.identifier.id);
}
// Ref-derived values can also propagate through mutation
for (const operand of eachInstructionValueOperand(instr.value)) {
switch (operand.effect) {
case Effect.Capture:
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
if (isMutable(instr, operand)) {
refDerivedValues.add(operand.identifier.id);
}
break;
}
case Effect.Freeze:
case Effect.Read: {
// no-op
break;
}
case Effect.Unknown: {
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
details: [
{
kind: 'error',
loc: operand.loc,
message: null,
},
],
suggestions: null,
});
}
default: {
assertExhaustive(
operand.effect,
`Unexpected effect kind \`${operand.effect}\``,
);
}
}
}
}
if (
@@ -278,7 +203,7 @@ function getSetStateCall(
isSetStateType(callee.identifier) ||
setStateFunctions.has(callee.identifier.id)
) {
if (enableAllowSetStateFromRefsInEffects) {
if (env.config.enableAllowSetStateFromRefsInEffects) {
const arg = instr.value.args.at(0);
if (
arg !== undefined &&
@@ -291,8 +216,6 @@ function getSetStateCall(
* be needed when initial layout measurements from refs need to be stored in state.
*/
return null;
} else if (isRefControlledBlock(block.id)) {
continue;
}
}
/*

View File

@@ -1,206 +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 {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerDiagnostic, CompilerError, ErrorCategory} from '..';
import {CodegenFunction} from '../ReactiveScopes';
import {Result} from '../Utils/Result';
/**
* IMPORTANT: This validation is only intended for use in unit tests.
* It is not intended for use in production.
*
* This validation is used to ensure that the generated AST has proper source locations
* for "important" original nodes.
*
* There's one big gotcha with this validation: it only works if the "important" original nodes
* are not optimized away by the compiler.
*
* When that scenario happens, we should just update the fixture to not include a node that has no
* corresponding node in the generated AST due to being completely removed during compilation.
*/
/**
* Some common node types that are important for coverage tracking.
* Based on istanbul-lib-instrument
*/
const IMPORTANT_INSTRUMENTED_TYPES = new Set([
'ArrowFunctionExpression',
'AssignmentPattern',
'ObjectMethod',
'ExpressionStatement',
'BreakStatement',
'ContinueStatement',
'ReturnStatement',
'ThrowStatement',
'TryStatement',
'VariableDeclarator',
'IfStatement',
'ForStatement',
'ForInStatement',
'ForOfStatement',
'WhileStatement',
'DoWhileStatement',
'SwitchStatement',
'SwitchCase',
'WithStatement',
'FunctionDeclaration',
'FunctionExpression',
'LabeledStatement',
'ConditionalExpression',
'LogicalExpression',
]);
/**
* Check if a node is a manual memoization call that the compiler optimizes away.
* These include useMemo and useCallback calls, which are intentionally removed
* by the DropManualMemoization pass.
*/
function isManualMemoization(node: t.Node): boolean {
// Check if this is a useMemo/useCallback call expression
if (t.isCallExpression(node)) {
const callee = node.callee;
if (t.isIdentifier(callee)) {
return callee.name === 'useMemo' || callee.name === 'useCallback';
}
if (
t.isMemberExpression(callee) &&
t.isIdentifier(callee.property) &&
t.isIdentifier(callee.object)
) {
return (
callee.object.name === 'React' &&
(callee.property.name === 'useMemo' ||
callee.property.name === 'useCallback')
);
}
}
return false;
}
/**
* Create a location key for comparison. We compare by line/column/source,
* not by object identity.
*/
function locationKey(loc: t.SourceLocation): string {
return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`;
}
/**
* Validates that important source locations from the original code are preserved
* in the generated AST. This ensures that Istanbul coverage instrumentation can
* properly map back to the original source code.
*
* The validator:
* 1. Collects locations from "important" nodes in the original AST (those that
* Istanbul instruments for coverage tracking)
* 2. Exempts known compiler optimizations (useMemo/useCallback removal)
* 3. Verifies that all important locations appear somewhere in the generated AST
*
* Missing locations can cause Istanbul to fail to track coverage for certain
* code paths, leading to inaccurate coverage reports.
*/
export function validateSourceLocations(
func: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
generatedAst: CodegenFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
// Step 1: Collect important locations from the original source
const importantOriginalLocations = new Map<
string,
{loc: t.SourceLocation; nodeType: string}
>();
func.traverse({
enter(path) {
const node = path.node;
// Only track node types that Istanbul instruments
if (!IMPORTANT_INSTRUMENTED_TYPES.has(node.type)) {
return;
}
// Skip manual memoization that the compiler intentionally removes
if (isManualMemoization(node)) {
return;
}
// Collect the location if it exists
if (node.loc) {
const key = locationKey(node.loc);
importantOriginalLocations.set(key, {
loc: node.loc,
nodeType: node.type,
});
}
},
});
// Step 2: Collect all locations from the generated AST
const generatedLocations = new Set<string>();
function collectGeneratedLocations(node: t.Node): void {
if (node.loc) {
generatedLocations.add(locationKey(node.loc));
}
// Use Babel's VISITOR_KEYS to traverse only actual node properties
const keys = t.VISITOR_KEYS[node.type as keyof typeof t.VISITOR_KEYS];
if (!keys) {
return;
}
for (const key of keys) {
const value = (node as any)[key];
if (Array.isArray(value)) {
for (const item of value) {
if (t.isNode(item)) {
collectGeneratedLocations(item);
}
}
} else if (t.isNode(value)) {
collectGeneratedLocations(value);
}
}
}
// Collect from main function body
collectGeneratedLocations(generatedAst.body);
// Collect from outlined functions
for (const outlined of generatedAst.outlined) {
collectGeneratedLocations(outlined.fn.body);
}
// Step 3: Validate that all important locations are preserved
for (const [key, {loc, nodeType}] of importantOriginalLocations) {
if (!generatedLocations.has(key)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason: 'Important source location missing in generated code',
description:
`Source location for ${nodeType} is missing in the generated output. This can cause coverage instrumentation ` +
`to fail to track this code properly, resulting in inaccurate coverage reports.`,
}).withDetails({
kind: 'error',
loc,
message: null,
}),
);
}
}
return errors.asResult();
}

View File

@@ -12,5 +12,4 @@ export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
export {validateNoRefAccessInRender} from './ValidateNoRefAccessInRender';
export {validateNoSetStateInRender} from './ValidateNoSetStateInRender';
export {validatePreservedManualMemoization} from './ValidatePreservedManualMemoization';
export {validateSourceLocations} from './ValidateSourceLocations';
export {validateUseMemo} from './ValidateUseMemo';

View File

@@ -1,148 +0,0 @@
## Input
```javascript
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates react-hook-form's handleSubmit
function handleSubmit<T>(callback: (data: T) => void | Promise<void>) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}
// Simulates an upload function
async function upload(file: any): Promise<{blob: {url: string}}> {
return {blob: {url: 'https://example.com/file.jpg'}};
}
interface SignatureRef {
toFile(): any;
}
function Component() {
const ref = useRef<SignatureRef>(null);
const onSubmit = async (value: any) => {
// This should be allowed: accessing ref.current in an async event handler
// that's wrapped and passed to onSubmit prop
let sigUrl: string;
if (value.hasSignature) {
const {blob} = await upload(ref.current?.toFile());
sigUrl = blob?.url || '';
} else {
sigUrl = value.signature;
}
console.log('Signature URL:', sigUrl);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" name="signature" />
<button type="submit">Submit</button>
</form>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers
import { useRef } from "react";
// Simulates react-hook-form's handleSubmit
function handleSubmit(callback) {
const $ = _c(2);
let t0;
if ($[0] !== callback) {
t0 = (event) => {
event.preventDefault();
callback({} as T);
};
$[0] = callback;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
// Simulates an upload function
async function upload(file) {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = { blob: { url: "https://example.com/file.jpg" } };
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
interface SignatureRef {
toFile(): any;
}
function Component() {
const $ = _c(4);
const ref = useRef(null);
const onSubmit = async (value) => {
let sigUrl;
if (value.hasSignature) {
const { blob } = await upload(ref.current?.toFile());
sigUrl = blob?.url || "";
} else {
sigUrl = value.signature;
}
console.log("Signature URL:", sigUrl);
};
const t0 = handleSubmit(onSubmit);
let t1;
let t2;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <input type="text" name="signature" />;
t2 = <button type="submit">Submit</button>;
$[0] = t1;
$[1] = t2;
} else {
t1 = $[0];
t2 = $[1];
}
let t3;
if ($[2] !== t0) {
t3 = (
<form onSubmit={t0}>
{t1}
{t2}
</form>
);
$[2] = t0;
$[3] = t3;
} else {
t3 = $[3];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) <form><input type="text" name="signature"><button type="submit">Submit</button></form>

View File

@@ -1,48 +0,0 @@
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates react-hook-form's handleSubmit
function handleSubmit<T>(callback: (data: T) => void | Promise<void>) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}
// Simulates an upload function
async function upload(file: any): Promise<{blob: {url: string}}> {
return {blob: {url: 'https://example.com/file.jpg'}};
}
interface SignatureRef {
toFile(): any;
}
function Component() {
const ref = useRef<SignatureRef>(null);
const onSubmit = async (value: any) => {
// This should be allowed: accessing ref.current in an async event handler
// that's wrapped and passed to onSubmit prop
let sigUrl: string;
if (value.hasSignature) {
const {blob} = await upload(ref.current?.toFile());
sigUrl = blob?.url || '';
} else {
sigUrl = value.signature;
}
console.log('Signature URL:', sigUrl);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" name="signature" />
<button type="submit">Submit</button>
</form>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

View File

@@ -1,101 +0,0 @@
## Input
```javascript
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates react-hook-form's handleSubmit or similar event handler wrappers
function handleSubmit<T>(callback: (data: T) => void) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}
function Component() {
const ref = useRef<HTMLInputElement>(null);
const onSubmit = (data: any) => {
// This should be allowed: accessing ref.current in an event handler
// that's wrapped by handleSubmit and passed to onSubmit prop
if (ref.current !== null) {
console.log(ref.current.value);
}
};
return (
<>
<input ref={ref} />
<form onSubmit={handleSubmit(onSubmit)}>
<button type="submit">Submit</button>
</form>
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers
import { useRef } from "react";
// Simulates react-hook-form's handleSubmit or similar event handler wrappers
function handleSubmit(callback) {
const $ = _c(2);
let t0;
if ($[0] !== callback) {
t0 = (event) => {
event.preventDefault();
callback({} as T);
};
$[0] = callback;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
function Component() {
const $ = _c(1);
const ref = useRef(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const onSubmit = (data) => {
if (ref.current !== null) {
console.log(ref.current.value);
}
};
t0 = (
<>
<input ref={ref} />
<form onSubmit={handleSubmit(onSubmit)}>
<button type="submit">Submit</button>
</form>
</>
);
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) <input><form><button type="submit">Submit</button></form>

View File

@@ -1,36 +0,0 @@
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates react-hook-form's handleSubmit or similar event handler wrappers
function handleSubmit<T>(callback: (data: T) => void) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}
function Component() {
const ref = useRef<HTMLInputElement>(null);
const onSubmit = (data: any) => {
// This should be allowed: accessing ref.current in an event handler
// that's wrapped by handleSubmit and passed to onSubmit prop
if (ref.current !== null) {
console.log(ref.current.value);
}
};
return (
<>
<input ref={ref} />
<form onSubmit={handleSubmit(onSubmit)}>
<button type="submit">Submit</button>
</form>
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

View File

@@ -1,63 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component({prop}) {
const [s, setS] = useState(0);
useEffect(() => {
setS(prop);
}, [prop, setS]);
return <div>{prop}</div>;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component(t0) {
const $ = _c(5);
const { prop } = t0;
const [, setS] = useState(0);
let t1;
let t2;
if ($[0] !== prop) {
t1 = () => {
setS(prop);
};
t2 = [prop, setS];
$[0] = prop;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] !== prop) {
t3 = <div>{prop}</div>;
$[3] = prop;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
```
## Logs
```
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [prop]\n\nData Flow Tree:\n└── prop (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":6,"column":4,"index":150},"end":{"line":6,"column":8,"index":154},"filename":"effect-used-in-dep-array-still-errors.ts","identifierName":"setS"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":64},"end":{"line":10,"column":1,"index":212},"filename":"effect-used-in-dep-array-still-errors.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,10 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component({prop}) {
const [s, setS] = useState(0);
useEffect(() => {
setS(prop);
}, [prop, setS]);
return <div>{prop}</div>;
}

View File

@@ -1,65 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @enableTreatSetIdentifiersAsStateSetters @loggerTestOnly
function Component({setParentState, prop}) {
useEffect(() => {
setParentState(prop);
}, [prop]);
return <div>{prop}</div>;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @enableTreatSetIdentifiersAsStateSetters @loggerTestOnly
function Component(t0) {
const $ = _c(7);
const { setParentState, prop } = t0;
let t1;
if ($[0] !== prop || $[1] !== setParentState) {
t1 = () => {
setParentState(prop);
};
$[0] = prop;
$[1] = setParentState;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] !== prop) {
t2 = [prop];
$[3] = prop;
$[4] = t2;
} else {
t2 = $[4];
}
useEffect(t1, t2);
let t3;
if ($[5] !== prop) {
t3 = <div>{prop}</div>;
$[5] = prop;
$[6] = t3;
} else {
t3 = $[6];
}
return t3;
}
```
## Logs
```
{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":105},"end":{"line":9,"column":1,"index":240},"filename":"from-props-setstate-in-effect-no-error.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,9 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @enableTreatSetIdentifiersAsStateSetters @loggerTestOnly
function Component({setParentState, prop}) {
useEffect(() => {
setParentState(prop);
}, [prop]);
return <div>{prop}</div>;
}

View File

@@ -1,115 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component() {
const [foo, setFoo] = useState({});
const [bar, setBar] = useState(new Set());
/*
* isChanged is considered context of the effect's function expression,
* if we don't bail out of effect mutation derivation tracking, isChanged
* will inherit the sources of the effect's function expression.
*
* This is innacurate and with the multiple passes ends up causing an infinite loop.
*/
useEffect(() => {
let isChanged = false;
const newData = foo.map(val => {
bar.someMethod(val);
isChanged = true;
});
if (isChanged) {
setFoo(newData);
}
}, [foo, bar]);
return (
<div>
{foo}, {bar}
</div>
);
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component() {
const $ = _c(9);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = {};
$[0] = t0;
} else {
t0 = $[0];
}
const [foo, setFoo] = useState(t0);
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = new Set();
$[1] = t1;
} else {
t1 = $[1];
}
const [bar] = useState(t1);
let t2;
let t3;
if ($[2] !== bar || $[3] !== foo) {
t2 = () => {
let isChanged = false;
const newData = foo.map((val) => {
bar.someMethod(val);
isChanged = true;
});
if (isChanged) {
setFoo(newData);
}
};
t3 = [foo, bar];
$[2] = bar;
$[3] = foo;
$[4] = t2;
$[5] = t3;
} else {
t2 = $[4];
t3 = $[5];
}
useEffect(t2, t3);
let t4;
if ($[6] !== bar || $[7] !== foo) {
t4 = (
<div>
{foo}, {bar}
</div>
);
$[6] = bar;
$[7] = foo;
$[8] = t4;
} else {
t4 = $[8];
}
return t4;
}
```
## Logs
```
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nState: [foo, bar]\n\nData Flow Tree:\n└── newData\n ├── foo (State)\n └── bar (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":23,"column":6,"index":663},"end":{"line":23,"column":12,"index":669},"filename":"function-expression-mutation-edge-case.ts","identifierName":"setFoo"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":64},"end":{"line":32,"column":1,"index":762},"filename":"function-expression-mutation-edge-case.ts"},"fnName":"Component","memoSlots":9,"memoBlocks":4,"memoValues":5,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,32 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
function Component() {
const [foo, setFoo] = useState({});
const [bar, setBar] = useState(new Set());
/*
* isChanged is considered context of the effect's function expression,
* if we don't bail out of effect mutation derivation tracking, isChanged
* will inherit the sources of the effect's function expression.
*
* This is innacurate and with the multiple passes ends up causing an infinite loop.
*/
useEffect(() => {
let isChanged = false;
const newData = foo.map(val => {
bar.someMethod(val);
isChanged = true;
});
if (isChanged) {
setFoo(newData);
}
}, [foo, bar]);
return (
<div>
{foo}, {bar}
</div>
);
}

View File

@@ -1,98 +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;
}, [x?.y.z?.a.b]);
const b = useMemo(() => {
return x.y.z?.a;
}, [x.y.z.a]);
const c = useMemo(() => {
return x?.y.z.a?.b;
}, [x?.y.z.a?.b.z]);
const d = useMemo(() => {
return x?.y?.[(console.log(y), z?.b)];
}, [x?.y, y, z?.b]);
const e = useMemo(() => {
const e = [];
e.push(x);
return e;
}, [x]);
const f = useMemo(() => {
return [];
}, [x, y.z, z?.y?.a]);
return <Stringify results={[a, b, c, d, e, f]} />;
}
```
## Error
```
Found 4 errors:
Compilation Skipped: Found non-exhaustive dependencies
Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI. This memoization cannot be safely rewritten by the compiler..
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 | }, [x?.y.z?.a.b]);
9 | const b = useMemo(() => {
10 | return x.y.z?.a;
Compilation Skipped: Found non-exhaustive dependencies
Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI. This memoization cannot be safely rewritten by the compiler..
error.invalid-exhaustive-deps.ts:10:11
8 | }, [x?.y.z?.a.b]);
9 | const b = useMemo(() => {
> 10 | return x.y.z?.a;
| ^^^^^^^^ Missing dependency `x.y.z?.a`
11 | }, [x.y.z.a]);
12 | const c = useMemo(() => {
13 | return x?.y.z.a?.b;
Compilation Skipped: Found non-exhaustive dependencies
Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI. This memoization cannot be safely rewritten by the compiler..
error.invalid-exhaustive-deps.ts:13:11
11 | }, [x.y.z.a]);
12 | const c = useMemo(() => {
> 13 | return x?.y.z.a?.b;
| ^^^^^^^^^^^ Missing dependency `x?.y.z.a?.b`
14 | }, [x?.y.z.a?.b.z]);
15 | const d = useMemo(() => {
16 | return x?.y?.[(console.log(y), z?.b)];
Compilation Skipped: 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. This memoization cannot be safely rewritten by the compiler.
error.invalid-exhaustive-deps.ts:23:20
21 | return e;
22 | }, [x]);
> 23 | const f = useMemo(() => {
| ^^^^^^^
> 24 | return [];
| ^^^^^^^^^^^^^^
> 25 | }, [x, y.z, z?.y?.a]);
| ^^^^ Unnecessary dependencies `x`, `y.z`, `z?.y?.a`
26 | return <Stringify results={[a, b, c, d, e, f]} />;
27 | }
28 |
```

View File

@@ -1,27 +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;
}, [x?.y.z?.a.b]);
const b = useMemo(() => {
return x.y.z?.a;
}, [x.y.z.a]);
const c = useMemo(() => {
return x?.y.z.a?.b;
}, [x?.y.z.a?.b.z]);
const d = useMemo(() => {
return x?.y?.[(console.log(y), z?.b)];
}, [x?.y, y, z?.b]);
const e = useMemo(() => {
const e = [];
e.push(x);
return e;
}, [x]);
const f = useMemo(() => {
return [];
}, [x, y.z, z?.y?.a]);
return <Stringify results={[a, b, c, d, e, f]} />;
}

View File

@@ -1,69 +0,0 @@
## Input
```javascript
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates a custom component wrapper
function CustomForm({onSubmit, children}: any) {
return <form onSubmit={onSubmit}>{children}</form>;
}
// Simulates react-hook-form's handleSubmit
function handleSubmit<T>(callback: (data: T) => void) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}
function Component() {
const ref = useRef<HTMLInputElement>(null);
const onSubmit = (data: any) => {
// This should error: passing function with ref access to custom component
// event handler, even though it would be safe on a native <form>
if (ref.current !== null) {
console.log(ref.current.value);
}
};
return (
<>
<input ref={ref} />
<CustomForm onSubmit={handleSubmit(onSubmit)}>
<button type="submit">Submit</button>
</CustomForm>
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Error
```
Found 1 error:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.ref-value-in-custom-component-event-handler-wrapper.ts:31:41
29 | <>
30 | <input ref={ref} />
> 31 | <CustomForm onSubmit={handleSubmit(onSubmit)}>
| ^^^^^^^^ Passing a ref to a function may read its value during render
32 | <button type="submit">Submit</button>
33 | </CustomForm>
34 | </>
```

View File

@@ -1,41 +0,0 @@
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates a custom component wrapper
function CustomForm({onSubmit, children}: any) {
return <form onSubmit={onSubmit}>{children}</form>;
}
// Simulates react-hook-form's handleSubmit
function handleSubmit<T>(callback: (data: T) => void) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}
function Component() {
const ref = useRef<HTMLInputElement>(null);
const onSubmit = (data: any) => {
// This should error: passing function with ref access to custom component
// event handler, even though it would be safe on a native <form>
if (ref.current !== null) {
console.log(ref.current.value);
}
};
return (
<>
<input ref={ref} />
<CustomForm onSubmit={handleSubmit(onSubmit)}>
<button type="submit">Submit</button>
</CustomForm>
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

View File

@@ -1,55 +0,0 @@
## Input
```javascript
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates a handler wrapper
function handleClick(value: any) {
return () => {
console.log(value);
};
}
function Component() {
const ref = useRef(null);
// This should still error: passing ref.current directly to a wrapper
// The ref value is accessed during render, not in the event handler
return (
<>
<input ref={ref} />
<button onClick={handleClick(ref.current)}>Click</button>
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Error
```
Found 1 error:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.ref-value-in-event-handler-wrapper.ts:19:35
17 | <>
18 | <input ref={ref} />
> 19 | <button onClick={handleClick(ref.current)}>Click</button>
| ^^^^^^^^^^^ Cannot access ref value during render
20 | </>
21 | );
22 | }
```

View File

@@ -1,27 +0,0 @@
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates a handler wrapper
function handleClick(value: any) {
return () => {
console.log(value);
};
}
function Component() {
const ref = useRef(null);
// This should still error: passing ref.current directly to a wrapper
// The ref value is accessed during render, not in the event handler
return (
<>
<input ref={ref} />
<button onClick={handleClick(ref.current)}>Click</button>
</>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

View File

@@ -1,224 +0,0 @@
## Input
```javascript
// @validateSourceLocations
import {useEffect, useCallback} from 'react';
function Component({prop1, prop2}) {
const x = prop1 + prop2;
const y = x * 2;
const arr = [x, y];
const obj = {x, y};
const [a, b] = arr;
const {x: c, y: d} = obj;
useEffect(() => {
if (a > 10) {
console.log(a);
}
}, [a]);
const foo = useCallback(() => {
return a + b;
}, [a, b]);
function bar() {
return (c + d) * 2;
}
console.log('Hello, world!');
return [y, foo, bar];
}
```
## Error
```
Found 13 errors:
Todo: Important source location missing in generated code
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:5:8
3 |
4 | function Component({prop1, prop2}) {
> 5 | const x = prop1 + prop2;
| ^^^^^^^^^^^^^^^^^
6 | const y = x * 2;
7 | const arr = [x, y];
8 | const obj = {x, y};
Todo: Important source location missing in generated code
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:6:8
4 | function Component({prop1, prop2}) {
5 | const x = prop1 + prop2;
> 6 | const y = x * 2;
| ^^^^^^^^^
7 | const arr = [x, y];
8 | const obj = {x, y};
9 | const [a, b] = arr;
Todo: Important source location missing in generated code
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:7:8
5 | const x = prop1 + prop2;
6 | const y = x * 2;
> 7 | const arr = [x, y];
| ^^^^^^^^^^^^
8 | const obj = {x, y};
9 | const [a, b] = arr;
10 | const {x: c, y: d} = obj;
Todo: Important source location missing in generated code
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:8:8
6 | const y = x * 2;
7 | const arr = [x, y];
> 8 | const obj = {x, y};
| ^^^^^^^^^^^^
9 | const [a, b] = arr;
10 | const {x: c, y: d} = obj;
11 |
Todo: Important source location missing in generated code
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:9:8
7 | const arr = [x, y];
8 | const obj = {x, y};
> 9 | const [a, b] = arr;
| ^^^^^^^^^^^^
10 | const {x: c, y: d} = obj;
11 |
12 | useEffect(() => {
Todo: Important source location missing in generated code
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:10:8
8 | const obj = {x, y};
9 | const [a, b] = arr;
> 10 | const {x: c, y: d} = obj;
| ^^^^^^^^^^^^^^^^^^
11 |
12 | useEffect(() => {
13 | if (a > 10) {
Todo: Important source location missing in generated code
Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:12:2
10 | const {x: c, y: d} = obj;
11 |
> 12 | useEffect(() => {
| ^^^^^^^^^^^^^^^^^
> 13 | if (a > 10) {
| ^^^^^^^^^^^^^^^^^
> 14 | console.log(a);
| ^^^^^^^^^^^^^^^^^
> 15 | }
| ^^^^^^^^^^^^^^^^^
> 16 | }, [a]);
| ^^^^^^^^^^^
17 |
18 | const foo = useCallback(() => {
19 | return a + b;
Todo: Important source location missing in generated code
Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:14:6
12 | useEffect(() => {
13 | if (a > 10) {
> 14 | console.log(a);
| ^^^^^^^^^^^^^^^
15 | }
16 | }, [a]);
17 |
Todo: Important source location missing in generated code
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:18:8
16 | }, [a]);
17 |
> 18 | const foo = useCallback(() => {
| ^^^^^^^^^^^^^^^^^^^^^^^^^
> 19 | return a + b;
| ^^^^^^^^^^^^^^^^^
> 20 | }, [a, b]);
| ^^^^^^^^^^^^^
21 |
22 | function bar() {
23 | return (c + d) * 2;
Todo: Important source location missing in generated code
Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:19:4
17 |
18 | const foo = useCallback(() => {
> 19 | return a + b;
| ^^^^^^^^^^^^^
20 | }, [a, b]);
21 |
22 | function bar() {
Todo: Important source location missing in generated code
Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:23:4
21 |
22 | function bar() {
> 23 | return (c + d) * 2;
| ^^^^^^^^^^^^^^^^^^^
24 | }
25 |
26 | console.log('Hello, world!');
Todo: Important source location missing in generated code
Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:26:2
24 | }
25 |
> 26 | console.log('Hello, world!');
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
27 |
28 | return [y, foo, bar];
29 | }
Todo: Important source location missing in generated code
Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
error.todo-missing-source-locations.ts:28:2
26 | console.log('Hello, world!');
27 |
> 28 | return [y, foo, bar];
| ^^^^^^^^^^^^^^^^^^^^^
29 | }
30 |
```

View File

@@ -1,29 +0,0 @@
// @validateSourceLocations
import {useEffect, useCallback} from 'react';
function Component({prop1, prop2}) {
const x = prop1 + prop2;
const y = x * 2;
const arr = [x, y];
const obj = {x, y};
const [a, b] = arr;
const {x: c, y: d} = obj;
useEffect(() => {
if (a > 10) {
console.log(a);
}
}, [a]);
const foo = useCallback(() => {
return a + b;
}, [a, b]);
function bar() {
return (c + d) * 2;
}
console.log('Hello, world!');
return [y, foo, bar];
}

View File

@@ -1,159 +0,0 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies
import {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 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);
return <Stringify results={[a, b, c, d, e, f]} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
import { 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 Component(t0) {
const $ = _c(7);
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);
let t1;
if (
$[0] !== a ||
$[1] !== b ||
$[2] !== c ||
$[3] !== d ||
$[4] !== e ||
$[5] !== f
) {
t1 = <Stringify results={[a, b, c, d, e, f]} />;
$[0] = a;
$[1] = b;
$[2] = c;
$[3] = d;
$[4] = e;
$[5] = f;
$[6] = t1;
} else {
t1 = $[6];
}
return t1;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,54 +0,0 @@
// @validateExhaustiveMemoizationDependencies
import {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 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);
return <Stringify results={[a, b, c, d, e, f]} />;
}

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,4 +1,4 @@
// @inferEffectDependencies @panicThreshold:"none"
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly
import {print} from 'shared-runtime';
import useEffectWrapper from 'useEffectWrapper';
import {AUTODEPS} from 'react';

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 @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,20 +0,0 @@
// @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}],
};

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,45 +0,0 @@
## Input
```javascript
export function useFormatRelativeTime(opts = {}) {
const {timeZone, minimal} = opts;
const format = useCallback(function formatWithUnit() {}, [minimal]);
// We previously recorded `{timeZone}` as capturing timeZone into the object,
// then assumed that dateTimeFormat() mutates that object,
// which in turn could mutate timeZone and the object it came from,
// which meanteans that the value `minimal` is derived from can change.
//
// The fix was to record a Capture from a maybefrozen value as an ImmutableCapture
// which doesn't propagate mutations
dateTimeFormat({timeZone});
return format;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
export function useFormatRelativeTime(t0) {
const $ = _c(1);
const opts = t0 === undefined ? {} : t0;
const { timeZone, minimal } = opts;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = function formatWithUnit() {};
$[0] = t1;
} else {
t1 = $[0];
}
const format = t1;
dateTimeFormat({ timeZone });
return format;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,13 +0,0 @@
export function useFormatRelativeTime(opts = {}) {
const {timeZone, minimal} = opts;
const format = useCallback(function formatWithUnit() {}, [minimal]);
// We previously recorded `{timeZone}` as capturing timeZone into the object,
// then assumed that dateTimeFormat() mutates that object,
// which in turn could mutate timeZone and the object it came from,
// which meanteans that the value `minimal` is derived from can change.
//
// The fix was to record a Capture from a maybefrozen value as an ImmutableCapture
// which doesn't propagate mutations
dateTimeFormat({timeZone});
return format;
}

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,10 +1,9 @@
// @inferEffectDependencies @outputMode:"lint" @panicThreshold:"none"
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel
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

@@ -1,91 +0,0 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
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;
}, []);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
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,33 +0,0 @@
// @validatePreserveExistingMemoizationGuarantees
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;
}, []);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};

View File

@@ -1,162 +0,0 @@
## Input
```javascript
// @flow @compilationMode:"infer"
'use strict';
function getWeekendDays(user) {
return [0, 6];
}
function getConfig(weekendDays) {
return [1, 5];
}
component Calendar(user, defaultFirstDay, currentDate, view) {
const weekendDays = getWeekendDays(user);
let firstDay = defaultFirstDay;
let daysToDisplay = 7;
if (view === 'week') {
let lastDay;
// this assignment produces invalid code
[firstDay, lastDay] = getConfig(weekendDays);
daysToDisplay = ((7 + lastDay - firstDay) % 7) + 1;
} else if (view === 'day') {
firstDay = currentDate.getDayOfWeek();
daysToDisplay = 1;
}
return [currentDate, firstDay, daysToDisplay];
}
export const FIXTURE_ENTRYPOINT = {
fn: Calendar,
params: [
{
user: {},
defaultFirstDay: 1,
currentDate: {getDayOfWeek: () => 3},
view: 'week',
},
],
sequentialRenders: [
{
user: {},
defaultFirstDay: 1,
currentDate: {getDayOfWeek: () => 3},
view: 'week',
},
{
user: {},
defaultFirstDay: 1,
currentDate: {getDayOfWeek: () => 3},
view: 'day',
},
],
};
```
## Code
```javascript
"use strict";
import { c as _c } from "react/compiler-runtime";
function getWeekendDays(user) {
return [0, 6];
}
function getConfig(weekendDays) {
return [1, 5];
}
function Calendar(t0) {
const $ = _c(12);
const { user, defaultFirstDay, currentDate, view } = t0;
let daysToDisplay;
let firstDay;
if (
$[0] !== currentDate ||
$[1] !== defaultFirstDay ||
$[2] !== user ||
$[3] !== view
) {
const weekendDays = getWeekendDays(user);
firstDay = defaultFirstDay;
daysToDisplay = 7;
if (view === "week") {
let lastDay;
[firstDay, lastDay] = getConfig(weekendDays);
daysToDisplay = ((7 + lastDay - firstDay) % 7) + 1;
} else {
if (view === "day") {
let t1;
if ($[6] !== currentDate) {
t1 = currentDate.getDayOfWeek();
$[6] = currentDate;
$[7] = t1;
} else {
t1 = $[7];
}
firstDay = t1;
daysToDisplay = 1;
}
}
$[0] = currentDate;
$[1] = defaultFirstDay;
$[2] = user;
$[3] = view;
$[4] = daysToDisplay;
$[5] = firstDay;
} else {
daysToDisplay = $[4];
firstDay = $[5];
}
let t1;
if ($[8] !== currentDate || $[9] !== daysToDisplay || $[10] !== firstDay) {
t1 = [currentDate, firstDay, daysToDisplay];
$[8] = currentDate;
$[9] = daysToDisplay;
$[10] = firstDay;
$[11] = t1;
} else {
t1 = $[11];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Calendar,
params: [
{
user: {},
defaultFirstDay: 1,
currentDate: { getDayOfWeek: () => 3 },
view: "week",
},
],
sequentialRenders: [
{
user: {},
defaultFirstDay: 1,
currentDate: { getDayOfWeek: () => 3 },
view: "week",
},
{
user: {},
defaultFirstDay: 1,
currentDate: { getDayOfWeek: () => 3 },
view: "day",
},
],
};
```
### Eval output
(kind: ok) [{"getDayOfWeek":"[[ function params=0 ]]"},1,5]
[{"getDayOfWeek":"[[ function params=0 ]]"},3,1]

View File

@@ -1,53 +0,0 @@
// @flow @compilationMode:"infer"
'use strict';
function getWeekendDays(user) {
return [0, 6];
}
function getConfig(weekendDays) {
return [1, 5];
}
component Calendar(user, defaultFirstDay, currentDate, view) {
const weekendDays = getWeekendDays(user);
let firstDay = defaultFirstDay;
let daysToDisplay = 7;
if (view === 'week') {
let lastDay;
// this assignment produces invalid code
[firstDay, lastDay] = getConfig(weekendDays);
daysToDisplay = ((7 + lastDay - firstDay) % 7) + 1;
} else if (view === 'day') {
firstDay = currentDate.getDayOfWeek();
daysToDisplay = 1;
}
return [currentDate, firstDay, daysToDisplay];
}
export const FIXTURE_ENTRYPOINT = {
fn: Calendar,
params: [
{
user: {},
defaultFirstDay: 1,
currentDate: {getDayOfWeek: () => 3},
view: 'week',
},
],
sequentialRenders: [
{
user: {},
defaultFirstDay: 1,
currentDate: {getDayOfWeek: () => 3},
view: 'week',
},
{
user: {},
defaultFirstDay: 1,
currentDate: {getDayOfWeek: () => 3},
view: 'day',
},
],
};

View File

@@ -1,60 +0,0 @@
## Input
```javascript
// @enableOptimizeForSSR
function Component() {
const [state, setState] = useState(0);
const ref = useRef(null);
const onChange = e => {
setState(e.target.value);
};
useEffect(() => {
log(ref.current.value);
});
return <input value={state} onChange={onChange} ref={ref} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @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;
}
```

View File

@@ -1,12 +0,0 @@
// @enableOptimizeForSSR
function Component() {
const [state, setState] = useState(0);
const ref = useRef(null);
const onChange = e => {
setState(e.target.value);
};
useEffect(() => {
log(ref.current.value);
});
return <input value={state} onChange={onChange} ref={ref} />;
}

View File

@@ -1,64 +0,0 @@
## Input
```javascript
// @enableOptimizeForSSR
function Component() {
const [state, setState] = useState(0);
const ref = useRef(null);
const onChange = e => {
// The known setState call allows us to infer this as an event handler
// and prune it
setState(e.target.value);
};
useEffect(() => {
log(ref.current.value);
});
return <CustomInput value={state} onChange={onChange} ref={ref} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @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 = <CustomInput value={state} onChange={onChange} ref={ref} />;
$[2] = state;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,14 +0,0 @@
// @enableOptimizeForSSR
function Component() {
const [state, setState] = useState(0);
const ref = useRef(null);
const onChange = e => {
// The known setState call allows us to infer this as an event handler
// and prune it
setState(e.target.value);
};
useEffect(() => {
log(ref.current.value);
});
return <CustomInput value={state} onChange={onChange} ref={ref} />;
}

View File

@@ -1,70 +0,0 @@
## Input
```javascript
// @enableOptimizeForSSR
function Component() {
const [, startTransition] = useTransition();
const [state, setState] = useState(0);
const ref = useRef(null);
const onChange = e => {
// The known startTransition call allows us to infer this as an event handler
// and prune it
startTransition(() => {
setState.call(null, e.target.value);
});
};
useEffect(() => {
log(ref.current.value);
});
return <CustomInput value={state} onChange={onChange} ref={ref} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR
function Component() {
const $ = _c(4);
const [, startTransition] = useTransition();
const [state, setState] = useState(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;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,17 +0,0 @@
// @enableOptimizeForSSR
function Component() {
const [, startTransition] = useTransition();
const [state, setState] = useState(0);
const ref = useRef(null);
const onChange = e => {
// The known startTransition call allows us to infer this as an event handler
// and prune it
startTransition(() => {
setState.call(null, e.target.value);
});
};
useEffect(() => {
log(ref.current.value);
});
return <CustomInput value={state} onChange={onChange} ref={ref} />;
}

View File

@@ -1,75 +0,0 @@
## Input
```javascript
// @enableOptimizeForSSR
import {useReducer} from 'react';
const initializer = x => x;
function Component() {
const [state, dispatch] = useReducer((_, next) => next, 0, initializer);
const ref = useRef(null);
const onChange = e => {
dispatch(e.target.value);
};
useEffect(() => {
log(ref.current.value);
});
return <input value={state} onChange={onChange} ref={ref} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableOptimizeForSSR
import { useReducer } from "react";
const initializer = (x) => {
return 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;
}
```

View File

@@ -1,17 +0,0 @@
// @enableOptimizeForSSR
import {useReducer} from 'react';
const initializer = x => x;
function Component() {
const [state, dispatch] = useReducer((_, next) => next, 0, initializer);
const ref = useRef(null);
const onChange = e => {
dispatch(e.target.value);
};
useEffect(() => {
log(ref.current.value);
});
return <input value={state} onChange={onChange} ref={ref} />;
}

View File

@@ -1,69 +0,0 @@
## Input
```javascript
// @enableOptimizeForSSR
import {useReducer} from 'react';
function Component() {
const [state, dispatch] = useReducer((_, next) => next, 0);
const ref = useRef(null);
const onChange = e => {
dispatch(e.target.value);
};
useEffect(() => {
log(ref.current.value);
});
return <input value={state} onChange={onChange} ref={ref} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @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;
}
```

View File

@@ -1,15 +0,0 @@
// @enableOptimizeForSSR
import {useReducer} from 'react';
function Component() {
const [state, dispatch] = useReducer((_, next) => next, 0);
const ref = useRef(null);
const onChange = e => {
dispatch(e.target.value);
};
useEffect(() => {
log(ref.current.value);
});
return <input value={state} onChange={onChange} ref={ref} />;
}

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

@@ -1,117 +0,0 @@
## Input
```javascript
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects @loggerTestOnly @compilationMode:"infer"
import {useState, useRef, useEffect} from 'react';
function Component({x, y}) {
const previousXRef = useRef(null);
const previousYRef = useRef(null);
const [data, setData] = useState(null);
useEffect(() => {
const previousX = previousXRef.current;
previousXRef.current = x;
const previousY = previousYRef.current;
previousYRef.current = y;
if (!areEqual(x, previousX) || !areEqual(y, previousY)) {
const data = load({x, y});
setData(data);
}
}, [x, y]);
return data;
}
function areEqual(a, b) {
return a === b;
}
function load({x, y}) {
return x * y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 0, y: 0}],
sequentialRenders: [
{x: 0, y: 0},
{x: 1, y: 0},
{x: 1, y: 1},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects @loggerTestOnly @compilationMode:"infer"
import { useState, useRef, useEffect } from "react";
function Component(t0) {
const $ = _c(4);
const { x, y } = t0;
const previousXRef = useRef(null);
const previousYRef = useRef(null);
const [data, setData] = useState(null);
let t1;
let t2;
if ($[0] !== x || $[1] !== y) {
t1 = () => {
const previousX = previousXRef.current;
previousXRef.current = x;
const previousY = previousYRef.current;
previousYRef.current = y;
if (!areEqual(x, previousX) || !areEqual(y, previousY)) {
const data_0 = load({ x, y });
setData(data_0);
}
};
t2 = [x, y];
$[0] = x;
$[1] = y;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
}
useEffect(t1, t2);
return data;
}
function areEqual(a, b) {
return a === b;
}
function load({ x, y }) {
return x * y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ x: 0, y: 0 }],
sequentialRenders: [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 1, y: 1 },
],
};
```
## Logs
```
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":163},"end":{"line":22,"column":1,"index":631},"filename":"valid-setState-in-useEffect-controlled-by-ref-value.ts"},"fnName":"Component","memoSlots":4,"memoBlocks":1,"memoValues":2,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
### Eval output
(kind: ok) 0
0
1

View File

@@ -1,40 +0,0 @@
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects @loggerTestOnly @compilationMode:"infer"
import {useState, useRef, useEffect} from 'react';
function Component({x, y}) {
const previousXRef = useRef(null);
const previousYRef = useRef(null);
const [data, setData] = useState(null);
useEffect(() => {
const previousX = previousXRef.current;
previousXRef.current = x;
const previousY = previousYRef.current;
previousYRef.current = y;
if (!areEqual(x, previousX) || !areEqual(y, previousY)) {
const data = load({x, y});
setData(data);
}
}, [x, y]);
return data;
}
function areEqual(a, b) {
return a === b;
}
function load({x, y}) {
return x * y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{x: 0, y: 0}],
sequentialRenders: [
{x: 0, y: 0},
{x: 1, y: 0},
{x: 1, y: 1},
],
};

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

@@ -487,13 +487,6 @@ const skipFilter = new Set([
'lower-context-selector-simple',
'lower-context-acess-multiple',
'bug-separate-memoization-due-to-callback-capturing',
// SSR optimization rewrites files in a way that causes differences or warnings
'ssr/optimize-ssr',
'ssr/ssr-use-reducer',
'ssr/ssr-use-reducer-initializer',
'ssr/infer-event-handlers-from-setState',
'ssr/infer-event-handlers-from-startTransition',
]);
export default skipFilter;

View File

@@ -44,21 +44,6 @@ function stripExtension(filename: string, extensions: Array<string>): string {
return filename;
}
/**
* Strip all extensions from a filename
* e.g., "foo.expect.md" -> "foo"
*/
function stripAllExtensions(filename: string): string {
let result = filename;
while (true) {
const extension = path.extname(result);
if (extension === '') {
return result;
}
result = path.basename(result, extension);
}
}
export async function readTestFilter(): Promise<TestFilter | null> {
if (!(await exists(FILTER_PATH))) {
throw new Error(`testfilter file not found at \`${FILTER_PATH}\``);
@@ -126,25 +111,11 @@ async function readInputFixtures(
} else {
inputFiles = (
await Promise.all(
filter.paths.map(pattern => {
// If the pattern already has an extension other than .expect.md,
// search for the pattern directly. Otherwise, search for the
// pattern with the expected input extensions added.
// Eg
// `alias-while` => search for `alias-while{.js,.jsx,.ts,.tsx}`
// `alias-while.js` => search as-is
// `alias-while.expect.md` => search for `alias-while{.js,.jsx,.ts,.tsx}`
const basename = path.basename(pattern);
const basenameWithoutExt = stripAllExtensions(basename);
const hasExtension = basename !== basenameWithoutExt;
const globPattern =
hasExtension && !pattern.endsWith(SNAPSHOT_EXTENSION)
? pattern
: `${basenameWithoutExt}{${INPUT_EXTENSIONS.join(',')}}`;
return glob.glob(globPattern, {
filter.paths.map(pattern =>
glob.glob(`${pattern}{${INPUT_EXTENSIONS.join(',')}}`, {
cwd: rootDir,
});
}),
}),
),
)
).flat();
}
@@ -179,13 +150,11 @@ async function readOutputFixtures(
} else {
outputFiles = (
await Promise.all(
filter.paths.map(pattern => {
// Strip all extensions and find matching .expect.md files
const basenameWithoutExt = stripAllExtensions(pattern);
return glob.glob(`${basenameWithoutExt}${SNAPSHOT_EXTENSION}`, {
filter.paths.map(pattern =>
glob.glob(`${pattern}${SNAPSHOT_EXTENSION}`, {
cwd: rootDir,
});
}),
}),
),
)
).flat();
}

View File

@@ -35,7 +35,6 @@ type RunnerOptions = {
watch: boolean;
filter: boolean;
update: boolean;
pattern?: string;
};
const opts: RunnerOptions = yargs
@@ -63,15 +62,9 @@ const opts: RunnerOptions = yargs
'Only run fixtures which match the contents of testfilter.txt',
)
.default('filter', false)
.string('pattern')
.alias('p', 'pattern')
.describe(
'pattern',
'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")',
)
.help('help')
.strict()
.parseSync(hideBin(process.argv)) as RunnerOptions;
.parseSync(hideBin(process.argv));
/**
* Do a test run and return the test results
@@ -178,13 +171,7 @@ export async function main(opts: RunnerOptions): Promise<void> {
worker.getStderr().pipe(process.stderr);
worker.getStdout().pipe(process.stdout);
// If pattern is provided, force watch mode off and use pattern filter
const shouldWatch = opts.watch && opts.pattern == null;
if (opts.watch && opts.pattern != null) {
console.warn('NOTE: --watch is ignored when a --pattern is supplied');
}
if (shouldWatch) {
if (opts.watch) {
makeWatchRunner(state => onChange(worker, state), opts.filter);
if (opts.filter) {
/**
@@ -229,18 +216,7 @@ export async function main(opts: RunnerOptions): Promise<void> {
try {
execSync('yarn build', {cwd: PROJECT_ROOT});
console.log('Built compiler successfully with tsup');
// Determine which filter to use
let testFilter: TestFilter | null = null;
if (opts.pattern) {
testFilter = {
debug: true,
paths: [opts.pattern],
};
} else if (opts.filter) {
testFilter = await readTestFilter();
}
const testFilter = opts.filter ? await readTestFilter() : null;
const results = await runFixtures(worker, testFilter, 0);
if (opts.update) {
update(results);

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

@@ -813,12 +813,6 @@ function createInitializedStreamChunk<
value: T,
controller: FlightStreamController,
): InitializedChunk<T> {
if (__DEV__) {
// Retain a strong reference to the Response while we wait for chunks.
if (response._pendingChunks++ === 0) {
response._weakResponse.response = response;
}
}
// We use the reason field to stash the controller since we already have that
// field. It's a bit of a hack but efficient.
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
@@ -3081,6 +3075,7 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
// We already resolved. We didn't expect to see this.
return;
}
releasePendingChunk(response, chunk);
const resolveListeners = chunk.value;
@@ -3380,14 +3375,6 @@ function stopStream(
// We didn't expect not to have an existing stream;
return;
}
if (__DEV__) {
if (--response._pendingChunks === 0) {
// We're no longer waiting for any more chunks. We can release the strong
// reference to the response. We'll regain it if we ask for any more data
// later on.
response._weakResponse.response = null;
}
}
const streamChunk: InitializedStreamChunk<any> = (chunk: any);
const controller = streamChunk.reason;
controller.close(row === '' ? '"$undefined"' : row);
@@ -4870,7 +4857,6 @@ export function processBinaryChunk(
resolvedRowTag === 65 /* "A" */ ||
resolvedRowTag === 79 /* "O" */ ||
resolvedRowTag === 111 /* "o" */ ||
resolvedRowTag === 98 /* "b" */ ||
resolvedRowTag === 85 /* "U" */ ||
resolvedRowTag === 83 /* "S" */ ||
resolvedRowTag === 115 /* "s" */ ||
@@ -4930,31 +4916,14 @@ export function processBinaryChunk(
// We found the last chunk of the row
const length = lastIdx - i;
const lastChunk = new Uint8Array(chunk.buffer, offset, length);
// Check if this is a Uint8Array for a byte stream. We enqueue it
// immediately but need to determine if we can use zero-copy or must copy.
if (rowTag === 98 /* "b" */) {
resolveBuffer(
response,
rowID,
// If we're at the end of the RSC chunk, no more parsing will access
// this buffer and we don't need to copy the chunk to allow detaching
// the buffer, otherwise we need to copy.
lastIdx === chunkLength ? lastChunk : lastChunk.slice(),
streamState,
);
} else {
// Process all other row types.
processFullBinaryRow(
response,
streamState,
rowID,
rowTag,
buffer,
lastChunk,
);
}
processFullBinaryRow(
response,
streamState,
rowID,
rowTag,
buffer,
lastChunk,
);
// Reset state machine for a new row
i = lastIdx;
if (rowState === ROW_CHUNK_BY_NEWLINE) {
@@ -4967,27 +4936,14 @@ export function processBinaryChunk(
rowLength = 0;
buffer.length = 0;
} else {
// The rest of this row is in a future chunk.
// The rest of this row is in a future chunk. We stash the rest of the
// current chunk until we can process the full row.
const length = chunk.byteLength - i;
const remainingSlice = new Uint8Array(chunk.buffer, offset, length);
// For byte streams, we can enqueue the partial row immediately without
// copying since we're at the end of the RSC chunk and no more parsing
// will access this buffer.
if (rowTag === 98 /* "b" */) {
// Update how many bytes we're still waiting for. We need to do this
// before enqueueing, as enqueue will detach the buffer and byteLength
// will become 0.
rowLength -= remainingSlice.byteLength;
resolveBuffer(response, rowID, remainingSlice, streamState);
} else {
// For other row types, stash the rest of the current chunk until we can
// process the full row.
buffer.push(remainingSlice);
// Update how many bytes we're still waiting for. If we're looking for
// a newline, this doesn't hurt since we'll just ignore it.
rowLength -= remainingSlice.byteLength;
}
buffer.push(remainingSlice);
// Update how many bytes we're still waiting for. If we're looking for
// a newline, this doesn't hurt since we'll just ignore it.
rowLength -= remainingSlice.byteLength;
break;
}
}

View File

@@ -3884,19 +3884,4 @@ describe('ReactFlight', () => {
</main>,
);
});
// @gate enableOptimisticKey
it('collapses optimistic keys to an optimistic key', async () => {
function Bar({text}) {
return <div />;
}
function Foo() {
return <Bar key={ReactServer.optimisticKey} />;
}
const transport = ReactNoopFlightServer.render({
element: <Foo key="Outer Key" />,
});
const model = await ReactNoopFlightClient.read(transport);
expect(model.element.key).toBe(React.optimisticKey);
});
});

View File

@@ -120,7 +120,6 @@ import {
MEMO_SYMBOL_STRING,
SERVER_CONTEXT_SYMBOL_STRING,
LAZY_SYMBOL_STRING,
REACT_OPTIMISTIC_KEY,
} from '../shared/ReactSymbols';
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
@@ -1577,6 +1576,7 @@ export function attach(
currentRoot = rootInstance;
unmountInstanceRecursively(rootInstance);
rootToFiberInstanceMap.delete(root);
flushPendingEvents();
currentRoot = (null: any);
});
@@ -1646,6 +1646,7 @@ export function attach(
currentRoot = newRoot;
setRootPseudoKey(currentRoot.id, root.current);
mountFiberRecursively(root.current, false);
flushPendingEvents();
currentRoot = (null: any);
});
@@ -2158,6 +2159,7 @@ export function attach(
let pendingOperationsQueue: Array<OperationsArray> | null = [];
const pendingStringTable: Map<string, StringTableEntry> = new Map();
let pendingStringTableLength: number = 0;
let pendingUnmountedRootID: FiberInstance['id'] | null = null;
function pushOperation(op: number): void {
if (__DEV__) {
@@ -2185,7 +2187,8 @@ export function attach(
pendingOperations.length === 0 &&
pendingRealUnmountedIDs.length === 0 &&
pendingRealUnmountedSuspenseIDs.length === 0 &&
pendingSuspenderChanges.size === 0
pendingSuspenderChanges.size === 0 &&
pendingUnmountedRootID === null
);
}
@@ -2247,7 +2250,9 @@ export function attach(
return;
}
const numUnmountIDs = pendingRealUnmountedIDs.length;
const numUnmountIDs =
pendingRealUnmountedIDs.length +
(pendingUnmountedRootID === null ? 0 : 1);
const numUnmountSuspenseIDs = pendingRealUnmountedSuspenseIDs.length;
const numSuspenderChanges = pendingSuspenderChanges.size;
@@ -2325,6 +2330,11 @@ export function attach(
for (let j = 0; j < pendingRealUnmountedIDs.length; j++) {
operations[i++] = pendingRealUnmountedIDs[j];
}
// The root ID should always be unmounted last.
if (pendingUnmountedRootID !== null) {
operations[i] = pendingUnmountedRootID;
i++;
}
}
// Fill in pending operations.
@@ -2372,6 +2382,7 @@ export function attach(
pendingRealUnmountedIDs.length = 0;
pendingRealUnmountedSuspenseIDs.length = 0;
pendingSuspenderChanges.clear();
pendingUnmountedRootID = null;
pendingStringTable.clear();
pendingStringTableLength = 0;
}
@@ -2857,6 +2868,7 @@ export function attach(
// Already disconnected.
return;
}
const fiber = fiberInstance.data;
if (trackedPathMatchInstance === fiberInstance) {
// We're in the process of trying to restore previous selection.
@@ -2866,7 +2878,17 @@ export function attach(
}
const id = fiberInstance.id;
pendingRealUnmountedIDs.push(id);
const isRoot = fiber.tag === HostRoot;
if (isRoot) {
// Roots must be removed only after all children have been removed.
// So we track it separately.
pendingUnmountedRootID = id;
} else {
// To maintain child-first ordering,
// we'll push it into one of these queues,
// and later arrange them in the correct order.
pendingRealUnmountedIDs.push(id);
}
}
function recordSuspenseResize(suspenseNode: SuspenseNode): void {
@@ -4850,10 +4872,7 @@ export function attach(
}
let previousSiblingOfBestMatch = null;
let bestMatch = remainingReconcilingChildren;
if (
componentInfo.key != null &&
componentInfo.key !== REACT_OPTIMISTIC_KEY
) {
if (componentInfo.key != null) {
// If there is a key try to find a matching key in the set.
bestMatch = remainingReconcilingChildren;
while (bestMatch !== null) {
@@ -5753,12 +5772,11 @@ export function attach(
mountFiberRecursively(root.current, false);
flushPendingEvents();
needsToFlushComponentLogs = false;
currentRoot = (null: any);
});
flushPendingEvents();
needsToFlushComponentLogs = false;
}
}
@@ -6149,7 +6167,7 @@ export function attach(
return {
displayName: getDisplayNameForFiber(fiber) || 'Anonymous',
id: instance.id,
key: fiber.key === REACT_OPTIMISTIC_KEY ? null : fiber.key,
key: fiber.key,
env: null,
stack:
fiber._debugOwner == null || fiber._debugStack == null
@@ -6162,11 +6180,7 @@ export function attach(
return {
displayName: componentInfo.name || 'Anonymous',
id: instance.id,
key:
componentInfo.key == null ||
componentInfo.key === REACT_OPTIMISTIC_KEY
? null
: componentInfo.key,
key: componentInfo.key == null ? null : componentInfo.key,
env: componentInfo.env == null ? null : componentInfo.env,
stack:
componentInfo.owner == null || componentInfo.debugStack == null
@@ -7090,7 +7104,7 @@ export function attach(
// Does the component have legacy context attached to it.
hasLegacyContext,
key: key != null && key !== REACT_OPTIMISTIC_KEY ? key : null,
key: key != null ? key : null,
type: elementType,
@@ -8649,7 +8663,7 @@ export function attach(
}
return {
displayName,
key: key === REACT_OPTIMISTIC_KEY ? null : key,
key,
index,
};
}
@@ -8657,11 +8671,7 @@ export function attach(
function getVirtualPathFrame(virtualInstance: VirtualInstance): PathFrame {
return {
displayName: virtualInstance.data.name || '',
key:
virtualInstance.data.key == null ||
virtualInstance.data.key === REACT_OPTIMISTIC_KEY
? null
: virtualInstance.data.key,
key: virtualInstance.data.key == null ? null : virtualInstance.data.key,
index: -1, // We use -1 to indicate that this is a virtual path frame.
};
}

View File

@@ -72,9 +72,3 @@ export const SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED_SYMBOL_STRING =
export const REACT_MEMO_CACHE_SENTINEL: symbol = Symbol.for(
'react.memo_cache_sentinel',
);
import type {ReactOptimisticKey} from 'shared/ReactTypes';
export const REACT_OPTIMISTIC_KEY: ReactOptimisticKey = (Symbol.for(
'react.optimistic_key',
): any);

View File

@@ -189,8 +189,6 @@ export default class Store extends EventEmitter<{
{errorCount: number, warningCount: number},
> = new Map();
_focusedTransition: 0 | Element['id'] = 0;
// At least one of the injected renderers contains (DEV only) owner metadata.
_hasOwnerMetadata: boolean = false;
@@ -937,9 +935,10 @@ export default class Store extends EventEmitter<{
}
/**
* @param rootID
* @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
*/
getSuspendableDocumentOrderSuspenseInitialPaint(
getSuspendableDocumentOrderSuspense(
uniqueSuspendersOnly: boolean,
): Array<SuspenseTimelineStep> {
const target: Array<SuspenseTimelineStep> = [];
@@ -991,76 +990,6 @@ export default class Store extends EventEmitter<{
return target;
}
_pushSuspenseChildrenInDocumentOrder(
children: Array<Element['id']>,
target: Array<SuspenseNode['id']>,
): void {
for (let i = 0; i < children.length; i++) {
const childID = children[i];
const suspense = this.getSuspenseByID(childID);
if (suspense !== null) {
target.push(suspense.id);
} else {
const childElement = this.getElementByID(childID);
if (childElement !== null) {
this._pushSuspenseChildrenInDocumentOrder(
childElement.children,
target,
);
}
}
}
}
getSuspenseChildren(id: Element['id']): Array<SuspenseNode['id']> {
const transitionChildren: Array<SuspenseNode['id']> = [];
const root = this._idToElement.get(id);
if (root === undefined) {
return transitionChildren;
}
this._pushSuspenseChildrenInDocumentOrder(
root.children,
transitionChildren,
);
return transitionChildren;
}
/**
* @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
*/
getSuspendableDocumentOrderSuspenseTransition(
uniqueSuspendersOnly: boolean,
): Array<SuspenseTimelineStep> {
const target: Array<SuspenseTimelineStep> = [];
const focusedTransitionID = this._focusedTransition;
if (focusedTransitionID === null) {
return target;
}
target.push({
id: focusedTransitionID,
// TODO: Get environment for Activity
environment: null,
endTime: 0,
});
const transitionChildren = this.getSuspenseChildren(focusedTransitionID);
this.pushTimelineStepsInDocumentOrder(
transitionChildren,
target,
uniqueSuspendersOnly,
// TODO: Get environment for Activity
[],
0, // Don't pass a minimum end time at the root. The root is always first so doesn't matter.
);
return target;
}
pushTimelineStepsInDocumentOrder(
children: Array<SuspenseNode['id']>,
target: Array<SuspenseTimelineStep>,
@@ -1116,14 +1045,7 @@ export default class Store extends EventEmitter<{
uniqueSuspendersOnly: boolean,
): $ReadOnlyArray<SuspenseTimelineStep> {
const timeline =
this._focusedTransition === 0
? this.getSuspendableDocumentOrderSuspenseInitialPaint(
uniqueSuspendersOnly,
)
: this.getSuspendableDocumentOrderSuspenseTransition(
uniqueSuspendersOnly,
);
this.getSuspendableDocumentOrderSuspense(uniqueSuspendersOnly);
if (timeline.length === 0) {
return timeline;
}
@@ -1136,33 +1058,6 @@ export default class Store extends EventEmitter<{
return timeline;
}
getActivities(): Array<{id: Element['id'], depth: number}> {
const target: Array<{id: Element['id'], depth: number}> = [];
// TODO: Keep a live tree in the backend so we don't need to recalculate
// this each time while also including filtered Activities.
this._pushActivitiesInDocumentOrder(this.roots, target, 0);
return target;
}
_pushActivitiesInDocumentOrder(
children: $ReadOnlyArray<Element['id']>,
target: Array<{id: Element['id'], depth: number}>,
depth: number,
): void {
for (let i = 0; i < children.length; i++) {
const child = this._idToElement.get(children[i]);
if (child === undefined) {
continue;
}
if (child.type === ElementTypeActivity && child.nameProp !== null) {
target.push({id: child.id, depth});
this._pushActivitiesInDocumentOrder(child.children, target, depth + 1);
} else {
this._pushActivitiesInDocumentOrder(child.children, target, depth);
}
}
}
getRendererIDForElement(id: number): number | null {
let current = this._idToElement.get(id);
while (current !== undefined) {
@@ -1349,7 +1244,7 @@ export default class Store extends EventEmitter<{
const removedElementIDs: Map<number, number> = new Map();
const removedSuspenseIDs: Map<SuspenseNode['id'], SuspenseNode['id']> =
new Map();
let nextActivitySliceID: Element['id'] | null = null;
let nextActivitySliceID = null;
let i = 2;
@@ -2224,10 +2119,6 @@ export default class Store extends EventEmitter<{
}
}
if (nextActivitySliceID !== null) {
this._focusedTransition = nextActivitySliceID;
}
this.emit('mutated', [
addedElementIDs,
removedElementIDs,

View File

@@ -59,7 +59,6 @@ export type StateContext = {
// Activity slice
activityID: Element['id'] | null,
activities: $ReadOnlyArray<{id: Element['id'], depth: number}>,
// Inspection element panel
inspectedElementID: number | null,
@@ -173,7 +172,6 @@ type State = {
// Activity slice
activityID: Element['id'] | null,
activities: $ReadOnlyArray<{id: Element['id'], depth: number}>,
// Inspection element panel
inspectedElementID: number | null,
@@ -811,7 +809,6 @@ function reduceActivityState(
case 'HANDLE_STORE_MUTATION':
let {activityID} = state;
const [, , activitySliceIDChange] = action.payload;
const activities = store.getActivities();
if (activitySliceIDChange === 0 && activityID !== null) {
activityID = null;
} else if (
@@ -820,11 +817,10 @@ function reduceActivityState(
) {
activityID = activitySliceIDChange;
}
if (activityID !== state.activityID || activities !== state.activities) {
if (activityID !== state.activityID) {
return {
...state,
activityID,
activities,
};
}
}
@@ -867,7 +863,6 @@ function getInitialState({
// Activity slice
activityID: null,
activities: store.getActivities(),
// Inspection element panel
inspectedElementID:

View File

@@ -1,33 +1,20 @@
.ActivityListContaier {
display: flex;
flex-direction: column;
}
.ActivityListHeader {
/* even if empty, provides layout alignment with the main view */
display: flex;
flex: 0 0 42px;
border-bottom: 1px solid var(--color-border);
}
.ActivityListList {
.ActivityList {
cursor: default;
list-style-type: none;
margin: 0;
padding: 0;
}
.ActivityListList[data-pending-activity-slice-selection="true"] {
.ActivityList[data-pending-activity-slice-selection="true"] {
cursor: wait;
}
.ActivityListList:focus {
.ActivityList:focus {
outline: none;
}
.ActivityListItem {
color: var(--color-component-name);
line-height: var(--line-height-data);
padding: 0 0.25rem;
user-select: none;
}
@@ -40,7 +27,7 @@
background-color: var(--color-background-inactive);
}
.ActivityListList:focus .ActivityListItem[aria-selected="true"] {
.ActivityList:focus .ActivityListItem[aria-selected="true"] {
background-color: var(--color-background-selected);
color: var(--color-text-selected);

View File

@@ -15,14 +15,10 @@ import typeof {
SyntheticMouseEvent,
SyntheticKeyboardEvent,
} from 'react-dom-bindings/src/events/SyntheticEvent';
import type Store from 'react-devtools-shared/src/devtools/store';
import * as React from 'react';
import {useContext, useMemo, useTransition} from 'react';
import {
ComponentFilterActivitySlice,
ElementTypeActivity,
} from 'react-devtools-shared/src/frontend/types';
import {useContext, useTransition} from 'react';
import {ComponentFilterActivitySlice} from 'react-devtools-shared/src/frontend/types';
import styles from './ActivityList.css';
import {
TreeStateContext,
@@ -30,8 +26,6 @@ import {
} from '../Components/TreeContext';
import {useHighlightHostInstance} from '../hooks';
import {StoreContext} from '../context';
import ButtonIcon from '../ButtonIcon';
import Button from '../Button';
export function useChangeActivitySliceAction(): (
id: Element['id'] | null,
@@ -68,49 +62,15 @@ export function useChangeActivitySliceAction(): (
return changeActivitySliceAction;
}
function findNearestActivityParentID(
elementID: Element['id'],
store: Store,
): Element['id'] | null {
let currentID: null | Element['id'] = elementID;
while (currentID !== null) {
const element = store.getElementByID(currentID);
if (element === null) {
return null;
}
if (element.type === ElementTypeActivity) {
return element.id;
}
currentID = element.parentID;
}
return currentID;
}
function useSelectedActivityID(): Element['id'] | null {
const {inspectedElementID} = useContext(TreeStateContext);
const store = useContext(StoreContext);
return useMemo(() => {
if (inspectedElementID === null) {
return null;
}
const nearestActivityID = findNearestActivityParentID(
inspectedElementID,
store,
);
return nearestActivityID;
}, [inspectedElementID, store]);
}
export default function ActivityList({
activities,
}: {
activities: $ReadOnlyArray<{id: Element['id'], depth: number}>,
activities: $ReadOnlyArray<Element>,
}): React$Node {
const {activityID, inspectedElementID} = useContext(TreeStateContext);
const {inspectedElementID} = useContext(TreeStateContext);
const treeDispatch = useContext(TreeDispatcherContext);
const store = useContext(StoreContext);
const selectedActivityID = useSelectedActivityID();
// TODO: Derive from inspected element
const selectedActivityID = inspectedElementID;
const {highlightHostInstance, clearHighlightHostInstance} =
useHighlightHostInstance();
@@ -119,13 +79,8 @@ export default function ActivityList({
const changeActivitySliceAction = useChangeActivitySliceAction();
function handleKeyDown(event: SyntheticKeyboardEvent) {
// TODO: Implement keyboard navigation
switch (event.key) {
case 'Escape':
startActivitySliceSelection(() => {
changeActivitySliceAction(null);
});
event.preventDefault();
break;
case 'Enter':
case ' ':
if (inspectedElementID !== null) {
@@ -194,61 +149,25 @@ export default function ActivityList({
}
return (
<div className={styles.ActivityListContaier}>
<div className={styles.ActivityListHeader}>
{activityID !== null && (
// TODO: Obsolete once filtered Activities are included in this list.
<Button
onClick={startActivitySliceSelection.bind(
null,
changeActivitySliceAction.bind(null, null),
)}
title="Back to full tree view">
<ButtonIcon type="previous" />
</Button>
)}
</div>
<ol
role="listbox"
className={styles.ActivityListList}
data-pending-activity-slice-selection={isPendingActivitySliceSelection}
tabIndex={0}
onKeyDown={handleKeyDown}>
{activities.map(({id, depth}) => {
const activity = store.getElementByID(id);
if (activity === null) {
return null;
}
const name = activity.nameProp;
if (name === null) {
// This shouldn't actually happen. We only want to show activities with a name.
// And hide the whole list if no named Activities are present.
return null;
}
// TODO: Filtered Activities should have dedicated styles once we include
// filtered Activities in this list.
return (
<li
key={activity.id}
role="option"
aria-selected={
activity.id === selectedActivityID ? 'true' : 'false'
}
className={styles.ActivityListItem}
onClick={handleClick.bind(null, activity.id)}
onDoubleClick={handleDoubleClick}
onPointerOver={highlightHostInstance.bind(
null,
activity.id,
false,
)}
onPointerLeave={clearHighlightHostInstance}>
{'\u00A0'.repeat(depth) + name}
</li>
);
})}
</ol>
</div>
<ol
role="listbox"
className={styles.ActivityList}
data-pending-activity-slice-selection={isPendingActivitySliceSelection}
tabIndex={0}
onKeyDown={handleKeyDown}>
{activities.map(activity => (
<li
key={activity.id}
role="option"
aria-selected={activity.id === selectedActivityID ? 'true' : 'false'}
className={styles.ActivityListItem}
onClick={handleClick.bind(null, activity.id)}
onDoubleClick={handleDoubleClick}
onPointerOver={highlightHostInstance.bind(null, activity.id, false)}
onPointerLeave={clearHighlightHostInstance}>
{activity.nameProp}
</li>
))}
</ol>
);
}

View File

@@ -12,10 +12,7 @@ import typeof {SyntheticMouseEvent} from 'react-dom-bindings/src/events/Syntheti
import * as React from 'react';
import {useContext} from 'react';
import {
TreeDispatcherContext,
TreeStateContext,
} from '../Components/TreeContext';
import {TreeDispatcherContext} from '../Components/TreeContext';
import {StoreContext} from '../context';
import {useHighlightHostInstance} from '../hooks';
import styles from './SuspenseBreadcrumbs.css';
@@ -26,7 +23,6 @@ import {
export default function SuspenseBreadcrumbs(): React$Node {
const store = useContext(StoreContext);
const {activityID} = useContext(TreeStateContext);
const treeDispatch = useContext(TreeDispatcherContext);
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
const {selectedSuspenseID, lineage, roots} = useContext(
@@ -46,8 +42,8 @@ export default function SuspenseBreadcrumbs(): React$Node {
<ol className={styles.SuspenseBreadcrumbsList}>
{lineage === null ? null : lineage.length === 0 ? (
// We selected the root. This means that we're currently viewing the Transition
// that rendered the whole screen. In laymans terms this is really "Initial Paint" .
// When we're looking at a subtree selection, then the equivalent is a
// that rendered the whole screen. In laymans terms this is really "Initial Paint".
// TODO: Once we add subtree selection, then the equivalent should be called
// "Transition" since in that case it's really about a Transition within the page.
roots.length > 0 ? (
<li
@@ -55,12 +51,9 @@ export default function SuspenseBreadcrumbs(): React$Node {
aria-current="true">
<button
className={styles.SuspenseBreadcrumbsButton}
onClick={handleClick.bind(
null,
activityID === null ? roots[0] : activityID,
)}
onClick={handleClick.bind(null, roots[0])}
type="button">
{activityID === null ? 'Initial Paint' : 'Transition'}
Initial Paint
</button>
</li>
) : null

View File

@@ -9,7 +9,6 @@
import type Store from 'react-devtools-shared/src/devtools/store';
import type {
Element,
SuspenseNode,
Rect,
} from 'react-devtools-shared/src/frontend/types';
@@ -19,7 +18,7 @@ import typeof {
} from 'react-dom-bindings/src/events/SyntheticEvent';
import * as React from 'react';
import {createContext, useContext, useLayoutEffect, useMemo} from 'react';
import {createContext, useContext, useLayoutEffect} from 'react';
import {
TreeDispatcherContext,
TreeStateContext,
@@ -427,30 +426,6 @@ function SuspenseRectsRoot({rootID}: {rootID: SuspenseNode['id']}): React$Node {
});
}
function SuspenseRectsInitialPaint(): React$Node {
const {roots} = useContext(SuspenseTreeStateContext);
return roots.map(rootID => {
return <SuspenseRectsRoot key={rootID} rootID={rootID} />;
});
}
function SuspenseRectsTransition({id}: {id: Element['id']}): React$Node {
const store = useContext(StoreContext);
const children = useMemo(() => {
return store.getSuspenseChildren(id);
}, [id, store]);
return children.map(suspenseID => {
return (
<SuspenseRects
key={suspenseID}
suspenseID={suspenseID}
parentRects={null}
/>
);
});
}
const ViewBox = createContext<Rect>((null: any));
function SuspenseRectsContainer({
@@ -459,25 +434,14 @@ function SuspenseRectsContainer({
scaleRef: {current: number},
}): React$Node {
const store = useContext(StoreContext);
const {activityID, inspectedElementID} = useContext(TreeStateContext);
const {inspectedElementID} = useContext(TreeStateContext);
const treeDispatch = useContext(TreeDispatcherContext);
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
const {roots, timeline, hoveredTimelineIndex, uniqueSuspendersOnly} =
useContext(SuspenseTreeStateContext);
const activityChildren: $ReadOnlyArray<SuspenseNode['id']> | null =
useMemo(() => {
if (activityID === null) {
return null;
}
return store.getSuspenseChildren(activityID);
}, [activityID, store]);
const transitionChildren =
activityChildren === null ? roots : activityChildren;
// We're using the bounding box of the entire document to anchor the Transition
// in the actual document.
// TODO: bbox does not consider uniqueSuspendersOnly filter
const boundingBox = getDocumentBoundingRect(store, roots);
const boundingBoxWidth = boundingBox.width;
@@ -492,18 +456,14 @@ function SuspenseRectsContainer({
// Already clicked on an inner rect
return;
}
if (transitionChildren.length === 0) {
if (roots.length === 0) {
// Nothing to select
return;
}
const arbitraryRootID = roots[0];
const transitionRoot = activityID === null ? arbitraryRootID : activityID;
event.preventDefault();
treeDispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: transitionRoot,
});
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: arbitraryRootID});
suspenseTreeDispatch({
type: 'SET_SUSPENSE_LINEAGE',
payload: arbitraryRootID,
@@ -523,8 +483,7 @@ function SuspenseRectsContainer({
}
const isRootSelected = roots.includes(inspectedElementID);
// When we're focusing a Transition, the first timeline step will not be a root.
const isRootHovered = activityID === null && hoveredTimelineIndex === 0;
const isRootHovered = hoveredTimelineIndex === 0;
let hasRootSuspenders = false;
if (!uniqueSuspendersOnly) {
@@ -577,13 +536,7 @@ function SuspenseRectsContainer({
<div
className={
styles.SuspenseRectsContainer +
(hasRootSuspenders &&
// We don't want to draw attention to the root if we're looking at a Transition.
// TODO: Draw bounding rect of Transition and check if the Transition
// has unique suspenders.
activityID === null
? ' ' + styles.SuspenseRectsRoot
: '') +
(hasRootSuspenders ? ' ' + styles.SuspenseRectsRoot : '') +
(isRootSelected ? ' ' + styles.SuspenseRectsRootOutline : '') +
' ' +
getClassNameForEnvironment(rootEnvironment)
@@ -595,11 +548,9 @@ function SuspenseRectsContainer({
<div
className={styles.SuspenseRectsViewBox}
style={{aspectRatio, width}}>
{activityID === null ? (
<SuspenseRectsInitialPaint />
) : (
<SuspenseRectsTransition id={activityID} />
)}
{roots.map(rootID => {
return <SuspenseRectsRoot key={rootID} rootID={rootID} />;
})}
{selectedBoundingBox !== null ? (
<ScaledRect
className={

View File

@@ -12,15 +12,13 @@ import type {SuspenseTimelineStep} from 'react-devtools-shared/src/frontend/type
import typeof {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
import * as React from 'react';
import {useContext, useRef} from 'react';
import {ElementTypeRoot} from 'react-devtools-shared/src/frontend/types';
import {useRef} from 'react';
import styles from './SuspenseScrubber.css';
import {getClassNameForEnvironment} from './SuspenseEnvironmentColors.js';
import Tooltip from '../Components/reach-ui/tooltip';
import {StoreContext} from '../context';
export default function SuspenseScrubber({
min,
@@ -45,7 +43,6 @@ export default function SuspenseScrubber({
onHoverSegment: (index: number) => void,
onHoverLeave: () => void,
}): React$Node {
const store = useContext(StoreContext);
const inputRef = useRef();
function handleChange(event: SyntheticEvent) {
const newValue = +event.currentTarget.value;
@@ -63,16 +60,12 @@ export default function SuspenseScrubber({
}
const steps = [];
for (let index = min; index <= max; index++) {
const step = timeline[index];
const environment = step.environment;
const element = store.getElementByID(step.id);
const environment = timeline[index].environment;
const label =
index === min
? // The first step in the timeline is always a Transition (Initial Paint).
element === null || element.type === ElementTypeRoot
? 'Initial Paint'
: 'Transition' +
(environment === null ? '' : ' (' + environment + ')')
'Initial Paint' +
(environment === null ? '' : ' (' + environment + ')')
: // TODO: Consider adding the name of this specific boundary if this step has only one.
environment === null
? 'Suspense'

View File

@@ -92,7 +92,7 @@
}
.ActivityList {
flex: 0 0 var(--horizontal-resize-activity-list-percentage);;
flex: 0 0 var(--horizontal-resize-tree-list-percentage);
border-right: 1px solid var(--color-border);
overflow: auto;
}

View File

@@ -6,11 +6,14 @@
*
* @flow
*/
import type {Element} from 'react-devtools-shared/src/frontend/types';
import * as React from 'react';
import {
useContext,
useEffect,
useLayoutEffect,
useMemo,
useReducer,
useRef,
Fragment,
@@ -41,13 +44,12 @@ import typeof {SyntheticPointerEvent} from 'react-dom-bindings/src/events/Synthe
import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
import {TreeStateContext} from '../Components/TreeContext';
type Orientation = 'horizontal' | 'vertical';
type LayoutActionType =
| 'ACTION_SET_ACTIVITY_LIST_TOGGLE'
| 'ACTION_SET_ACTIVITY_LIST_HORIZONTAL_FRACTION'
| 'ACTION_SET_TREE_LIST_TOGGLE'
| 'ACTION_SET_TREE_LIST_HORIZONTAL_FRACTION'
| 'ACTION_SET_INSPECTED_ELEMENT_TOGGLE'
| 'ACTION_SET_INSPECTED_ELEMENT_HORIZONTAL_FRACTION'
| 'ACTION_SET_INSPECTED_ELEMENT_VERTICAL_FRACTION';
@@ -57,8 +59,8 @@ type LayoutAction = {
};
type LayoutState = {
activityListHidden: boolean,
activityListHorizontalFraction: number,
treeListHidden: boolean,
treeListHorizontalFraction: number,
inspectedElementHidden: boolean,
inspectedElementHorizontalFraction: number,
inspectedElementVerticalFraction: number,
@@ -95,7 +97,7 @@ function ToggleUniqueSuspenders() {
);
}
function ToggleActivityList({
function ToggleTreeList({
dispatch,
state,
}: {
@@ -106,15 +108,13 @@ function ToggleActivityList({
<Button
onClick={() =>
dispatch({
type: 'ACTION_SET_ACTIVITY_LIST_TOGGLE',
type: 'ACTION_SET_TREE_LIST_TOGGLE',
payload: null,
})
}
title={
state.activityListHidden ? 'Show Activity List' : 'Hide Activity List'
}>
title={state.treeListHidden ? 'Show Tree List' : 'Hide Tree List'}>
<ButtonIcon
type={state.activityListHidden ? 'panel-left-open' : 'panel-left-close'}
type={state.treeListHidden ? 'panel-left-open' : 'panel-left-close'}
/>
</Button>
);
@@ -272,6 +272,17 @@ function SynchronizedScrollContainer({
);
}
// TODO: Get this from the store directly.
// The backend needs to keep a separate tree so that resuspending keeps Activity around.
function useActivities(): $ReadOnlyArray<Element> {
const activities = useMemo(() => {
const items: Array<Element> = [];
return items;
}, []);
return activities;
}
function SuspenseTab(_: {}) {
const store = useContext(StoreContext);
const {hideSettings} = useContext(OptionsContext);
@@ -281,14 +292,14 @@ function SuspenseTab(_: {}) {
initLayoutState,
);
const {activities} = useContext(TreeStateContext);
const activities = useActivities();
// If there are no named Activity boundaries, we don't have any tree list and we should hide
// both the panel and the button to toggle it.
const activityListDisabled = activities.length === 0;
const treeListDisabled = activities.length === 0;
const wrapperTreeRef = useRef<null | HTMLElement>(null);
const resizeTreeRef = useRef<null | HTMLElement>(null);
const resizeActivityListRef = useRef<null | HTMLElement>(null);
const resizeTreeListRef = useRef<null | HTMLElement>(null);
// TODO: We'll show the recently inspected element in this tab when it should probably
// switch to the nearest Suspense boundary when we switch into this tab.
@@ -297,8 +308,8 @@ function SuspenseTab(_: {}) {
inspectedElementHidden,
inspectedElementHorizontalFraction,
inspectedElementVerticalFraction,
activityListHidden,
activityListHorizontalFraction,
treeListHidden,
treeListHorizontalFraction,
} = state;
useLayoutEffect(() => {
@@ -317,12 +328,12 @@ function SuspenseTab(_: {}) {
inspectedElementVerticalFraction * 100,
);
const resizeActivityListElement = resizeActivityListRef.current;
const resizeTreeListElement = resizeTreeListRef.current;
setResizeCSSVariable(
resizeActivityListElement,
'activity-list',
resizeTreeListElement,
'tree-list',
'horizontal',
activityListHorizontalFraction * 100,
treeListHorizontalFraction * 100,
);
}, []);
useEffect(() => {
@@ -333,8 +344,8 @@ function SuspenseTab(_: {}) {
inspectedElementHidden,
inspectedElementHorizontalFraction,
inspectedElementVerticalFraction,
activityListHidden,
activityListHorizontalFraction,
treeListHidden,
treeListHorizontalFraction,
}),
);
}, 500);
@@ -344,8 +355,8 @@ function SuspenseTab(_: {}) {
inspectedElementHidden,
inspectedElementHorizontalFraction,
inspectedElementVerticalFraction,
activityListHidden,
activityListHorizontalFraction,
treeListHidden,
treeListHorizontalFraction,
]);
const onResizeStart = (event: SyntheticPointerEvent) => {
@@ -409,14 +420,14 @@ function SuspenseTab(_: {}) {
}
};
const onResizeActivityList = (event: SyntheticPointerEvent) => {
const onResizeTreeList = (event: SyntheticPointerEvent) => {
const element = event.currentTarget;
const isResizing = element.hasPointerCapture(event.pointerId);
if (!isResizing) {
return;
}
const resizeElement = resizeActivityListRef.current;
const resizeElement = resizeTreeListRef.current;
const wrapperElement = resizeTreeRef.current;
if (wrapperElement === null || resizeElement === null) {
@@ -432,11 +443,11 @@ function SuspenseTab(_: {}) {
const currentMousePosition =
orientation === 'horizontal' ? event.clientX - left : event.clientY - top;
const boundaryMin = MINIMUM_ACTIVITY_LIST_SIZE;
const boundaryMin = MINIMUM_TREE_LIST_SIZE;
const boundaryMax =
orientation === 'horizontal'
? width - MINIMUM_ACTIVITY_LIST_SIZE
: height - MINIMUM_ACTIVITY_LIST_SIZE;
? width - MINIMUM_TREE_LIST_SIZE
: height - MINIMUM_TREE_LIST_SIZE;
const isMousePositionInBounds =
currentMousePosition > boundaryMin && currentMousePosition < boundaryMax;
@@ -444,15 +455,10 @@ function SuspenseTab(_: {}) {
if (isMousePositionInBounds) {
const resizedElementDimension =
orientation === 'horizontal' ? width : height;
const actionType = 'ACTION_SET_ACTIVITY_LIST_HORIZONTAL_FRACTION';
const actionType = 'ACTION_SET_TREE_LIST_HORIZONTAL_FRACTION';
const percentage = (currentMousePosition / resizedElementDimension) * 100;
setResizeCSSVariable(
resizeElement,
'activity-list',
orientation,
percentage,
);
setResizeCSSVariable(resizeElement, 'tree-list', orientation, percentage);
dispatch({
type: actionType,
@@ -467,21 +473,19 @@ function SuspenseTab(_: {}) {
<SettingsModalContextController>
<div className={styles.SuspenseTab} ref={wrapperTreeRef}>
<div className={styles.TreeWrapper} ref={resizeTreeRef}>
{activityListDisabled ? null : (
{treeListDisabled ? null : (
<div
className={styles.ActivityList}
hidden={activityListHidden}
ref={resizeActivityListRef}>
hidden={treeListHidden}
ref={resizeTreeListRef}>
<ActivityList activities={activities} />
</div>
)}
{activityListDisabled ? null : (
<div
className={styles.ResizeBarWrapper}
hidden={activityListHidden}>
{treeListDisabled ? null : (
<div className={styles.ResizeBarWrapper} hidden={treeListHidden}>
<div
onPointerDown={onResizeStart}
onPointerMove={onResizeActivityList}
onPointerMove={onResizeTreeList}
onPointerUp={onResizeEnd}
className={styles.ResizeBar}
/>
@@ -489,10 +493,10 @@ function SuspenseTab(_: {}) {
)}
<div className={styles.TreeView}>
<header className={styles.SuspenseTreeViewHeader}>
{activityListDisabled ? (
{treeListDisabled ? (
<div />
) : (
<ToggleActivityList dispatch={dispatch} state={state} />
<ToggleTreeList dispatch={dispatch} state={state} />
)}
{store.supportsClickToInspect && (
<Fragment>
@@ -555,19 +559,19 @@ function SuspenseTab(_: {}) {
const LOCAL_STORAGE_KEY = 'React::DevTools::SuspenseTab::layout';
const VERTICAL_TREE_MODE_MAX_WIDTH = 600;
const MINIMUM_TREE_SIZE = 100;
const MINIMUM_ACTIVITY_LIST_SIZE = 100;
const MINIMUM_TREE_LIST_SIZE = 100;
function layoutReducer(state: LayoutState, action: LayoutAction): LayoutState {
switch (action.type) {
case 'ACTION_SET_ACTIVITY_LIST_TOGGLE':
case 'ACTION_SET_TREE_LIST_TOGGLE':
return {
...state,
activityListHidden: !state.activityListHidden,
treeListHidden: !state.treeListHidden,
};
case 'ACTION_SET_ACTIVITY_LIST_HORIZONTAL_FRACTION':
case 'ACTION_SET_TREE_LIST_HORIZONTAL_FRACTION':
return {
...state,
activityListHorizontalFraction: action.payload,
treeListHorizontalFraction: action.payload,
};
case 'ACTION_SET_INSPECTED_ELEMENT_TOGGLE':
return {
@@ -593,8 +597,8 @@ function initLayoutState(): LayoutState {
let inspectedElementHidden = false;
let inspectedElementHorizontalFraction = 0.65;
let inspectedElementVerticalFraction = 0.5;
let activityListHidden = false;
let activityListHorizontalFraction = 0.35;
let treeListHidden = false;
let treeListHorizontalFraction = 0.35;
try {
let data = localStorageGetItem(LOCAL_STORAGE_KEY);
@@ -604,8 +608,8 @@ function initLayoutState(): LayoutState {
inspectedElementHorizontalFraction =
data.inspectedElementHorizontalFraction;
inspectedElementVerticalFraction = data.inspectedElementVerticalFraction;
activityListHidden = data.activityListHidden;
activityListHorizontalFraction = data.activityListHorizontalFraction;
treeListHidden = data.treeListHidden;
treeListHorizontalFraction = data.treeListHorizontalFraction;
}
} catch (error) {}
@@ -613,8 +617,8 @@ function initLayoutState(): LayoutState {
inspectedElementHidden,
inspectedElementHorizontalFraction,
inspectedElementVerticalFraction,
activityListHidden,
activityListHorizontalFraction,
treeListHidden,
treeListHorizontalFraction,
};
}
@@ -630,7 +634,7 @@ function getTreeOrientation(
function setResizeCSSVariable(
resizeElement: null | HTMLElement,
name: 'tree' | 'activity-list',
name: 'tree' | 'tree-list',
orientation: null | Orientation,
percentage: number,
): void {

View File

@@ -204,11 +204,7 @@ export type Rect = {
};
export type SuspenseTimelineStep = {
/**
* The first step is either a host root (initial paint) or Activity (Transition).
* Subsequent steps are always Suspense nodes.
*/
id: SuspenseNode['id'] | Element['id'], // TODO: Will become a group.
id: SuspenseNode['id'], // TODO: Will become a group.
environment: null | string,
endTime: number,
};

View File

@@ -75,26 +75,22 @@ function Root({children}: {children: React.Node}): React.Node {
);
}
const dynamicData = deferred(10, 'Dynamic Data: 📈📉📊', 'dynamicData');
export default function Segments(): React.Node {
return (
<>
<p>{dynamicData}</p>
<React.Activity name="root" mode="visible">
<Root>
<React.Activity name="outer" mode="visible">
<OuterSegment>
<React.Activity name="inner" mode="visible">
<InnerSegment>
<React.Activity name="slot" mode="visible">
<Page />
</React.Activity>
</InnerSegment>
</React.Activity>
</OuterSegment>
</React.Activity>
</Root>
</React.Activity>
</>
<React.Activity name="/" mode="visible">
<Root>
<React.Activity name="/outer/" mode="visible">
<OuterSegment>
<React.Activity name="/outer/inner" mode="visible">
<InnerSegment>
<React.Activity name="/outer/inner/page" mode="visible">
<Page />
</React.Activity>
</InnerSegment>
</React.Activity>
</OuterSegment>
</React.Activity>
</Root>
</React.Activity>
);
}

View File

@@ -341,6 +341,7 @@ export function getEventPriority(domEventName: DOMEventName): EventPriority {
case 'pointerup':
case 'ratechange':
case 'reset':
case 'resize':
case 'seeked':
case 'submit':
case 'toggle':
@@ -379,7 +380,6 @@ export function getEventPriority(domEventName: DOMEventName): EventPriority {
case 'pointermove':
case 'pointerout':
case 'pointerover':
case 'resize':
case 'scroll':
case 'touchmove':
case 'wheel':

View File

@@ -1111,64 +1111,4 @@ describe('ReactDOMFizzStaticBrowser', () => {
</div>,
);
});
// @gate enableHalt && enableOptimisticKey
it('can resume an optimistic keyed slot', async () => {
const errors = [];
let resolve;
const promise = new Promise(r => (resolve = r));
async function Component() {
await promise;
return 'Hi';
}
if (React.optimisticKey === undefined) {
throw new Error('optimisticKey missing');
}
function App() {
return (
<div>
<Suspense fallback="Loading">
<Component key={React.optimisticKey} />
</Suspense>
</div>
);
}
const controller = new AbortController();
const pendingResult = serverAct(() =>
ReactDOMFizzStatic.prerender(<App />, {
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
}),
);
await serverAct(() => {
controller.abort();
});
const prerendered = await pendingResult;
const postponedState = JSON.stringify(prerendered.postponed);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(<div>Loading</div>);
expect(prerendered.postponed).not.toBe(null);
await resolve();
const dynamic = await serverAct(() =>
ReactDOMFizzServer.resume(<App />, JSON.parse(postponedState)),
);
await readIntoContainer(dynamic);
expect(getVisibleChildren(container)).toEqual(<div>Hi</div>);
});
});

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