Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87af9cdb63 | ||
|
|
16e16ec6ff | ||
|
|
9599e7a787 | ||
|
|
67c1487ffd | ||
|
|
454e01e603 | ||
|
|
c9a8cf3411 | ||
|
|
fca172e3f3 | ||
|
|
fd524fe02a | ||
|
|
40b4a5bf71 | ||
|
|
df75af4edc | ||
|
|
bcc3fd8b05 | ||
|
|
50e7ec8a69 | ||
|
|
4cf770d7e1 | ||
|
|
7d67591041 | ||
|
|
7ee974de92 | ||
|
|
8ac5f4eb36 | ||
|
|
eb89912ee5 | ||
|
|
0972e23908 | ||
|
|
194c12d949 | ||
|
|
7f1a085b28 | ||
|
|
ea4899e13f | ||
|
|
b946a249b5 | ||
|
|
d6b1a0573b | ||
|
|
b315a0f713 | ||
|
|
7df96b0c1a | ||
|
|
45bc3c9f04 | ||
|
|
fb2177c153 | ||
|
|
647e13366c | ||
|
|
19b769fa5f | ||
|
|
dbf2538355 | ||
|
|
21f282425c | ||
|
|
257b033fc7 | ||
|
|
de97ef9ad5 |
9
compiler/.claude/settings.local.json
Normal file
9
compiler/.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(node scripts/enable-feature-flag.js:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
@@ -304,6 +304,30 @@ export class CompilerError extends Error {
|
||||
disabledDetails: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
printedMessage: string | null = null;
|
||||
|
||||
static simpleInvariant(
|
||||
condition: unknown,
|
||||
options: {
|
||||
reason: CompilerDiagnosticOptions['reason'];
|
||||
description?: CompilerDiagnosticOptions['description'];
|
||||
loc: SourceLocation;
|
||||
},
|
||||
): asserts condition {
|
||||
if (!condition) {
|
||||
const errors = new CompilerError();
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
reason: options.reason,
|
||||
description: options.description ?? null,
|
||||
category: ErrorCategory.Invariant,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: options.loc,
|
||||
message: options.reason,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
static invariant(
|
||||
condition: unknown,
|
||||
options: Omit<CompilerDiagnosticOptions, 'category'>,
|
||||
@@ -576,7 +600,8 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
case ErrorCategory.Suppression:
|
||||
case ErrorCategory.Syntax:
|
||||
case ErrorCategory.UseMemo:
|
||||
case ErrorCategory.VoidUseMemo: {
|
||||
case ErrorCategory.VoidUseMemo:
|
||||
case ErrorCategory.MemoDependencies: {
|
||||
heading = 'Error';
|
||||
break;
|
||||
}
|
||||
@@ -634,6 +659,10 @@ export enum ErrorCategory {
|
||||
* Checks that manual memoization is preserved
|
||||
*/
|
||||
PreserveManualMemo = 'PreserveManualMemo',
|
||||
/**
|
||||
* Checks for exhaustive useMemo/useCallback dependencies without extraneous values
|
||||
*/
|
||||
MemoDependencies = 'MemoDependencies',
|
||||
/**
|
||||
* Checks for known incompatible libraries
|
||||
*/
|
||||
@@ -1031,6 +1060,24 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
preset: LintRulePreset.RecommendedLatest,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.MemoDependencies: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'memo-dependencies',
|
||||
description:
|
||||
'Validates that useMemo() and useCallback() specify comprehensive dependencies without extraneous values. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
/**
|
||||
* TODO: the "MemoDependencies" rule largely reimplements the "exhaustive-deps" non-compiler rule,
|
||||
* allowing the compiler to ensure it does not regress change behavior due to different dependencies.
|
||||
* We previously relied on the source having ESLint suppressions for any exhaustive-deps violations,
|
||||
* but it's more reliable to verify it within the compiler.
|
||||
*
|
||||
* Long-term we should de-duplicate these implementations.
|
||||
*/
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.IncompatibleLibrary: {
|
||||
return {
|
||||
category,
|
||||
|
||||
@@ -102,14 +102,25 @@ 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
|
||||
@@ -212,6 +223,19 @@ 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).
|
||||
@@ -293,6 +317,7 @@ export const defaultOptions: ParsedPluginOptions = {
|
||||
logger: null,
|
||||
gating: null,
|
||||
noEmit: false,
|
||||
outputMode: null,
|
||||
dynamicGating: null,
|
||||
eslintSuppressionRules: null,
|
||||
flowSuppressions: true,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {Logger, ProgramContext} from '.';
|
||||
import {CompilerOutputMode, Logger, ProgramContext} from '.';
|
||||
import {
|
||||
HIRFunction,
|
||||
ReactiveFunction,
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
pruneUnusedLabelsHIR,
|
||||
} from '../HIR';
|
||||
import {
|
||||
CompilerMode,
|
||||
Environment,
|
||||
EnvironmentConfig,
|
||||
ReactFunctionType,
|
||||
@@ -105,6 +104,8 @@ 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 =
|
||||
@@ -119,7 +120,7 @@ function run(
|
||||
>,
|
||||
config: EnvironmentConfig,
|
||||
fnType: ReactFunctionType,
|
||||
mode: CompilerMode,
|
||||
mode: CompilerOutputMode,
|
||||
programContext: ProgramContext,
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
@@ -169,7 +170,7 @@ function runWithEnvironment(
|
||||
validateUseMemo(hir).unwrap();
|
||||
|
||||
if (
|
||||
env.isInferredMemoEnabled &&
|
||||
env.enableDropManualMemoization &&
|
||||
!env.config.enablePreserveExistingManualUseMemo &&
|
||||
!env.config.disableMemoizationForDebugging &&
|
||||
!env.config.enableChangeDetectionForDebugging
|
||||
@@ -205,7 +206,7 @@ function runWithEnvironment(
|
||||
inferTypes(hir);
|
||||
log({kind: 'hir', name: 'InferTypes', value: hir});
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.enableValidations) {
|
||||
if (env.config.validateHooksUsage) {
|
||||
validateHooksUsage(hir).unwrap();
|
||||
}
|
||||
@@ -231,12 +232,17 @@ function runWithEnvironment(
|
||||
|
||||
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
|
||||
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.enableValidations) {
|
||||
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});
|
||||
@@ -253,14 +259,14 @@ function runWithEnvironment(
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.enableValidations) {
|
||||
if (mutabilityAliasingRangeErrors.isErr()) {
|
||||
throw mutabilityAliasingRangeErrors.unwrapErr();
|
||||
}
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.enableValidations) {
|
||||
if (env.config.assertValidMutableRanges) {
|
||||
assertValidMutableRanges(hir);
|
||||
}
|
||||
@@ -297,6 +303,13 @@ function runWithEnvironment(
|
||||
inferReactivePlaces(hir);
|
||||
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
|
||||
|
||||
if (env.enableValidations) {
|
||||
if (env.config.validateExhaustiveMemoizationDependencies) {
|
||||
// NOTE: this relies on reactivity inference running first
|
||||
validateExhaustiveDependencies(hir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
rewriteInstructionKindsBasedOnReassignment(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
@@ -304,11 +317,11 @@ function runWithEnvironment(
|
||||
value: hir,
|
||||
});
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.config.validateStaticComponents) {
|
||||
env.logErrors(validateStaticComponents(hir));
|
||||
}
|
||||
if (env.enableValidations && 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
|
||||
@@ -580,7 +593,7 @@ export function compileFn(
|
||||
>,
|
||||
config: EnvironmentConfig,
|
||||
fnType: ReactFunctionType,
|
||||
mode: CompilerMode,
|
||||
mode: CompilerOutputMode,
|
||||
programContext: ProgramContext,
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
validateRestrictedImports,
|
||||
} from './Imports';
|
||||
import {
|
||||
CompilerOutputMode,
|
||||
CompilerReactTarget,
|
||||
ParsedPluginOptions,
|
||||
PluginOptions,
|
||||
@@ -399,7 +400,15 @@ export function compileProgram(
|
||||
*/
|
||||
const suppressions = findProgramSuppressions(
|
||||
pass.comments,
|
||||
pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS,
|
||||
/*
|
||||
* If the compiler is validating hooks rules and exhaustive memo dependencies, we don't need to check
|
||||
* for React ESLint suppressions
|
||||
*/
|
||||
pass.opts.environment.validateExhaustiveMemoizationDependencies &&
|
||||
pass.opts.environment.validateHooksUsage
|
||||
? null
|
||||
: (pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS),
|
||||
// Always bail on Flow suppressions
|
||||
pass.opts.flowSuppressions,
|
||||
);
|
||||
|
||||
@@ -421,9 +430,17 @@ 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);
|
||||
const compiled = processFn(
|
||||
current.fn,
|
||||
current.fnType,
|
||||
programContext,
|
||||
outputMode,
|
||||
);
|
||||
|
||||
if (compiled != null) {
|
||||
for (const outlined of compiled.outlined) {
|
||||
@@ -581,6 +598,7 @@ function processFn(
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
programContext: ProgramContext,
|
||||
outputMode: CompilerOutputMode,
|
||||
): null | CodegenFunction {
|
||||
let directives: {
|
||||
optIn: t.Directive | null;
|
||||
@@ -616,18 +634,27 @@ function processFn(
|
||||
}
|
||||
|
||||
let compiledFn: CodegenFunction;
|
||||
const compileResult = tryCompileFunction(fn, fnType, programContext);
|
||||
const compileResult = tryCompileFunction(
|
||||
fn,
|
||||
fnType,
|
||||
programContext,
|
||||
outputMode,
|
||||
);
|
||||
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);
|
||||
}
|
||||
const retryResult = retryCompileFunction(fn, fnType, programContext);
|
||||
if (retryResult == null) {
|
||||
if (outputMode === 'client') {
|
||||
const retryResult = retryCompileFunction(fn, fnType, programContext);
|
||||
if (retryResult == null) {
|
||||
return null;
|
||||
}
|
||||
compiledFn = retryResult;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
compiledFn = retryResult;
|
||||
} else {
|
||||
compiledFn = compileResult.compiledFn;
|
||||
}
|
||||
@@ -663,7 +690,7 @@ function processFn(
|
||||
|
||||
if (programContext.hasModuleScopeOptOut) {
|
||||
return null;
|
||||
} else if (programContext.opts.noEmit) {
|
||||
} else if (programContext.opts.outputMode === 'lint') {
|
||||
/**
|
||||
* inferEffectDependencies + noEmit is currently only used for linting. In
|
||||
* this mode, add source locations for where the compiler *can* infer effect
|
||||
@@ -693,6 +720,7 @@ function tryCompileFunction(
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
programContext: ProgramContext,
|
||||
outputMode: CompilerOutputMode,
|
||||
):
|
||||
| {kind: 'compile'; compiledFn: CodegenFunction}
|
||||
| {kind: 'error'; error: unknown} {
|
||||
@@ -719,7 +747,7 @@ function tryCompileFunction(
|
||||
fn,
|
||||
programContext.opts.environment,
|
||||
fnType,
|
||||
'all_features',
|
||||
outputMode,
|
||||
programContext,
|
||||
programContext.opts.logger,
|
||||
programContext.filename,
|
||||
@@ -757,7 +785,7 @@ function retryCompileFunction(
|
||||
fn,
|
||||
environment,
|
||||
fnType,
|
||||
'no_inferred_memo',
|
||||
'client-no-memo',
|
||||
programContext,
|
||||
programContext.opts.logger,
|
||||
programContext.filename,
|
||||
|
||||
@@ -78,7 +78,7 @@ export function filterSuppressionsThatAffectFunction(
|
||||
|
||||
export function findProgramSuppressions(
|
||||
programComments: Array<t.Comment>,
|
||||
ruleNames: Array<string>,
|
||||
ruleNames: Array<string> | null,
|
||||
flowSuppressions: boolean,
|
||||
): Array<SuppressionRange> {
|
||||
const suppressionRanges: Array<SuppressionRange> = [];
|
||||
@@ -89,7 +89,7 @@ export function findProgramSuppressions(
|
||||
let disableNextLinePattern: RegExp | null = null;
|
||||
let disablePattern: RegExp | null = null;
|
||||
let enablePattern: RegExp | null = null;
|
||||
if (ruleNames.length !== 0) {
|
||||
if (ruleNames != null && ruleNames.length !== 0) {
|
||||
const rulePattern = `(${ruleNames.join('|')})`;
|
||||
disableNextLinePattern = new RegExp(
|
||||
`eslint-disable-next-line ${rulePattern}`,
|
||||
|
||||
@@ -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 {Logger, ProgramContext} from '../Entrypoint';
|
||||
import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
DEFAULT_GLOBALS,
|
||||
@@ -51,6 +51,7 @@ 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([
|
||||
@@ -217,6 +218,11 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validatePreserveExistingMemoizationGuarantees: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Validate that dependencies supplied to manual memoization calls are exhaustive.
|
||||
*/
|
||||
validateExhaustiveMemoizationDependencies: z.boolean().default(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
|
||||
@@ -672,11 +678,25 @@ export const EnvironmentConfigSchema = z.object({
|
||||
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
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>;
|
||||
@@ -716,7 +736,7 @@ export class Environment {
|
||||
code: string | null;
|
||||
config: EnvironmentConfig;
|
||||
fnType: ReactFunctionType;
|
||||
compilerMode: CompilerMode;
|
||||
outputMode: CompilerOutputMode;
|
||||
programContext: ProgramContext;
|
||||
hasFireRewrite: boolean;
|
||||
hasInferredEffect: boolean;
|
||||
@@ -731,7 +751,7 @@ export class Environment {
|
||||
constructor(
|
||||
scope: BabelScope,
|
||||
fnType: ReactFunctionType,
|
||||
compilerMode: CompilerMode,
|
||||
outputMode: CompilerOutputMode,
|
||||
config: EnvironmentConfig,
|
||||
contextIdentifiers: Set<t.Identifier>,
|
||||
parentFunction: NodePath<t.Function>, // the outermost function being compiled
|
||||
@@ -742,7 +762,7 @@ export class Environment {
|
||||
) {
|
||||
this.#scope = scope;
|
||||
this.fnType = fnType;
|
||||
this.compilerMode = compilerMode;
|
||||
this.outputMode = outputMode;
|
||||
this.config = config;
|
||||
this.filename = filename;
|
||||
this.code = code;
|
||||
@@ -838,8 +858,65 @@ export class Environment {
|
||||
return this.#flowTypeEnvironment;
|
||||
}
|
||||
|
||||
get isInferredMemoEnabled(): boolean {
|
||||
return this.compilerMode !== 'no_inferred_memo';
|
||||
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 nextIdentifierId(): IdentifierId {
|
||||
|
||||
@@ -23,13 +23,14 @@ import {
|
||||
BuiltInUseInsertionEffectHookId,
|
||||
BuiltInUseLayoutEffectHookId,
|
||||
BuiltInUseOperatorId,
|
||||
BuiltInUseOptimisticId,
|
||||
BuiltInUseReducerId,
|
||||
BuiltInUseRefId,
|
||||
BuiltInUseStateId,
|
||||
BuiltInUseTransitionId,
|
||||
BuiltInWeakMapId,
|
||||
BuiltInWeakSetId,
|
||||
BuiltinEffectEventId,
|
||||
BuiltInEffectEventId,
|
||||
ReanimatedSharedValueId,
|
||||
ShapeRegistry,
|
||||
addFunction,
|
||||
@@ -818,6 +819,18 @@ 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(
|
||||
@@ -863,7 +876,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
returnType: {
|
||||
kind: 'Function',
|
||||
return: {kind: 'Poly'},
|
||||
shapeId: BuiltinEffectEventId,
|
||||
shapeId: BuiltInEffectEventId,
|
||||
isConstructor: false,
|
||||
},
|
||||
calleeEffect: Effect.Read,
|
||||
|
||||
@@ -803,6 +803,7 @@ export type ManualMemoDependency = {
|
||||
| {
|
||||
kind: 'NamedLocal';
|
||||
value: Place;
|
||||
constant: boolean;
|
||||
}
|
||||
| {kind: 'Global'; identifierName: string};
|
||||
path: DependencyPath;
|
||||
@@ -817,6 +818,11 @@ export type StartMemoize = {
|
||||
* (e.g. useMemo without a second arg)
|
||||
*/
|
||||
deps: Array<ManualMemoDependency> | null;
|
||||
/**
|
||||
* The source location of the dependencies argument. Used for
|
||||
* emitting diagnostics with a suggested replacement
|
||||
*/
|
||||
depsLoc: SourceLocation | null;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
export type FinishMemoize = {
|
||||
@@ -1680,6 +1686,28 @@ export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
|
||||
)
|
||||
);
|
||||
}
|
||||
export function isSubPath(
|
||||
subpath: DependencyPath,
|
||||
path: DependencyPath,
|
||||
): boolean {
|
||||
return (
|
||||
subpath.length <= path.length &&
|
||||
subpath.every(
|
||||
(item, ix) =>
|
||||
item.property === path[ix].property &&
|
||||
item.optional === path[ix].optional,
|
||||
)
|
||||
);
|
||||
}
|
||||
export function isSubPathIgnoringOptionals(
|
||||
subpath: DependencyPath,
|
||||
path: DependencyPath,
|
||||
): boolean {
|
||||
return (
|
||||
subpath.length <= path.length &&
|
||||
subpath.every((item, ix) => item.property === path[ix].property)
|
||||
);
|
||||
}
|
||||
|
||||
export function getPlaceScope(
|
||||
id: InstructionId,
|
||||
@@ -1823,6 +1851,10 @@ 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';
|
||||
}
|
||||
@@ -1887,6 +1919,18 @@ 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'
|
||||
@@ -1920,7 +1964,8 @@ export function isStableType(id: Identifier): boolean {
|
||||
isSetActionStateType(id) ||
|
||||
isDispatcherType(id) ||
|
||||
isUseRefType(id) ||
|
||||
isStartTransitionType(id)
|
||||
isStartTransitionType(id) ||
|
||||
isSetOptimisticType(id)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1931,8 +1976,9 @@ export function isStableTypeContainer(id: Identifier): boolean {
|
||||
}
|
||||
return (
|
||||
isUseStateType(id) || // setState
|
||||
type_.shapeId === 'BuiltInUseActionState' || // setActionState
|
||||
isUseActionStateType(id) || // setActionState
|
||||
isUseReducerType(id) || // dispatcher
|
||||
isUseOptimisticType(id) || // setOptimistic
|
||||
type_.shapeId === 'BuiltInUseTransition' // startTransition
|
||||
);
|
||||
}
|
||||
@@ -1952,6 +1998,7 @@ export function evaluatesToStableTypeOrContainer(
|
||||
case 'useActionState':
|
||||
case 'useRef':
|
||||
case 'useTransition':
|
||||
case 'useOptimistic':
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +304,7 @@ export type HookKind =
|
||||
| 'useTransition'
|
||||
| 'useImperativeHandle'
|
||||
| 'useEffectEvent'
|
||||
| 'useOptimistic'
|
||||
| 'Custom';
|
||||
|
||||
/*
|
||||
@@ -399,12 +400,15 @@ 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';
|
||||
@@ -1185,6 +1189,25 @@ 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'}],
|
||||
[
|
||||
@@ -1243,7 +1266,20 @@ addFunction(
|
||||
calleeEffect: Effect.ConditionallyMutate,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
},
|
||||
BuiltinEffectEventId,
|
||||
BuiltInEffectEventId,
|
||||
);
|
||||
|
||||
addFunction(
|
||||
BUILTIN_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: Effect.ConditionallyMutate,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.ConditionallyMutate,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
},
|
||||
BuiltInEventHandlerId,
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
Instruction,
|
||||
InstructionKind,
|
||||
InstructionValue,
|
||||
makeInstructionId,
|
||||
Pattern,
|
||||
@@ -32,6 +33,32 @@ 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> {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -42,7 +42,7 @@ type IdentifierSidemap = {
|
||||
functions: Map<IdentifierId, TInstruction<FunctionExpression>>;
|
||||
manualMemos: Map<IdentifierId, ManualMemoCallee>;
|
||||
react: Set<IdentifierId>;
|
||||
maybeDepsLists: Map<IdentifierId, Array<Place>>;
|
||||
maybeDepsLists: Map<IdentifierId, {loc: SourceLocation; deps: Array<Place>}>;
|
||||
maybeDeps: Map<IdentifierId, ManualMemoDependency>;
|
||||
optionals: Set<IdentifierId>;
|
||||
};
|
||||
@@ -92,6 +92,7 @@ export function collectMaybeMemoDependencies(
|
||||
root: {
|
||||
kind: 'NamedLocal',
|
||||
value: {...value.place},
|
||||
constant: false,
|
||||
},
|
||||
path: [],
|
||||
};
|
||||
@@ -159,10 +160,10 @@ function collectTemporaries(
|
||||
}
|
||||
case 'ArrayExpression': {
|
||||
if (value.elements.every(e => e.kind === 'Identifier')) {
|
||||
sidemap.maybeDepsLists.set(
|
||||
instr.lvalue.identifier.id,
|
||||
value.elements as Array<Place>,
|
||||
);
|
||||
sidemap.maybeDepsLists.set(instr.lvalue.identifier.id, {
|
||||
loc: value.loc,
|
||||
deps: value.elements as Array<Place>,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -182,6 +183,7 @@ function makeManualMemoizationMarkers(
|
||||
fnExpr: Place,
|
||||
env: Environment,
|
||||
depsList: Array<ManualMemoDependency> | null,
|
||||
depsLoc: SourceLocation | null,
|
||||
memoDecl: Place,
|
||||
manualMemoId: number,
|
||||
): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
|
||||
@@ -197,6 +199,7 @@ function makeManualMemoizationMarkers(
|
||||
* as dependencies
|
||||
*/
|
||||
deps: depsList,
|
||||
depsLoc,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
effects: null,
|
||||
@@ -287,86 +290,85 @@ function extractManualMemoizationArgs(
|
||||
sidemap: IdentifierSidemap,
|
||||
errors: CompilerError,
|
||||
): {
|
||||
fnPlace: Place | null;
|
||||
fnPlace: Place;
|
||||
depsList: Array<ManualMemoDependency> | null;
|
||||
} {
|
||||
depsLoc: SourceLocation | null;
|
||||
} | null {
|
||||
const [fnPlace, depsListPlace] = instr.value.args as Array<
|
||||
Place | SpreadPattern | undefined
|
||||
>;
|
||||
if (fnPlace == null) {
|
||||
if (fnPlace == null || fnPlace.kind !== 'Identifier') {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected a callback function to be passed to ${kind}`,
|
||||
description: `Expected a callback function to be passed to ${kind}`,
|
||||
description:
|
||||
kind === 'useCallback'
|
||||
? 'The first argument to useCallback() must be a function to cache'
|
||||
: 'The first argument to useMemo() must be a function that calculates a result to cache',
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: `Expected a callback function to be passed to ${kind}`,
|
||||
message:
|
||||
kind === 'useCallback'
|
||||
? `Expected a callback function`
|
||||
: `Expected a memoization function`,
|
||||
}),
|
||||
);
|
||||
return {fnPlace: null, depsList: null};
|
||||
return null;
|
||||
}
|
||||
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
|
||||
if (depsListPlace == null) {
|
||||
return {
|
||||
fnPlace,
|
||||
depsList: null,
|
||||
depsLoc: null,
|
||||
};
|
||||
}
|
||||
const maybeDepsList =
|
||||
depsListPlace.kind === 'Identifier'
|
||||
? sidemap.maybeDepsLists.get(depsListPlace.identifier.id)
|
||||
: null;
|
||||
if (maybeDepsList == null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Unexpected spread argument to ${kind}`,
|
||||
description: `Unexpected spread argument to ${kind}`,
|
||||
reason: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
description: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: `Unexpected spread argument to ${kind}`,
|
||||
loc:
|
||||
depsListPlace?.kind === 'Identifier' ? depsListPlace.loc : instr.loc,
|
||||
message: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
}),
|
||||
);
|
||||
return {fnPlace: null, depsList: null};
|
||||
return null;
|
||||
}
|
||||
let depsList: Array<ManualMemoDependency> | null = null;
|
||||
if (depsListPlace != null) {
|
||||
const maybeDepsList = sidemap.maybeDepsLists.get(
|
||||
depsListPlace.identifier.id,
|
||||
);
|
||||
if (maybeDepsList == null) {
|
||||
const depsList: Array<ManualMemoDependency> = [];
|
||||
for (const dep of maybeDepsList.deps) {
|
||||
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
|
||||
if (maybeDep == null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
description: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: depsListPlace.loc,
|
||||
message: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
loc: dep.loc,
|
||||
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
}),
|
||||
);
|
||||
return {fnPlace, depsList: null};
|
||||
}
|
||||
depsList = [];
|
||||
for (const dep of maybeDepsList) {
|
||||
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
|
||||
if (maybeDep == null) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: dep.loc,
|
||||
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
depsList.push(maybeDep);
|
||||
}
|
||||
} else {
|
||||
depsList.push(maybeDep);
|
||||
}
|
||||
}
|
||||
return {
|
||||
fnPlace,
|
||||
depsList,
|
||||
depsLoc: maybeDepsList.loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -427,16 +429,17 @@ export function dropManualMemoization(
|
||||
|
||||
const manualMemo = sidemap.manualMemos.get(id);
|
||||
if (manualMemo != null) {
|
||||
const {fnPlace, depsList} = extractManualMemoizationArgs(
|
||||
const memoDetails = extractManualMemoizationArgs(
|
||||
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
|
||||
manualMemo.kind,
|
||||
sidemap,
|
||||
errors,
|
||||
);
|
||||
|
||||
if (fnPlace == null) {
|
||||
if (memoDetails == null) {
|
||||
continue;
|
||||
}
|
||||
const {fnPlace, depsList, depsLoc} = memoDetails;
|
||||
|
||||
instr.value = getManualMemoizationReplacement(
|
||||
fnPlace,
|
||||
@@ -487,6 +490,7 @@ export function dropManualMemoization(
|
||||
fnPlace,
|
||||
func.env,
|
||||
depsList,
|
||||
depsLoc,
|
||||
memoDecl,
|
||||
nextManualMemoId++,
|
||||
);
|
||||
|
||||
@@ -954,6 +954,7 @@ function applyEffect(
|
||||
case ValueKind.Primitive: {
|
||||
break;
|
||||
}
|
||||
case ValueKind.MaybeFrozen:
|
||||
case ValueKind.Frozen: {
|
||||
sourceType = 'frozen';
|
||||
break;
|
||||
@@ -2451,7 +2452,7 @@ function computeEffectsForLegacySignature(
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) {
|
||||
if (signature.knownIncompatible != null && state.env.enableValidations) {
|
||||
const errors = new CompilerError();
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
BlockId,
|
||||
Effect,
|
||||
Environment,
|
||||
HIRFunction,
|
||||
@@ -15,14 +14,12 @@ import {
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
Place,
|
||||
computePostDominatorTree,
|
||||
evaluatesToStableTypeOrContainer,
|
||||
getHookKind,
|
||||
isStableType,
|
||||
isStableTypeContainer,
|
||||
isUseOperator,
|
||||
} from '../HIR';
|
||||
import {PostDominator} from '../HIR/Dominator';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionOperand,
|
||||
@@ -35,6 +32,7 @@ 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
|
||||
@@ -212,45 +210,9 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
reactiveIdentifiers.markReactive(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;
|
||||
}
|
||||
const isReactiveControlledBlock = createControlDominators(fn, place =>
|
||||
reactiveIdentifiers.isReactive(place),
|
||||
);
|
||||
|
||||
do {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
@@ -411,61 +373,6 @@ 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();
|
||||
|
||||
@@ -609,6 +609,19 @@ function evaluateInstruction(
|
||||
constantPropagationImpl(value.loweredFunc.func, constants);
|
||||
return null;
|
||||
}
|
||||
case 'StartMemoize': {
|
||||
if (value.deps != null) {
|
||||
for (const dep of value.deps) {
|
||||
if (dep.root.kind === 'NamedLocal') {
|
||||
const placeValue = read(constants, dep.root.value);
|
||||
if (placeValue != null && placeValue.kind === 'Primitive') {
|
||||
dep.root.constant = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
default: {
|
||||
// TODO: handle more cases
|
||||
return null;
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
import {
|
||||
BlockId,
|
||||
Environment,
|
||||
getHookKind,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
@@ -68,9 +70,14 @@ 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);
|
||||
@@ -112,7 +119,7 @@ function findReferencedIdentifiers(fn: HIRFunction): State {
|
||||
const hasLoop = hasBackEdge(fn);
|
||||
const reversedBlocks = [...fn.body.blocks.values()].reverse();
|
||||
|
||||
const state = new State();
|
||||
const state = new State(fn.env);
|
||||
let size = state.count;
|
||||
do {
|
||||
size = state.count;
|
||||
@@ -310,12 +317,27 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
|
||||
// explicitly retain debugger statements to not break debugging workflows
|
||||
return false;
|
||||
}
|
||||
case 'Await':
|
||||
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 'ComputedDelete':
|
||||
case 'ComputedStore':
|
||||
case 'PropertyDelete':
|
||||
case 'MethodCall':
|
||||
case 'PropertyStore':
|
||||
case 'StoreGlobal': {
|
||||
/*
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -159,7 +159,7 @@ export function codegenFunction(
|
||||
const compiled = compileResult.unwrap();
|
||||
|
||||
const hookGuard = fn.env.config.enableEmitHookGuards;
|
||||
if (hookGuard != null && fn.env.isInferredMemoEnabled) {
|
||||
if (hookGuard != null && fn.env.outputMode === 'client') {
|
||||
compiled.body = t.blockStatement([
|
||||
createHookGuard(
|
||||
hookGuard,
|
||||
@@ -259,7 +259,7 @@ export function codegenFunction(
|
||||
if (
|
||||
emitInstrumentForget != null &&
|
||||
fn.id != null &&
|
||||
fn.env.isInferredMemoEnabled
|
||||
fn.env.outputMode === 'client'
|
||||
) {
|
||||
/*
|
||||
* Technically, this is a conditional hook call. However, we expect
|
||||
@@ -591,7 +591,10 @@ function codegenBlockNoReset(
|
||||
}
|
||||
|
||||
function wrapCacheDep(cx: Context, value: t.Expression): t.Expression {
|
||||
if (cx.env.config.enableEmitFreeze != null && cx.env.isInferredMemoEnabled) {
|
||||
if (
|
||||
cx.env.config.enableEmitFreeze != null &&
|
||||
cx.env.outputMode === 'client'
|
||||
) {
|
||||
const emitFreezeIdentifier = cx.env.programContext.addImportSpecifier(
|
||||
cx.env.config.enableEmitFreeze,
|
||||
).name;
|
||||
@@ -1359,8 +1362,6 @@ 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 &&
|
||||
@@ -1368,26 +1369,6 @@ 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);
|
||||
}
|
||||
@@ -1794,7 +1775,7 @@ function createCallExpression(
|
||||
}
|
||||
|
||||
const hookGuard = env.config.enableEmitHookGuards;
|
||||
if (hookGuard != null && isHook && env.isInferredMemoEnabled) {
|
||||
if (hookGuard != null && isHook && env.outputMode === 'client') {
|
||||
const iife = t.functionExpression(
|
||||
null,
|
||||
[],
|
||||
|
||||
@@ -19,7 +19,11 @@ import {
|
||||
promoteTemporary,
|
||||
} from '../HIR';
|
||||
import {clonePlaceToTemporary} from '../HIR/HIRBuilder';
|
||||
import {eachPatternOperand, mapPatternOperands} from '../HIR/visitors';
|
||||
import {
|
||||
eachInstructionLValueWithKind,
|
||||
eachPatternOperand,
|
||||
mapPatternOperands,
|
||||
} from '../HIR/visitors';
|
||||
import {
|
||||
ReactiveFunctionTransform,
|
||||
Transformed,
|
||||
@@ -113,6 +117,9 @@ 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,
|
||||
@@ -120,7 +127,8 @@ class Visitor extends ReactiveFunctionTransform<State> {
|
||||
instruction.value,
|
||||
);
|
||||
if (transformed) {
|
||||
return {
|
||||
instructionsToProcess = transformed;
|
||||
result = {
|
||||
kind: 'replace-many',
|
||||
value: transformed.map(instruction => ({
|
||||
kind: 'instruction',
|
||||
@@ -129,7 +137,17 @@ class Visitor extends ReactiveFunctionTransform<State> {
|
||||
};
|
||||
}
|
||||
}
|
||||
return {kind: 'keep'};
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,10 +162,13 @@ function transformDestructuring(
|
||||
const isDeclared = state.declared.has(place.identifier.declarationId);
|
||||
if (isDeclared) {
|
||||
reassigned.add(place.identifier.id);
|
||||
} else {
|
||||
hasDeclaration = true;
|
||||
}
|
||||
hasDeclaration ||= !isDeclared;
|
||||
}
|
||||
if (reassigned.size === 0 || !hasDeclaration) {
|
||||
if (!hasDeclaration) {
|
||||
// all reassignments
|
||||
destructure.lvalue.kind = InstructionKind.Reassign;
|
||||
return null;
|
||||
}
|
||||
/*
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
BuiltInArrayId,
|
||||
BuiltInEventHandlerId,
|
||||
BuiltInFunctionId,
|
||||
BuiltInJsxId,
|
||||
BuiltInMixedReadonlyId,
|
||||
@@ -471,6 +472,41 @@ 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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,828 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
CompilerSuggestionOperation,
|
||||
SourceLocation,
|
||||
} from '..';
|
||||
import {CompilerSuggestion, ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
areEqualPaths,
|
||||
BlockId,
|
||||
DependencyPath,
|
||||
FinishMemoize,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionKind,
|
||||
isPrimitiveType,
|
||||
isStableType,
|
||||
isSubPath,
|
||||
isSubPathIgnoringOptionals,
|
||||
isUseRefType,
|
||||
LoadGlobal,
|
||||
ManualMemoDependency,
|
||||
Place,
|
||||
StartMemoize,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueLValue,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {retainWhere} from '../Utils/utils';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
/**
|
||||
* Validates that existing manual memoization is exhaustive and does not
|
||||
* have extraneous dependencies. The goal of the validation is to ensure
|
||||
* that auto-memoization will not substantially change the behavior of
|
||||
* the program:
|
||||
* - If the manual dependencies were non-exhaustive (missing important deps)
|
||||
* then auto-memoization will include those dependencies, and cause the
|
||||
* value to update *more* frequently.
|
||||
* - If the manual dependencies had extraneous deps, then auto memoization
|
||||
* will remove them and cause the value to update *less* frequently.
|
||||
*
|
||||
* The implementation compares the manual dependencies against the values
|
||||
* actually used within the memoization function
|
||||
* - For each value V referenced in the memo function, either:
|
||||
* - If the value is non-reactive *and* a known stable type, then the
|
||||
* value may optionally be specified as an exact dependency.
|
||||
* - Otherwise, report an error unless there is a manual dependency that will
|
||||
* invalidate whenever V invalidates. If `x.y.z` is referenced, there must
|
||||
* be a manual dependency for `x.y.z`, `x.y`, or `x`. Note that we assume
|
||||
* no interior mutability, ie we assume that any changes to inner paths must
|
||||
* always cause the other path to change as well.
|
||||
* - Any dependencies that do not correspond to a value referenced in the memo
|
||||
* function are considered extraneous and throw an error
|
||||
*
|
||||
* ## TODO: Invalid, Complex Deps
|
||||
*
|
||||
* Handle cases where the user deps were not simple identifiers + property chains.
|
||||
* We try to detect this in ValidateUseMemo but we miss some cases. The problem
|
||||
* is that invalid forms can be value blocks or function calls that don't get
|
||||
* removed by DCE, leaving a structure like:
|
||||
*
|
||||
* StartMemoize
|
||||
* t0 = <value to memoize>
|
||||
* ...non-DCE'd code for manual deps...
|
||||
* FinishMemoize decl=t0
|
||||
*
|
||||
* When we go to compute the dependencies, we then think that the user's manual dep
|
||||
* logic is part of what the memo computation logic.
|
||||
*/
|
||||
export function validateExhaustiveDependencies(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const reactive = collectReactiveIdentifiersHIR(fn);
|
||||
|
||||
const temporaries: Map<IdentifierId, Temporary> = new Map();
|
||||
for (const param of fn.params) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
temporaries.set(place.identifier.id, {
|
||||
kind: 'Local',
|
||||
identifier: place.identifier,
|
||||
path: [],
|
||||
context: false,
|
||||
loc: place.loc,
|
||||
});
|
||||
}
|
||||
const error = new CompilerError();
|
||||
let startMemo: StartMemoize | null = null;
|
||||
|
||||
function onStartMemoize(
|
||||
value: StartMemoize,
|
||||
dependencies: Set<InferredDependency>,
|
||||
locals: Set<IdentifierId>,
|
||||
): void {
|
||||
CompilerError.simpleInvariant(startMemo == null, {
|
||||
reason: 'Unexpected nested memo calls',
|
||||
loc: value.loc,
|
||||
});
|
||||
startMemo = value;
|
||||
dependencies.clear();
|
||||
locals.clear();
|
||||
}
|
||||
function onFinishMemoize(
|
||||
value: FinishMemoize,
|
||||
dependencies: Set<InferredDependency>,
|
||||
locals: Set<IdentifierId>,
|
||||
): void {
|
||||
CompilerError.simpleInvariant(
|
||||
startMemo != null && startMemo.manualMemoId === value.manualMemoId,
|
||||
{
|
||||
reason: 'Found FinishMemoize without corresponding StartMemoize',
|
||||
loc: value.loc,
|
||||
},
|
||||
);
|
||||
visitCandidateDependency(value.decl, temporaries, dependencies, locals);
|
||||
const inferred: Array<InferredDependency> = Array.from(dependencies);
|
||||
// Sort dependencies by name and path, with shorter/non-optional paths first
|
||||
inferred.sort((a, b) => {
|
||||
if (a.kind === 'Global' && b.kind == 'Global') {
|
||||
return a.binding.name.localeCompare(b.binding.name);
|
||||
} else if (a.kind == 'Local' && b.kind == 'Local') {
|
||||
CompilerError.simpleInvariant(
|
||||
a.identifier.name != null &&
|
||||
a.identifier.name.kind === 'named' &&
|
||||
b.identifier.name != null &&
|
||||
b.identifier.name.kind === 'named',
|
||||
{
|
||||
reason: 'Expected dependencies to be named variables',
|
||||
loc: a.loc,
|
||||
},
|
||||
);
|
||||
if (a.identifier.id !== b.identifier.id) {
|
||||
return a.identifier.name.value.localeCompare(b.identifier.name.value);
|
||||
}
|
||||
if (a.path.length !== b.path.length) {
|
||||
// if a's path is shorter this returns a negative, sorting a first
|
||||
return a.path.length - b.path.length;
|
||||
}
|
||||
for (let i = 0; i < a.path.length; i++) {
|
||||
const aProperty = a.path[i];
|
||||
const bProperty = b.path[i];
|
||||
const aOptional = aProperty.optional ? 0 : 1;
|
||||
const bOptional = bProperty.optional ? 0 : 1;
|
||||
if (aOptional !== bOptional) {
|
||||
// sort non-optionals first
|
||||
return aOptional - bOptional;
|
||||
} else if (aProperty.property !== bProperty.property) {
|
||||
return String(aProperty.property).localeCompare(
|
||||
String(bProperty.property),
|
||||
);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
} else {
|
||||
const aName =
|
||||
a.kind === 'Global' ? a.binding.name : a.identifier.name?.value;
|
||||
const bName =
|
||||
b.kind === 'Global' ? b.binding.name : b.identifier.name?.value;
|
||||
if (aName != null && bName != null) {
|
||||
return aName.localeCompare(bName);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
// remove redundant inferred dependencies
|
||||
retainWhere(inferred, (dep, ix) => {
|
||||
const match = inferred.findIndex(prevDep => {
|
||||
return (
|
||||
isEqualTemporary(prevDep, dep) ||
|
||||
(prevDep.kind === 'Local' &&
|
||||
dep.kind === 'Local' &&
|
||||
prevDep.identifier.id === dep.identifier.id &&
|
||||
isSubPath(prevDep.path, dep.path))
|
||||
);
|
||||
});
|
||||
// only retain entries that don't have a prior match
|
||||
return match === -1 || match >= ix;
|
||||
});
|
||||
// Validate that all manual dependencies belong there
|
||||
if (DEBUG) {
|
||||
console.log('manual');
|
||||
console.log(
|
||||
(startMemo.deps ?? [])
|
||||
.map(x => ' ' + printManualMemoDependency(x))
|
||||
.join('\n'),
|
||||
);
|
||||
console.log('inferred');
|
||||
console.log(
|
||||
inferred.map(x => ' ' + printInferredDependency(x)).join('\n'),
|
||||
);
|
||||
}
|
||||
const manualDependencies = startMemo.deps ?? [];
|
||||
const matched: Set<ManualMemoDependency> = new Set();
|
||||
const missing: Array<Extract<InferredDependency, {kind: 'Local'}>> = [];
|
||||
const extra: Array<ManualMemoDependency> = [];
|
||||
for (const inferredDependency of inferred) {
|
||||
if (inferredDependency.kind === 'Global') {
|
||||
for (const manualDependency of manualDependencies) {
|
||||
if (
|
||||
manualDependency.root.kind === 'Global' &&
|
||||
manualDependency.root.identifierName ===
|
||||
inferredDependency.binding.name
|
||||
) {
|
||||
matched.add(manualDependency);
|
||||
extra.push(manualDependency);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
CompilerError.simpleInvariant(inferredDependency.kind === 'Local', {
|
||||
reason: 'Unexpected function dependency',
|
||||
loc: value.loc,
|
||||
});
|
||||
let hasMatchingManualDependency = false;
|
||||
for (const manualDependency of manualDependencies) {
|
||||
if (
|
||||
manualDependency.root.kind === 'NamedLocal' &&
|
||||
manualDependency.root.value.identifier.id ===
|
||||
inferredDependency.identifier.id &&
|
||||
(areEqualPaths(manualDependency.path, inferredDependency.path) ||
|
||||
isSubPathIgnoringOptionals(
|
||||
manualDependency.path,
|
||||
inferredDependency.path,
|
||||
))
|
||||
) {
|
||||
hasMatchingManualDependency = true;
|
||||
matched.add(manualDependency);
|
||||
}
|
||||
}
|
||||
const isOptionalDependency =
|
||||
!reactive.has(inferredDependency.identifier.id) &&
|
||||
(isStableType(inferredDependency.identifier) ||
|
||||
isPrimitiveType(inferredDependency.identifier));
|
||||
if (hasMatchingManualDependency || isOptionalDependency) {
|
||||
continue;
|
||||
}
|
||||
missing.push(inferredDependency);
|
||||
}
|
||||
|
||||
for (const dep of startMemo.deps ?? []) {
|
||||
if (matched.has(dep)) {
|
||||
continue;
|
||||
}
|
||||
if (dep.root.kind === 'NamedLocal' && dep.root.constant) {
|
||||
CompilerError.simpleInvariant(
|
||||
!dep.root.value.reactive &&
|
||||
isPrimitiveType(dep.root.value.identifier),
|
||||
{
|
||||
reason: 'Expected constant-folded dependency to be non-reactive',
|
||||
loc: dep.root.value.loc,
|
||||
},
|
||||
);
|
||||
/*
|
||||
* Constant primitives can get constant-folded, which means we won't
|
||||
* see a LoadLocal for the value within the memo function.
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
extra.push(dep);
|
||||
}
|
||||
|
||||
if (missing.length !== 0 || extra.length !== 0) {
|
||||
let suggestions: Array<CompilerSuggestion> | null = null;
|
||||
if (startMemo.depsLoc != null && typeof startMemo.depsLoc !== 'symbol') {
|
||||
suggestions = [
|
||||
{
|
||||
description: 'Update dependencies',
|
||||
range: [startMemo.depsLoc.start.index, startMemo.depsLoc.end.index],
|
||||
op: CompilerSuggestionOperation.Replace,
|
||||
text: `[${inferred.map(printInferredDependency).join(', ')}]`,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (missing.length !== 0) {
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.MemoDependencies,
|
||||
reason: 'Found missing memoization dependencies',
|
||||
description:
|
||||
'Missing dependencies can cause a value not to update when those inputs change, ' +
|
||||
'resulting in stale UI',
|
||||
suggestions,
|
||||
});
|
||||
for (const dep of missing) {
|
||||
let reactiveStableValueHint = '';
|
||||
if (isStableType(dep.identifier)) {
|
||||
reactiveStableValueHint =
|
||||
'. Refs, setState functions, and other "stable" values generally do not need to be added as dependencies, but this variable may change over time to point to different values';
|
||||
}
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
message: `Missing dependency \`${printInferredDependency(dep)}\`${reactiveStableValueHint}`,
|
||||
loc: dep.loc,
|
||||
});
|
||||
}
|
||||
error.pushDiagnostic(diagnostic);
|
||||
} else if (extra.length !== 0) {
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: ErrorCategory.MemoDependencies,
|
||||
reason: 'Found unnecessary memoization dependencies',
|
||||
description:
|
||||
'Unnecessary dependencies can cause a value to update more often than necessary, ' +
|
||||
'causing performance regressions and effects to fire more often than expected',
|
||||
});
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
message: `Unnecessary dependencies ${extra.map(dep => `\`${printManualMemoDependency(dep)}\``).join(', ')}`,
|
||||
loc: startMemo.depsLoc ?? value.loc,
|
||||
});
|
||||
error.pushDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
dependencies.clear();
|
||||
locals.clear();
|
||||
startMemo = null;
|
||||
}
|
||||
|
||||
collectDependencies(
|
||||
fn,
|
||||
temporaries,
|
||||
{
|
||||
onStartMemoize,
|
||||
onFinishMemoize,
|
||||
},
|
||||
false, // isFunctionExpression
|
||||
);
|
||||
return error.asResult();
|
||||
}
|
||||
|
||||
function addDependency(
|
||||
dep: Temporary,
|
||||
dependencies: Set<InferredDependency>,
|
||||
locals: Set<IdentifierId>,
|
||||
): void {
|
||||
if (dep.kind === 'Function') {
|
||||
for (const x of dep.dependencies) {
|
||||
addDependency(x, dependencies, locals);
|
||||
}
|
||||
} else if (dep.kind === 'Global') {
|
||||
dependencies.add(dep);
|
||||
} else if (!locals.has(dep.identifier.id)) {
|
||||
dependencies.add(dep);
|
||||
}
|
||||
}
|
||||
|
||||
function visitCandidateDependency(
|
||||
place: Place,
|
||||
temporaries: Map<IdentifierId, Temporary>,
|
||||
dependencies: Set<InferredDependency>,
|
||||
locals: Set<IdentifierId>,
|
||||
): void {
|
||||
const dep = temporaries.get(place.identifier.id);
|
||||
if (dep != null) {
|
||||
addDependency(dep, dependencies, locals);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function determines the dependencies of the given function relative to
|
||||
* its external context. Dependencies are collected eagerly, the first time an
|
||||
* external variable is referenced, as opposed to trying to delay or aggregate
|
||||
* calculation of dependencies until they are later "used".
|
||||
*
|
||||
* For example, in
|
||||
*
|
||||
* ```
|
||||
* function f() {
|
||||
* let x = y; // we record a dependency on `y` here
|
||||
* ...
|
||||
* use(x); // as opposed to trying to delay that dependency until here
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* That said, LoadLocal/LoadContext does not immediately take a dependency,
|
||||
* we store the dependency in a temporary and set it as used when that temporary
|
||||
* is referenced as an operand.
|
||||
*
|
||||
* As we proceed through the function we track local variables that it creates
|
||||
* and don't consider later references to these variables as dependencies.
|
||||
*
|
||||
* For function expressions we first collect the function's dependencies by
|
||||
* calling this function recursively, _without_ taking into account whether
|
||||
* the "external" variables it accesses are actually external or just locals
|
||||
* in the parent. We then prune any locals and immediately consider any
|
||||
* remaining externals that it accesses as a dependency:
|
||||
*
|
||||
* ```
|
||||
* function Component() {
|
||||
* const local = ...;
|
||||
* const f = () => { return [external, local] };
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Here we calculate `f` as having dependencies `external, `local` and save
|
||||
* this into `temporaries`. We then also immediately take these as dependencies
|
||||
* at the Component scope, at which point we filter out `local` as a local variable,
|
||||
* leaving just a dependency on `external`.
|
||||
*
|
||||
* When calling this function on a top-level component or hook, the collected dependencies
|
||||
* will only contain the globals that it accesses which isn't useful. Instead, passing
|
||||
* onStartMemoize/onFinishMemoize callbacks allows looking at the dependencies within
|
||||
* blocks of manual memoization.
|
||||
*/
|
||||
function collectDependencies(
|
||||
fn: HIRFunction,
|
||||
temporaries: Map<IdentifierId, Temporary>,
|
||||
callbacks: {
|
||||
onStartMemoize: (
|
||||
startMemo: StartMemoize,
|
||||
dependencies: Set<InferredDependency>,
|
||||
locals: Set<IdentifierId>,
|
||||
) => void;
|
||||
onFinishMemoize: (
|
||||
finishMemo: FinishMemoize,
|
||||
dependencies: Set<InferredDependency>,
|
||||
locals: Set<IdentifierId>,
|
||||
) => void;
|
||||
} | null,
|
||||
isFunctionExpression: boolean,
|
||||
): Extract<Temporary, {kind: 'Function'}> {
|
||||
const optionals = findOptionalPlaces(fn);
|
||||
if (DEBUG) {
|
||||
console.log(prettyFormat(optionals));
|
||||
}
|
||||
const locals: Set<IdentifierId> = new Set();
|
||||
if (isFunctionExpression) {
|
||||
for (const param of fn.params) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
locals.add(place.identifier.id);
|
||||
}
|
||||
}
|
||||
|
||||
const dependencies: Set<InferredDependency> = new Set();
|
||||
function visit(place: Place): void {
|
||||
visitCandidateDependency(place, temporaries, dependencies, locals);
|
||||
}
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
let deps: Array<Temporary> | null = null;
|
||||
for (const operand of phi.operands.values()) {
|
||||
const dep = temporaries.get(operand.identifier.id);
|
||||
if (dep == null) {
|
||||
continue;
|
||||
}
|
||||
if (deps == null) {
|
||||
deps = [dep];
|
||||
} else {
|
||||
deps.push(dep);
|
||||
}
|
||||
}
|
||||
if (deps == null) {
|
||||
continue;
|
||||
} else if (deps.length === 1) {
|
||||
temporaries.set(phi.place.identifier.id, deps[0]!);
|
||||
} else {
|
||||
temporaries.set(phi.place.identifier.id, {
|
||||
kind: 'Function',
|
||||
dependencies: new Set(deps),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'LoadGlobal': {
|
||||
temporaries.set(lvalue.identifier.id, {
|
||||
kind: 'Global',
|
||||
binding: value.binding,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'LoadContext':
|
||||
case 'LoadLocal': {
|
||||
if (locals.has(value.place.identifier.id)) {
|
||||
break;
|
||||
}
|
||||
const temp = temporaries.get(value.place.identifier.id);
|
||||
if (temp != null) {
|
||||
if (temp.kind === 'Local') {
|
||||
const local: Temporary = {...temp, loc: value.place.loc};
|
||||
temporaries.set(lvalue.identifier.id, local);
|
||||
} else {
|
||||
temporaries.set(lvalue.identifier.id, temp);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'DeclareLocal': {
|
||||
const local: Temporary = {
|
||||
kind: 'Local',
|
||||
identifier: value.lvalue.place.identifier,
|
||||
path: [],
|
||||
context: false,
|
||||
loc: value.lvalue.place.loc,
|
||||
};
|
||||
temporaries.set(value.lvalue.place.identifier.id, local);
|
||||
locals.add(value.lvalue.place.identifier.id);
|
||||
break;
|
||||
}
|
||||
case 'StoreLocal': {
|
||||
if (value.lvalue.place.identifier.name == null) {
|
||||
const temp = temporaries.get(value.value.identifier.id);
|
||||
if (temp != null) {
|
||||
temporaries.set(value.lvalue.place.identifier.id, temp);
|
||||
}
|
||||
break;
|
||||
}
|
||||
visit(value.value);
|
||||
if (value.lvalue.kind !== InstructionKind.Reassign) {
|
||||
const local: Temporary = {
|
||||
kind: 'Local',
|
||||
identifier: value.lvalue.place.identifier,
|
||||
path: [],
|
||||
context: false,
|
||||
loc: value.lvalue.place.loc,
|
||||
};
|
||||
temporaries.set(value.lvalue.place.identifier.id, local);
|
||||
locals.add(value.lvalue.place.identifier.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'DeclareContext': {
|
||||
const local: Temporary = {
|
||||
kind: 'Local',
|
||||
identifier: value.lvalue.place.identifier,
|
||||
path: [],
|
||||
context: true,
|
||||
loc: value.lvalue.place.loc,
|
||||
};
|
||||
temporaries.set(value.lvalue.place.identifier.id, local);
|
||||
break;
|
||||
}
|
||||
case 'StoreContext': {
|
||||
visit(value.value);
|
||||
if (value.lvalue.kind !== InstructionKind.Reassign) {
|
||||
const local: Temporary = {
|
||||
kind: 'Local',
|
||||
identifier: value.lvalue.place.identifier,
|
||||
path: [],
|
||||
context: true,
|
||||
loc: value.lvalue.place.loc,
|
||||
};
|
||||
temporaries.set(value.lvalue.place.identifier.id, local);
|
||||
locals.add(value.lvalue.place.identifier.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Destructure': {
|
||||
visit(value.value);
|
||||
if (value.lvalue.kind !== InstructionKind.Reassign) {
|
||||
for (const lvalue of eachInstructionValueLValue(value)) {
|
||||
const local: Temporary = {
|
||||
kind: 'Local',
|
||||
identifier: lvalue.identifier,
|
||||
path: [],
|
||||
context: false,
|
||||
loc: lvalue.loc,
|
||||
};
|
||||
temporaries.set(lvalue.identifier.id, local);
|
||||
locals.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
if (
|
||||
typeof value.property === 'number' ||
|
||||
(isUseRefType(value.object.identifier) &&
|
||||
value.property === 'current')
|
||||
) {
|
||||
visit(value.object);
|
||||
break;
|
||||
}
|
||||
const object = temporaries.get(value.object.identifier.id);
|
||||
if (object != null && object.kind === 'Local') {
|
||||
const optional = optionals.get(value.object.identifier.id) ?? false;
|
||||
const local: Temporary = {
|
||||
kind: 'Local',
|
||||
identifier: object.identifier,
|
||||
context: object.context,
|
||||
path: [
|
||||
...object.path,
|
||||
{
|
||||
optional,
|
||||
property: value.property,
|
||||
},
|
||||
],
|
||||
loc: value.loc,
|
||||
};
|
||||
temporaries.set(lvalue.identifier.id, local);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'FunctionExpression':
|
||||
case 'ObjectMethod': {
|
||||
const functionDeps = collectDependencies(
|
||||
value.loweredFunc.func,
|
||||
temporaries,
|
||||
null,
|
||||
true, // isFunctionExpression
|
||||
);
|
||||
temporaries.set(lvalue.identifier.id, functionDeps);
|
||||
addDependency(functionDeps, dependencies, locals);
|
||||
break;
|
||||
}
|
||||
case 'StartMemoize': {
|
||||
const onStartMemoize = callbacks?.onStartMemoize;
|
||||
if (onStartMemoize != null) {
|
||||
onStartMemoize(value, dependencies, locals);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'FinishMemoize': {
|
||||
const onFinishMemoize = callbacks?.onFinishMemoize;
|
||||
if (onFinishMemoize != null) {
|
||||
onFinishMemoize(value, dependencies, locals);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MethodCall': {
|
||||
// Ignore the method itself
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (operand.identifier.id === value.property.identifier.id) {
|
||||
continue;
|
||||
}
|
||||
visit(operand);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
visit(operand);
|
||||
}
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
locals.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (optionals.has(operand.identifier.id)) {
|
||||
continue;
|
||||
}
|
||||
visit(operand);
|
||||
}
|
||||
}
|
||||
return {kind: 'Function', dependencies};
|
||||
}
|
||||
|
||||
function printInferredDependency(dep: InferredDependency): string {
|
||||
switch (dep.kind) {
|
||||
case 'Global': {
|
||||
return dep.binding.name;
|
||||
}
|
||||
case 'Local': {
|
||||
CompilerError.simpleInvariant(
|
||||
dep.identifier.name != null && dep.identifier.name.kind === 'named',
|
||||
{
|
||||
reason: 'Expected dependencies to be named variables',
|
||||
loc: dep.loc,
|
||||
},
|
||||
);
|
||||
return `${dep.identifier.name.value}${dep.path.map(p => (p.optional ? '?' : '') + '.' + p.property).join('')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printManualMemoDependency(dep: ManualMemoDependency): string {
|
||||
let identifierName: string;
|
||||
if (dep.root.kind === 'Global') {
|
||||
identifierName = dep.root.identifierName;
|
||||
} else {
|
||||
const name = dep.root.value.identifier.name;
|
||||
CompilerError.simpleInvariant(name != null && name.kind === 'named', {
|
||||
reason: 'Expected manual dependencies to be named variables',
|
||||
loc: dep.root.value.loc,
|
||||
});
|
||||
identifierName = name.value;
|
||||
}
|
||||
return `${identifierName}${dep.path.map(p => (p.optional ? '?' : '') + '.' + p.property).join('')}`;
|
||||
}
|
||||
|
||||
function isEqualTemporary(a: Temporary, b: Temporary): boolean {
|
||||
switch (a.kind) {
|
||||
case 'Function': {
|
||||
return false;
|
||||
}
|
||||
case 'Global': {
|
||||
return b.kind === 'Global' && a.binding.name === b.binding.name;
|
||||
}
|
||||
case 'Local': {
|
||||
return (
|
||||
b.kind === 'Local' &&
|
||||
a.identifier.id === b.identifier.id &&
|
||||
areEqualPaths(a.path, b.path)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Temporary =
|
||||
| {kind: 'Global'; binding: LoadGlobal['binding']}
|
||||
| {
|
||||
kind: 'Local';
|
||||
identifier: Identifier;
|
||||
path: DependencyPath;
|
||||
context: boolean;
|
||||
loc: SourceLocation;
|
||||
}
|
||||
| {kind: 'Function'; dependencies: Set<Temporary>};
|
||||
type InferredDependency = Extract<Temporary, {kind: 'Local' | 'Global'}>;
|
||||
|
||||
function collectReactiveIdentifiersHIR(fn: HIRFunction): Set<IdentifierId> {
|
||||
const reactive = new Set<IdentifierId>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
if (lvalue.reactive) {
|
||||
reactive.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
if (operand.reactive) {
|
||||
reactive.add(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (operand.reactive) {
|
||||
reactive.add(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reactive;
|
||||
}
|
||||
|
||||
export function findOptionalPlaces(
|
||||
fn: HIRFunction,
|
||||
): Map<IdentifierId, boolean> {
|
||||
const optionals = new Map<IdentifierId, boolean>();
|
||||
const visited: Set<BlockId> = new Set();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
if (visited.has(block.id)) {
|
||||
continue;
|
||||
}
|
||||
if (block.terminal.kind === 'optional') {
|
||||
visited.add(block.id);
|
||||
const optionalTerminal = block.terminal;
|
||||
let testBlock = fn.body.blocks.get(block.terminal.test)!;
|
||||
const queue: Array<boolean | null> = [block.terminal.optional];
|
||||
loop: while (true) {
|
||||
visited.add(testBlock.id);
|
||||
const terminal = testBlock.terminal;
|
||||
switch (terminal.kind) {
|
||||
case 'branch': {
|
||||
const isOptional = queue.pop();
|
||||
CompilerError.simpleInvariant(isOptional !== undefined, {
|
||||
reason:
|
||||
'Expected an optional value for each optional test condition',
|
||||
loc: terminal.test.loc,
|
||||
});
|
||||
if (isOptional != null) {
|
||||
optionals.set(terminal.test.identifier.id, isOptional);
|
||||
}
|
||||
if (terminal.fallthrough === optionalTerminal.fallthrough) {
|
||||
// found it
|
||||
const consequent = fn.body.blocks.get(terminal.consequent)!;
|
||||
const last = consequent.instructions.at(-1);
|
||||
if (last !== undefined && last.value.kind === 'StoreLocal') {
|
||||
if (isOptional != null) {
|
||||
optionals.set(last.value.value.identifier.id, isOptional);
|
||||
}
|
||||
}
|
||||
break loop;
|
||||
} else {
|
||||
testBlock = fn.body.blocks.get(terminal.fallthrough)!;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'optional': {
|
||||
queue.push(terminal.optional);
|
||||
testBlock = fn.body.blocks.get(terminal.test)!;
|
||||
break;
|
||||
}
|
||||
case 'logical':
|
||||
case 'ternary': {
|
||||
queue.push(null);
|
||||
testBlock = fn.body.blocks.get(terminal.test)!;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'sequence': {
|
||||
// Do we need sequence?? In any case, don't push to queue bc there is no corresponding branch terminal
|
||||
testBlock = fn.body.blocks.get(terminal.block)!;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
CompilerError.simpleInvariant(false, {
|
||||
reason: `Unexpected terminal in optional`,
|
||||
loc: terminal.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
CompilerError.simpleInvariant(queue.length === 0, {
|
||||
reason:
|
||||
'Expected a matching number of conditional blocks and branch points',
|
||||
loc: block.terminal.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
return optionals;
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
BasicBlock,
|
||||
isUseRefType,
|
||||
SourceLocation,
|
||||
ArrayExpression,
|
||||
} from '../HIR';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
@@ -36,15 +37,23 @@ 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 effects: Set<HIRFunction>;
|
||||
readonly effectsCache: Map<IdentifierId, EffectMetadata>;
|
||||
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();
|
||||
@@ -175,18 +184,20 @@ 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 effects: Set<HIRFunction> = new Set();
|
||||
const effectsCache: Map<IdentifierId, EffectMetadata> = new Map();
|
||||
|
||||
const setStateLoads: Map<IdentifierId, IdentifierId> = new Map();
|
||||
const setStateUsages: Map<IdentifierId, Set<SourceLocation>> = new Map();
|
||||
|
||||
const context: ValidationContext = {
|
||||
functions,
|
||||
candidateDependencies,
|
||||
errors,
|
||||
derivationCache,
|
||||
effects,
|
||||
effectsCache,
|
||||
setStateLoads,
|
||||
setStateUsages,
|
||||
};
|
||||
@@ -215,6 +226,7 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
}
|
||||
|
||||
let isFirstPass = true;
|
||||
let iterationCount = 0;
|
||||
do {
|
||||
context.derivationCache.takeSnapshot();
|
||||
|
||||
@@ -227,10 +239,23 @@ 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 effects) {
|
||||
validateEffect(effect, context);
|
||||
for (const [, effect] of effectsCache) {
|
||||
validateEffect(effect.effect, effect.dependencies, context);
|
||||
}
|
||||
|
||||
return errors.asResult();
|
||||
@@ -354,10 +379,16 @@ function recordInstructionDerivations(
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const effectFunction = context.functions.get(value.args[0].identifier.id);
|
||||
if (effectFunction != null) {
|
||||
context.effects.add(effectFunction.loweredFunc.func);
|
||||
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,
|
||||
});
|
||||
}
|
||||
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
|
||||
} else if (isUseStateType(lvalue.identifier)) {
|
||||
typeOfValue = 'fromState';
|
||||
context.derivationCache.addDerivationEntry(
|
||||
lvalue,
|
||||
@@ -367,6 +398,8 @@ function recordInstructionDerivations(
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
context.candidateDependencies.set(lvalue.identifier.id, value);
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
@@ -405,6 +438,14 @@ 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:
|
||||
@@ -495,6 +536,19 @@ 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,
|
||||
@@ -596,6 +650,7 @@ function getFnLocalDeps(
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
dependencies: ArrayExpression,
|
||||
context: ValidationContext,
|
||||
): void {
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
@@ -612,6 +667,16 @@ 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();
|
||||
@@ -662,6 +727,18 @@ 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,
|
||||
);
|
||||
|
||||
@@ -14,12 +14,14 @@ import {
|
||||
BlockId,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Identifier,
|
||||
Place,
|
||||
SourceLocation,
|
||||
getHookKindForType,
|
||||
isRefValueType,
|
||||
isUseRefType,
|
||||
} from '../HIR';
|
||||
import {BuiltInEventHandlerId} from '../HIR/ObjectShape';
|
||||
import {
|
||||
eachInstructionOperand,
|
||||
eachInstructionValueOperand,
|
||||
@@ -183,6 +185,11 @@ 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;
|
||||
@@ -519,6 +526,9 @@ 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,
|
||||
@@ -526,29 +536,16 @@ function validateNoRefAccessInRenderImpl(
|
||||
*/
|
||||
if (
|
||||
isRefLValue ||
|
||||
isEventHandlerLValue ||
|
||||
(hookKind != null &&
|
||||
hookKind !== 'useState' &&
|
||||
hookKind !== 'useReducer')
|
||||
) {
|
||||
/**
|
||||
* 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.
|
||||
* 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)
|
||||
*/
|
||||
validateNoDirectRefValueAccess(errors, operand, env);
|
||||
} else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
|
||||
|
||||
@@ -21,13 +21,17 @@ 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 {Iterable_some} from '../Utils/utils';
|
||||
import {assertExhaustive, Iterable_some} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Validates against calling setState in the body of an effect (useEffect and friends),
|
||||
@@ -140,6 +144,8 @@ 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 => {
|
||||
@@ -150,9 +156,38 @@ 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 (env.config.enableAllowSetStateFromRefsInEffects) {
|
||||
if (enableAllowSetStateFromRefsInEffects) {
|
||||
const hasRefOperand = Iterable_some(
|
||||
eachInstructionValueOperand(instr.value),
|
||||
isDerivedFromRef,
|
||||
@@ -162,6 +197,46 @@ 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 (
|
||||
@@ -203,7 +278,7 @@ function getSetStateCall(
|
||||
isSetStateType(callee.identifier) ||
|
||||
setStateFunctions.has(callee.identifier.id)
|
||||
) {
|
||||
if (env.config.enableAllowSetStateFromRefsInEffects) {
|
||||
if (enableAllowSetStateFromRefsInEffects) {
|
||||
const arg = instr.value.args.at(0);
|
||||
if (
|
||||
arg !== undefined &&
|
||||
@@ -216,6 +291,8 @@ function getSetStateCall(
|
||||
* be needed when initial layout measurements from refs need to be stored in state.
|
||||
*/
|
||||
return null;
|
||||
} else if (isRefControlledBlock(block.id)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
@@ -267,6 +267,7 @@ function validateInferredDep(
|
||||
effect: Effect.Read,
|
||||
reactive: false,
|
||||
},
|
||||
constant: false,
|
||||
},
|
||||
path: [...dep.path],
|
||||
};
|
||||
@@ -379,6 +380,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
root: {
|
||||
kind: 'NamedLocal',
|
||||
value: storeTarget,
|
||||
constant: false,
|
||||
},
|
||||
path: [],
|
||||
});
|
||||
@@ -408,6 +410,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
root: {
|
||||
kind: 'NamedLocal',
|
||||
value: {...lvalue},
|
||||
constant: false,
|
||||
},
|
||||
path: [],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
|
||||
## 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>
|
||||
@@ -0,0 +1,48 @@
|
||||
// @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: [{}],
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
|
||||
## 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>
|
||||
@@ -0,0 +1,36 @@
|
||||
// @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: [{}],
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({x}) {
|
||||
useEffect(
|
||||
() => {
|
||||
console.log(x);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
},
|
||||
[
|
||||
/* intentionally missing deps */
|
||||
]
|
||||
);
|
||||
|
||||
const memo = useMemo(() => {
|
||||
return [x];
|
||||
}, [x]);
|
||||
|
||||
return <ValidateMemoization inputs={[x]} output={memo} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(10);
|
||||
const { x } = t0;
|
||||
let t1;
|
||||
if ($[0] !== x) {
|
||||
t1 = () => {
|
||||
console.log(x);
|
||||
};
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [];
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== x) {
|
||||
t3 = [x];
|
||||
$[3] = x;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
const memo = t3;
|
||||
let t4;
|
||||
if ($[5] !== x) {
|
||||
t4 = [x];
|
||||
$[5] = x;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] !== memo || $[8] !== t4) {
|
||||
t5 = <ValidateMemoization inputs={t4} output={memo} />;
|
||||
$[7] = memo;
|
||||
$[8] = t4;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,22 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({x}) {
|
||||
useEffect(
|
||||
() => {
|
||||
console.log(x);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
},
|
||||
[
|
||||
/* intentionally missing deps */
|
||||
]
|
||||
);
|
||||
|
||||
const memo = useMemo(() => {
|
||||
return [x];
|
||||
}, [x]);
|
||||
|
||||
return <ValidateMemoization inputs={[x]} output={memo} />;
|
||||
}
|
||||
@@ -11,7 +11,7 @@ function Component(props) {
|
||||
|
||||
Component = useMemo(() => {
|
||||
return Component;
|
||||
});
|
||||
}, [Component]);
|
||||
|
||||
return <Component {...props} />;
|
||||
}
|
||||
@@ -36,6 +36,7 @@ function Component(props) {
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
Component = Stringify;
|
||||
|
||||
Component;
|
||||
Component = Component;
|
||||
$[0] = Component;
|
||||
} else {
|
||||
|
||||
@@ -7,7 +7,7 @@ function Component(props) {
|
||||
|
||||
Component = useMemo(() => {
|
||||
return Component;
|
||||
});
|
||||
}, [Component]);
|
||||
|
||||
return <Component {...props} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,10 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component({prop}) {
|
||||
const [s, setS] = useState(0);
|
||||
useEffect(() => {
|
||||
setS(prop);
|
||||
}, [prop, setS]);
|
||||
|
||||
return <div>{prop}</div>;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,9 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp @enableTreatSetIdentifiersAsStateSetters @loggerTestOnly
|
||||
|
||||
function Component({setParentState, prop}) {
|
||||
useEffect(() => {
|
||||
setParentState(prop);
|
||||
}, [prop]);
|
||||
|
||||
return <div>{prop}</div>;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,32 @@
|
||||
// @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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
import {useState} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
const x = useMemo(() => {
|
||||
return [state];
|
||||
// error: `setState` is a stable type, but not actually referenced
|
||||
}, [state, setState]);
|
||||
|
||||
return 'oops';
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Found unnecessary memoization dependencies
|
||||
|
||||
Unnecessary dependencies can cause a value to update more often than necessary, causing performance regressions and effects to fire more often than expected.
|
||||
|
||||
error.invalid-exhaustive-deps-disallow-unused-stable-types.ts:11:5
|
||||
9 | return [state];
|
||||
10 | // error: `setState` is a stable type, but not actually referenced
|
||||
> 11 | }, [state, setState]);
|
||||
| ^^^^^^^^^^^^^^^^^ Unnecessary dependencies `setState`
|
||||
12 |
|
||||
13 | return 'oops';
|
||||
14 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
import {useState} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
const x = useMemo(() => {
|
||||
return [state];
|
||||
// error: `setState` is a stable type, but not actually referenced
|
||||
}, [state, setState]);
|
||||
|
||||
return 'oops';
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
import {useMemo} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
const a = useMemo(() => {
|
||||
return x?.y.z?.a;
|
||||
// error: too precise
|
||||
}, [x?.y.z?.a.b]);
|
||||
const b = useMemo(() => {
|
||||
return x.y.z?.a;
|
||||
// ok, not our job to type check nullability
|
||||
}, [x.y.z.a]);
|
||||
const c = useMemo(() => {
|
||||
return x?.y.z.a?.b;
|
||||
// error: too precise
|
||||
}, [x?.y.z.a?.b.z]);
|
||||
const d = useMemo(() => {
|
||||
return x?.y?.[(console.log(y), z?.b)];
|
||||
// ok
|
||||
}, [x?.y, y, z?.b]);
|
||||
const e = useMemo(() => {
|
||||
const e = [];
|
||||
e.push(x);
|
||||
return e;
|
||||
// ok
|
||||
}, [x]);
|
||||
const f = useMemo(() => {
|
||||
return [];
|
||||
// error: unnecessary
|
||||
}, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
|
||||
const ref1 = useRef(null);
|
||||
const ref2 = useRef(null);
|
||||
const ref = z ? ref1 : ref2;
|
||||
const cb = useMemo(() => {
|
||||
return () => {
|
||||
return ref.current;
|
||||
};
|
||||
// error: ref is a stable type but reactive
|
||||
}, []);
|
||||
return <Stringify results={[a, b, c, d, e, f, cb]} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 4 errors:
|
||||
|
||||
Error: Found missing memoization dependencies
|
||||
|
||||
Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
|
||||
|
||||
error.invalid-exhaustive-deps.ts:7:11
|
||||
5 | function Component({x, y, z}) {
|
||||
6 | const a = useMemo(() => {
|
||||
> 7 | return x?.y.z?.a;
|
||||
| ^^^^^^^^^ Missing dependency `x?.y.z?.a`
|
||||
8 | // error: too precise
|
||||
9 | }, [x?.y.z?.a.b]);
|
||||
10 | const b = useMemo(() => {
|
||||
|
||||
Error: Found missing memoization dependencies
|
||||
|
||||
Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
|
||||
|
||||
error.invalid-exhaustive-deps.ts:15:11
|
||||
13 | }, [x.y.z.a]);
|
||||
14 | const c = useMemo(() => {
|
||||
> 15 | return x?.y.z.a?.b;
|
||||
| ^^^^^^^^^^^ Missing dependency `x?.y.z.a?.b`
|
||||
16 | // error: too precise
|
||||
17 | }, [x?.y.z.a?.b.z]);
|
||||
18 | const d = useMemo(() => {
|
||||
|
||||
Error: Found unnecessary memoization dependencies
|
||||
|
||||
Unnecessary dependencies can cause a value to update more often than necessary, causing performance regressions and effects to fire more often than expected.
|
||||
|
||||
error.invalid-exhaustive-deps.ts:31:5
|
||||
29 | return [];
|
||||
30 | // error: unnecessary
|
||||
> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unnecessary dependencies `x`, `y.z`, `z?.y?.a`, `UNUSED_GLOBAL`
|
||||
32 | const ref1 = useRef(null);
|
||||
33 | const ref2 = useRef(null);
|
||||
34 | const ref = z ? ref1 : ref2;
|
||||
|
||||
Error: Found missing memoization dependencies
|
||||
|
||||
Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
|
||||
|
||||
error.invalid-exhaustive-deps.ts:37:13
|
||||
35 | const cb = useMemo(() => {
|
||||
36 | return () => {
|
||||
> 37 | return ref.current;
|
||||
| ^^^ Missing dependency `ref`. Refs, setState functions, and other "stable" values generally do not need to be added as dependencies, but this variable may change over time to point to different values
|
||||
38 | };
|
||||
39 | // error: ref is a stable type but reactive
|
||||
40 | }, []);
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
import {useMemo} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
const a = useMemo(() => {
|
||||
return x?.y.z?.a;
|
||||
// error: too precise
|
||||
}, [x?.y.z?.a.b]);
|
||||
const b = useMemo(() => {
|
||||
return x.y.z?.a;
|
||||
// ok, not our job to type check nullability
|
||||
}, [x.y.z.a]);
|
||||
const c = useMemo(() => {
|
||||
return x?.y.z.a?.b;
|
||||
// error: too precise
|
||||
}, [x?.y.z.a?.b.z]);
|
||||
const d = useMemo(() => {
|
||||
return x?.y?.[(console.log(y), z?.b)];
|
||||
// ok
|
||||
}, [x?.y, y, z?.b]);
|
||||
const e = useMemo(() => {
|
||||
const e = [];
|
||||
e.push(x);
|
||||
return e;
|
||||
// ok
|
||||
}, [x]);
|
||||
const f = useMemo(() => {
|
||||
return [];
|
||||
// error: unnecessary
|
||||
}, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
|
||||
const ref1 = useRef(null);
|
||||
const ref2 = useRef(null);
|
||||
const ref = z ? ref1 : ref2;
|
||||
const cb = useMemo(() => {
|
||||
return () => {
|
||||
return ref.current;
|
||||
};
|
||||
// error: ref is a stable type but reactive
|
||||
}, []);
|
||||
return <Stringify results={[a, b, c, d, e, f, cb]} />;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
|
||||
## 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 | </>
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// @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: [{}],
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
## 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 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// @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: [{}],
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
function Component() {
|
||||
const x = 0;
|
||||
const y = useMemo(() => {
|
||||
return [x];
|
||||
// x gets constant-folded but shouldn't count as extraneous,
|
||||
// it was referenced in the memo block
|
||||
}, [x]);
|
||||
return y;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
|
||||
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
const x = 0;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = [0];
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const y = t0;
|
||||
return y;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,11 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
|
||||
function Component() {
|
||||
const x = 0;
|
||||
const y = useMemo(() => {
|
||||
return [x];
|
||||
// x gets constant-folded but shouldn't count as extraneous,
|
||||
// it was referenced in the memo block
|
||||
}, [x]);
|
||||
return y;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
import {
|
||||
useCallback,
|
||||
useTransition,
|
||||
useState,
|
||||
useOptimistic,
|
||||
useActionState,
|
||||
useRef,
|
||||
useReducer,
|
||||
} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
const [s, setState] = useState();
|
||||
const ref = useRef(null);
|
||||
const [t, startTransition] = useTransition();
|
||||
const [u, addOptimistic] = useOptimistic();
|
||||
const [v, dispatch] = useReducer(() => {}, null);
|
||||
const [isPending, dispatchAction] = useActionState(() => {}, null);
|
||||
|
||||
return useCallback(() => {
|
||||
dispatch();
|
||||
startTransition(() => {});
|
||||
addOptimistic();
|
||||
setState(null);
|
||||
dispatchAction();
|
||||
ref.current = true;
|
||||
}, [
|
||||
// intentionally adding unnecessary deps on nonreactive stable values
|
||||
// to check that they're allowed
|
||||
dispatch,
|
||||
startTransition,
|
||||
addOptimistic,
|
||||
setState,
|
||||
dispatchAction,
|
||||
ref,
|
||||
]);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
|
||||
import {
|
||||
useCallback,
|
||||
useTransition,
|
||||
useState,
|
||||
useOptimistic,
|
||||
useActionState,
|
||||
useRef,
|
||||
useReducer,
|
||||
} from "react";
|
||||
|
||||
function useFoo() {
|
||||
const $ = _c(1);
|
||||
const [, setState] = useState();
|
||||
const ref = useRef(null);
|
||||
const [, startTransition] = useTransition();
|
||||
const [, addOptimistic] = useOptimistic();
|
||||
const [, dispatch] = useReducer(_temp, null);
|
||||
const [, dispatchAction] = useActionState(_temp2, null);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
dispatch();
|
||||
startTransition(_temp3);
|
||||
addOptimistic();
|
||||
setState(null);
|
||||
dispatchAction();
|
||||
ref.current = true;
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _temp3() {}
|
||||
function _temp2() {}
|
||||
function _temp() {}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) "[[ function params=0 ]]"
|
||||
@@ -0,0 +1,42 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
import {
|
||||
useCallback,
|
||||
useTransition,
|
||||
useState,
|
||||
useOptimistic,
|
||||
useActionState,
|
||||
useRef,
|
||||
useReducer,
|
||||
} from 'react';
|
||||
|
||||
function useFoo() {
|
||||
const [s, setState] = useState();
|
||||
const ref = useRef(null);
|
||||
const [t, startTransition] = useTransition();
|
||||
const [u, addOptimistic] = useOptimistic();
|
||||
const [v, dispatch] = useReducer(() => {}, null);
|
||||
const [isPending, dispatchAction] = useActionState(() => {}, null);
|
||||
|
||||
return useCallback(() => {
|
||||
dispatch();
|
||||
startTransition(() => {});
|
||||
addOptimistic();
|
||||
setState(null);
|
||||
dispatchAction();
|
||||
ref.current = true;
|
||||
}, [
|
||||
// intentionally adding unnecessary deps on nonreactive stable values
|
||||
// to check that they're allowed
|
||||
dispatch,
|
||||
startTransition,
|
||||
addOptimistic,
|
||||
setState,
|
||||
dispatchAction,
|
||||
ref,
|
||||
]);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
@@ -0,0 +1,201 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
import {useCallback, useMemo} from 'react';
|
||||
import {makeObject_Primitives, Stringify} from 'shared-runtime';
|
||||
|
||||
function useHook1(x) {
|
||||
return useMemo(() => {
|
||||
return x?.y.z?.a;
|
||||
}, [x?.y.z?.a]);
|
||||
}
|
||||
function useHook2(x) {
|
||||
useMemo(() => {
|
||||
return x.y.z?.a;
|
||||
}, [x.y.z?.a]);
|
||||
}
|
||||
function useHook3(x) {
|
||||
return useMemo(() => {
|
||||
return x?.y.z.a?.b;
|
||||
}, [x?.y.z.a?.b]);
|
||||
}
|
||||
function useHook4(x, y, z) {
|
||||
return useMemo(() => {
|
||||
return x?.y?.[(console.log(y), z?.b)];
|
||||
}, [x?.y, y, z?.b]);
|
||||
}
|
||||
function useHook5(x) {
|
||||
return useMemo(() => {
|
||||
const e = [];
|
||||
const local = makeObject_Primitives(x);
|
||||
const fn = () => {
|
||||
e.push(local);
|
||||
};
|
||||
fn();
|
||||
return e;
|
||||
}, [x]);
|
||||
}
|
||||
function useHook6(x) {
|
||||
return useMemo(() => {
|
||||
const f = [];
|
||||
f.push(x.y.z);
|
||||
f.push(x.y);
|
||||
f.push(x);
|
||||
return f;
|
||||
}, [x]);
|
||||
}
|
||||
|
||||
function useHook7(x) {
|
||||
const [state, setState] = useState(true);
|
||||
const f = () => {
|
||||
setState(x => !x);
|
||||
};
|
||||
return useCallback(() => {
|
||||
f();
|
||||
}, [f]);
|
||||
}
|
||||
|
||||
function Component({x, y, z}) {
|
||||
const a = useHook1(x);
|
||||
const b = useHook2(x);
|
||||
const c = useHook3(x);
|
||||
const d = useHook4(x, y, z);
|
||||
const e = useHook5(x);
|
||||
const f = useHook6(x);
|
||||
const g = useHook7(x);
|
||||
return <Stringify results={[a, b, c, d, e, f, g]} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { makeObject_Primitives, Stringify } from "shared-runtime";
|
||||
|
||||
function useHook1(x) {
|
||||
x?.y.z?.a;
|
||||
return x?.y.z?.a;
|
||||
}
|
||||
|
||||
function useHook2(x) {
|
||||
x.y.z?.a;
|
||||
}
|
||||
|
||||
function useHook3(x) {
|
||||
x?.y.z.a?.b;
|
||||
return x?.y.z.a?.b;
|
||||
}
|
||||
|
||||
function useHook4(x, y, z) {
|
||||
x?.y;
|
||||
z?.b;
|
||||
return x?.y?.[(console.log(y), z?.b)];
|
||||
}
|
||||
|
||||
function useHook5(x) {
|
||||
const $ = _c(2);
|
||||
let e;
|
||||
if ($[0] !== x) {
|
||||
e = [];
|
||||
const local = makeObject_Primitives(x);
|
||||
const fn = () => {
|
||||
e.push(local);
|
||||
};
|
||||
|
||||
fn();
|
||||
$[0] = x;
|
||||
$[1] = e;
|
||||
} else {
|
||||
e = $[1];
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
function useHook6(x) {
|
||||
const $ = _c(2);
|
||||
let f;
|
||||
if ($[0] !== x) {
|
||||
f = [];
|
||||
f.push(x.y.z);
|
||||
f.push(x.y);
|
||||
f.push(x);
|
||||
$[0] = x;
|
||||
$[1] = f;
|
||||
} else {
|
||||
f = $[1];
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
function useHook7(x) {
|
||||
const $ = _c(2);
|
||||
const [, setState] = useState(true);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
setState(_temp);
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const f = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => {
|
||||
f();
|
||||
};
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
function _temp(x_0) {
|
||||
return !x_0;
|
||||
}
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(8);
|
||||
const { x, y, z } = t0;
|
||||
const a = useHook1(x);
|
||||
const b = useHook2(x);
|
||||
const c = useHook3(x);
|
||||
const d = useHook4(x, y, z);
|
||||
const e = useHook5(x);
|
||||
const f = useHook6(x);
|
||||
const g = useHook7(x);
|
||||
let t1;
|
||||
if (
|
||||
$[0] !== a ||
|
||||
$[1] !== b ||
|
||||
$[2] !== c ||
|
||||
$[3] !== d ||
|
||||
$[4] !== e ||
|
||||
$[5] !== f ||
|
||||
$[6] !== g
|
||||
) {
|
||||
t1 = <Stringify results={[a, b, c, d, e, f, g]} />;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = c;
|
||||
$[3] = d;
|
||||
$[4] = e;
|
||||
$[5] = f;
|
||||
$[6] = g;
|
||||
$[7] = t1;
|
||||
} else {
|
||||
t1 = $[7];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,65 @@
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
import {useCallback, useMemo} from 'react';
|
||||
import {makeObject_Primitives, Stringify} from 'shared-runtime';
|
||||
|
||||
function useHook1(x) {
|
||||
return useMemo(() => {
|
||||
return x?.y.z?.a;
|
||||
}, [x?.y.z?.a]);
|
||||
}
|
||||
function useHook2(x) {
|
||||
useMemo(() => {
|
||||
return x.y.z?.a;
|
||||
}, [x.y.z?.a]);
|
||||
}
|
||||
function useHook3(x) {
|
||||
return useMemo(() => {
|
||||
return x?.y.z.a?.b;
|
||||
}, [x?.y.z.a?.b]);
|
||||
}
|
||||
function useHook4(x, y, z) {
|
||||
return useMemo(() => {
|
||||
return x?.y?.[(console.log(y), z?.b)];
|
||||
}, [x?.y, y, z?.b]);
|
||||
}
|
||||
function useHook5(x) {
|
||||
return useMemo(() => {
|
||||
const e = [];
|
||||
const local = makeObject_Primitives(x);
|
||||
const fn = () => {
|
||||
e.push(local);
|
||||
};
|
||||
fn();
|
||||
return e;
|
||||
}, [x]);
|
||||
}
|
||||
function useHook6(x) {
|
||||
return useMemo(() => {
|
||||
const f = [];
|
||||
f.push(x.y.z);
|
||||
f.push(x.y);
|
||||
f.push(x);
|
||||
return f;
|
||||
}, [x]);
|
||||
}
|
||||
|
||||
function useHook7(x) {
|
||||
const [state, setState] = useState(true);
|
||||
const f = () => {
|
||||
setState(x => !x);
|
||||
};
|
||||
return useCallback(() => {
|
||||
f();
|
||||
}, [f]);
|
||||
}
|
||||
|
||||
function Component({x, y, z}) {
|
||||
const a = useHook1(x);
|
||||
const b = useHook2(x);
|
||||
const c = useHook3(x);
|
||||
const d = useHook4(x, y, z);
|
||||
const e = useHook5(x);
|
||||
const f = useHook6(x);
|
||||
const g = useHook7(x);
|
||||
return <Stringify results={[a, b, c, d, e, f, g]} />;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"} @noEmit
|
||||
// @dynamicGating:{"source":"shared-runtime"} @outputMode:"lint"
|
||||
|
||||
function Foo() {
|
||||
'use memo if(getTrue)';
|
||||
@@ -19,7 +19,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @dynamicGating:{"source":"shared-runtime"} @noEmit
|
||||
// @dynamicGating:{"source":"shared-runtime"} @outputMode:"lint"
|
||||
|
||||
function Foo() {
|
||||
"use memo if(getTrue)";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @dynamicGating:{"source":"shared-runtime"} @noEmit
|
||||
// @dynamicGating:{"source":"shared-runtime"} @outputMode:"lint"
|
||||
|
||||
function Foo() {
|
||||
'use memo if(getTrue)';
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
|
||||
## 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 ]]
|
||||
@@ -0,0 +1,48 @@
|
||||
|
||||
## 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);
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly
|
||||
// @inferEffectDependencies @outputMode:"lint" @panicThreshold:"none"
|
||||
import {print} from 'shared-runtime';
|
||||
import useEffectWrapper from 'useEffectWrapper';
|
||||
import {AUTODEPS} from 'react';
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
## 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);
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @compilationMode:"all" @inferEffectDependencies @panicThreshold:"none" @noEmit
|
||||
// @inferEffectDependencies @outputMode:"lint" @panicThreshold:"none"
|
||||
import {print} from 'shared-runtime';
|
||||
import {AUTODEPS} from 'react';
|
||||
import useEffectWrapper from 'useEffectWrapper';
|
||||
import {AUTODEPS} from 'react';
|
||||
|
||||
function Foo({propVal}) {
|
||||
'use memo';
|
||||
@@ -11,7 +11,6 @@ function Foo({propVal}) {
|
||||
const arr2 = [];
|
||||
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
|
||||
arr2.push(2);
|
||||
|
||||
return {arr, arr2};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
## 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 ]]
|
||||
@@ -1,4 +1,4 @@
|
||||
// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel
|
||||
// @inferEffectDependencies @panicThreshold:"none"
|
||||
import {print} from 'shared-runtime';
|
||||
import useEffectWrapper from 'useEffectWrapper';
|
||||
import {AUTODEPS} from 'react';
|
||||
@@ -2,10 +2,10 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"all" @inferEffectDependencies @panicThreshold:"none" @noEmit
|
||||
// @inferEffectDependencies @panicThreshold:"none"
|
||||
import {print} from 'shared-runtime';
|
||||
import {AUTODEPS} from 'react';
|
||||
import useEffectWrapper from 'useEffectWrapper';
|
||||
import {AUTODEPS} from 'react';
|
||||
|
||||
function Foo({propVal}) {
|
||||
'use memo';
|
||||
@@ -15,7 +15,6 @@ function Foo({propVal}) {
|
||||
const arr2 = [];
|
||||
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
|
||||
arr2.push(2);
|
||||
|
||||
return {arr, arr2};
|
||||
}
|
||||
|
||||
@@ -30,20 +29,21 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"all" @inferEffectDependencies @panicThreshold:"none" @noEmit
|
||||
// @inferEffectDependencies @panicThreshold:"none"
|
||||
import { print } from "shared-runtime";
|
||||
import { AUTODEPS } from "react";
|
||||
import useEffectWrapper from "useEffectWrapper";
|
||||
import { AUTODEPS } from "react";
|
||||
|
||||
function Foo({ propVal }) {
|
||||
function Foo(t0) {
|
||||
"use memo";
|
||||
const { propVal } = t0;
|
||||
|
||||
const arr = [propVal];
|
||||
useEffectWrapper(() => print(arr), AUTODEPS);
|
||||
useEffectWrapper(() => print(arr), [arr]);
|
||||
|
||||
const arr2 = [];
|
||||
useEffectWrapper(() => arr2.push(propVal), AUTODEPS);
|
||||
useEffectWrapper(() => arr2.push(propVal), [arr2, propVal]);
|
||||
arr2.push(2);
|
||||
|
||||
return { arr, arr2 };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// @inferEffectDependencies @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}],
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @noEmit
|
||||
// @inferEffectDependencies @outputMode:"lint"
|
||||
import {print} from 'shared-runtime';
|
||||
import useEffectWrapper from 'useEffectWrapper';
|
||||
import {AUTODEPS} from 'react';
|
||||
@@ -17,7 +17,7 @@ function ReactiveVariable({propVal}) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @noEmit
|
||||
// @inferEffectDependencies @outputMode:"lint"
|
||||
import { print } from "shared-runtime";
|
||||
import useEffectWrapper from "useEffectWrapper";
|
||||
import { AUTODEPS } from "react";
|
||||
@@ -1,4 +1,4 @@
|
||||
// @inferEffectDependencies @noEmit
|
||||
// @inferEffectDependencies @outputMode:"lint"
|
||||
import {print} from 'shared-runtime';
|
||||
import useEffectWrapper from 'useEffectWrapper';
|
||||
import {AUTODEPS} from 'react';
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,13 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
|
||||
## 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 ]]
|
||||
@@ -0,0 +1,91 @@
|
||||
|
||||
## 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 ]]"
|
||||
@@ -0,0 +1,33 @@
|
||||
// @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: [],
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
|
||||
|
||||
import {useMemo} from 'react';
|
||||
|
||||
@@ -27,7 +27,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
|
||||
|
||||
import { useMemo } from "react";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
// @validatePreserveExistingMemoizationGuarantees @validateExhaustiveMemoizationDependencies:false
|
||||
|
||||
import {useMemo} from 'react';
|
||||
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
|
||||
## 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]
|
||||
@@ -0,0 +1,53 @@
|
||||
// @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',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
|
||||
## 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;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// @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} />;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,14 @@
|
||||
// @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} />;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,17 @@
|
||||
// @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} />;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
|
||||
## 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;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// @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} />;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
|
||||
## 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;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// @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} />;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @noEmit
|
||||
// @outputMode:"lint"
|
||||
|
||||
function Foo() {
|
||||
'use memo';
|
||||
@@ -19,7 +19,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @noEmit
|
||||
// @outputMode:"lint"
|
||||
|
||||
function Foo() {
|
||||
"use memo";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @noEmit
|
||||
// @outputMode:"lint"
|
||||
|
||||
function Foo() {
|
||||
'use memo';
|
||||
|
||||
@@ -15,7 +15,7 @@ function Component(props) {
|
||||
const x = makeObject_Primitives();
|
||||
x.value = props.value;
|
||||
mutate(x, free, part);
|
||||
}, [props.value]);
|
||||
}, [props.value, free, part]);
|
||||
mutate(free, part);
|
||||
return callback;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ function Component(props) {
|
||||
const x = makeObject_Primitives();
|
||||
x.value = props.value;
|
||||
mutate(x, free, part);
|
||||
}, [props.value]);
|
||||
}, [props.value, free, part]);
|
||||
mutate(free, part);
|
||||
return callback;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,40 @@
|
||||
// @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},
|
||||
],
|
||||
};
|
||||
@@ -21,7 +21,7 @@ import {isDeepStrictEqual} from 'util';
|
||||
import type {ParseResult} from '@babel/parser';
|
||||
|
||||
const COMPILER_OPTIONS: PluginOptions = {
|
||||
noEmit: true,
|
||||
outputMode: 'lint',
|
||||
panicThreshold: 'none',
|
||||
// Don't emit errors on Flow suppressions--Flow already gave a signal
|
||||
flowSuppressions: false,
|
||||
|
||||
@@ -487,6 +487,13 @@ 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;
|
||||
|
||||
@@ -44,6 +44,21 @@ 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}\``);
|
||||
@@ -111,11 +126,25 @@ async function readInputFixtures(
|
||||
} else {
|
||||
inputFiles = (
|
||||
await Promise.all(
|
||||
filter.paths.map(pattern =>
|
||||
glob.glob(`${pattern}{${INPUT_EXTENSIONS.join(',')}}`, {
|
||||
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, {
|
||||
cwd: rootDir,
|
||||
}),
|
||||
),
|
||||
});
|
||||
}),
|
||||
)
|
||||
).flat();
|
||||
}
|
||||
@@ -150,11 +179,13 @@ async function readOutputFixtures(
|
||||
} else {
|
||||
outputFiles = (
|
||||
await Promise.all(
|
||||
filter.paths.map(pattern =>
|
||||
glob.glob(`${pattern}${SNAPSHOT_EXTENSION}`, {
|
||||
filter.paths.map(pattern => {
|
||||
// Strip all extensions and find matching .expect.md files
|
||||
const basenameWithoutExt = stripAllExtensions(pattern);
|
||||
return glob.glob(`${basenameWithoutExt}${SNAPSHOT_EXTENSION}`, {
|
||||
cwd: rootDir,
|
||||
}),
|
||||
),
|
||||
});
|
||||
}),
|
||||
)
|
||||
).flat();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ type RunnerOptions = {
|
||||
watch: boolean;
|
||||
filter: boolean;
|
||||
update: boolean;
|
||||
pattern?: string;
|
||||
};
|
||||
|
||||
const opts: RunnerOptions = yargs
|
||||
@@ -52,8 +53,10 @@ const opts: RunnerOptions = yargs
|
||||
.default('worker-threads', true)
|
||||
.boolean('watch')
|
||||
.describe('watch', 'Run compiler in watch mode, re-running after changes')
|
||||
.alias('w', 'watch')
|
||||
.default('watch', false)
|
||||
.boolean('update')
|
||||
.alias('u', 'update')
|
||||
.describe('update', 'Update fixtures')
|
||||
.default('update', false)
|
||||
.boolean('filter')
|
||||
@@ -62,9 +65,15 @@ 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));
|
||||
.parseSync(hideBin(process.argv)) as RunnerOptions;
|
||||
|
||||
/**
|
||||
* Do a test run and return the test results
|
||||
@@ -171,7 +180,13 @@ export async function main(opts: RunnerOptions): Promise<void> {
|
||||
worker.getStderr().pipe(process.stderr);
|
||||
worker.getStdout().pipe(process.stdout);
|
||||
|
||||
if (opts.watch) {
|
||||
// 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) {
|
||||
makeWatchRunner(state => onChange(worker, state), opts.filter);
|
||||
if (opts.filter) {
|
||||
/**
|
||||
@@ -216,7 +231,18 @@ export async function main(opts: RunnerOptions): Promise<void> {
|
||||
try {
|
||||
execSync('yarn build', {cwd: PROJECT_ROOT});
|
||||
console.log('Built compiler successfully with tsup');
|
||||
const testFilter = opts.filter ? await readTestFilter() : null;
|
||||
|
||||
// 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 results = await runFixtures(worker, testFilter, 0);
|
||||
if (opts.update) {
|
||||
update(results);
|
||||
|
||||
347
compiler/scripts/enable-feature-flag.js
Executable file
347
compiler/scripts/enable-feature-flag.js
Executable file
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {execSync} = require('child_process');
|
||||
const yargs = require('yargs/yargs');
|
||||
const {hideBin} = require('yargs/helpers');
|
||||
|
||||
// Constants
|
||||
const COMPILER_ROOT = path.resolve(__dirname, '..');
|
||||
const ENVIRONMENT_TS_PATH = path.join(
|
||||
COMPILER_ROOT,
|
||||
'packages/babel-plugin-react-compiler/src/HIR/Environment.ts'
|
||||
);
|
||||
const FIXTURES_PATH = path.join(
|
||||
COMPILER_ROOT,
|
||||
'packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler'
|
||||
);
|
||||
const FIXTURE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx'];
|
||||
|
||||
/**
|
||||
* Parse command line arguments
|
||||
*/
|
||||
function parseArgs() {
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.usage('Usage: $0 <flag-name>')
|
||||
.command('$0 <flag-name>', 'Enable a feature flag by default', yargs => {
|
||||
yargs.positional('flag-name', {
|
||||
describe: 'Name of the feature flag to enable',
|
||||
type: 'string',
|
||||
});
|
||||
})
|
||||
.example(
|
||||
'$0 validateExhaustiveMemoizationDependencies',
|
||||
'Enable the validateExhaustiveMemoizationDependencies flag'
|
||||
)
|
||||
.help('h')
|
||||
.alias('h', 'help')
|
||||
.strict()
|
||||
.parseSync();
|
||||
|
||||
return argv['flag-name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a feature flag in Environment.ts by changing default(false) to default(true)
|
||||
*/
|
||||
function enableFlagInEnvironment(flagName) {
|
||||
console.log(`\nEnabling flag "${flagName}" in Environment.ts...`);
|
||||
|
||||
const content = fs.readFileSync(ENVIRONMENT_TS_PATH, 'utf8');
|
||||
|
||||
// Check if the flag exists with default(false)
|
||||
const flagPatternFalse = new RegExp(
|
||||
`(${escapeRegex(flagName)}:\\s*z\\.boolean\\(\\)\\.default\\()false(\\))`,
|
||||
'g'
|
||||
);
|
||||
|
||||
if (!flagPatternFalse.test(content)) {
|
||||
// Check if flag exists at all
|
||||
const flagExistsPattern = new RegExp(
|
||||
`${escapeRegex(flagName)}:\\s*z\\.boolean\\(\\)`,
|
||||
'g'
|
||||
);
|
||||
if (flagExistsPattern.test(content)) {
|
||||
// Check if it's already true
|
||||
const flagPatternTrue = new RegExp(
|
||||
`${escapeRegex(flagName)}:\\s*z\\.boolean\\(\\)\\.default\\(true\\)`,
|
||||
'g'
|
||||
);
|
||||
if (flagPatternTrue.test(content)) {
|
||||
console.error(`Error: Flag "${flagName}" already has default(true)`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.error(
|
||||
`Error: Flag "${flagName}" exists but doesn't have default(false)`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.error(`Error: Flag "${flagName}" not found in Environment.ts`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Perform the replacement
|
||||
const newContent = content.replace(flagPatternFalse, '$1true$2');
|
||||
|
||||
// Verify the replacement worked
|
||||
if (content === newContent) {
|
||||
console.error(`Error: Failed to replace flag "${flagName}"`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.writeFileSync(ENVIRONMENT_TS_PATH, newContent, 'utf8');
|
||||
console.log(`Successfully enabled "${flagName}" in Environment.ts`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to escape regex special characters
|
||||
*/
|
||||
function escapeRegex(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run yarn snap and capture output
|
||||
*/
|
||||
function runTests() {
|
||||
console.log('\nRunning test suite (yarn snap)...');
|
||||
|
||||
try {
|
||||
const output = execSync('yarn snap', {
|
||||
cwd: COMPILER_ROOT,
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe',
|
||||
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
||||
});
|
||||
return {success: true, output};
|
||||
} catch (error) {
|
||||
// yarn snap exits with code 1 when tests fail, which throws an error
|
||||
return {success: false, output: error.stdout || error.message};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse failing test names from test output
|
||||
*/
|
||||
function parseFailingTests(output) {
|
||||
const failingTests = [];
|
||||
|
||||
// Look for lines that contain "FAIL:" followed by the test name
|
||||
// Format: "FAIL: test-name" or with ANSI codes
|
||||
const lines = output.split('\n');
|
||||
for (const line of lines) {
|
||||
// Remove ANSI codes for easier parsing
|
||||
const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
|
||||
// Match "FAIL: test-name"
|
||||
const match = cleanLine.match(/^FAIL:\s*(.+)$/);
|
||||
if (match) {
|
||||
failingTests.push(match[1].trim());
|
||||
}
|
||||
}
|
||||
|
||||
return failingTests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the fixture file for a given test name
|
||||
*/
|
||||
function findFixtureFile(testName) {
|
||||
const basePath = path.join(FIXTURES_PATH, testName);
|
||||
|
||||
for (const ext of FIXTURE_EXTENSIONS) {
|
||||
const filePath = basePath + ext;
|
||||
if (fs.existsSync(filePath)) {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add pragma to disable the feature flag in a fixture file
|
||||
*/
|
||||
function addPragmaToFixture(filePath, flagName) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
if (lines.length === 0) {
|
||||
console.warn(`Warning: Empty file ${filePath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstLine = lines[0];
|
||||
const pragma = `@${flagName}:false`;
|
||||
|
||||
// Check if pragma already exists
|
||||
if (firstLine.includes(pragma)) {
|
||||
return false; // Already has the pragma
|
||||
}
|
||||
|
||||
// Check if first line is a single-line comment
|
||||
if (firstLine.trim().startsWith('//')) {
|
||||
// Append pragma to existing comment
|
||||
lines[0] = firstLine + ' ' + pragma;
|
||||
} else if (firstLine.trim().startsWith('/*')) {
|
||||
// Multi-line comment - insert new line before it
|
||||
lines.unshift('// ' + pragma);
|
||||
} else {
|
||||
// No comment - insert new comment as first line
|
||||
lines.unshift('// ' + pragma);
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update snapshot files
|
||||
*/
|
||||
function updateSnapshots() {
|
||||
console.log('\nUpdating snapshots (yarn snap -u)...');
|
||||
|
||||
try {
|
||||
execSync('yarn snap -u', {
|
||||
cwd: COMPILER_ROOT,
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe',
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
console.log('Snapshots updated successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error updating snapshots:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify all tests pass
|
||||
*/
|
||||
function verifyAllTestsPass() {
|
||||
console.log('\nRunning final verification (yarn snap)...');
|
||||
|
||||
const {success, output} = runTests();
|
||||
|
||||
// Parse summary line: "N Tests, N Passed, N Failed"
|
||||
const summaryMatch = output.match(
|
||||
/(\d+)\s+Tests,\s+(\d+)\s+Passed,\s+(\d+)\s+Failed/
|
||||
);
|
||||
|
||||
if (summaryMatch) {
|
||||
const [, total, passed, failed] = summaryMatch;
|
||||
console.log(
|
||||
`\nTest Results: ${total} Tests, ${passed} Passed, ${failed} Failed`
|
||||
);
|
||||
|
||||
if (failed === '0') {
|
||||
console.log('All tests passed!');
|
||||
return true;
|
||||
} else {
|
||||
console.error(`${failed} tests still failing`);
|
||||
const failingTests = parseFailingTests(output);
|
||||
if (failingTests.length > 0) {
|
||||
console.error('\nFailing tests:');
|
||||
failingTests.forEach(test => console.error(` - ${test}`));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
async function main() {
|
||||
const flagName = parseArgs();
|
||||
|
||||
console.log(`\nEnabling flag: '${flagName}'`);
|
||||
|
||||
try {
|
||||
// Step 1: Enable flag in Environment.ts
|
||||
enableFlagInEnvironment(flagName);
|
||||
|
||||
// Step 2: Run tests to find failures
|
||||
const {output} = runTests();
|
||||
const failingTests = parseFailingTests(output);
|
||||
|
||||
console.log(`\nFound ${failingTests.length} failing tests`);
|
||||
|
||||
if (failingTests.length === 0) {
|
||||
console.log('No failing tests! Feature flag enabled successfully.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Step 3: Add pragma to each failing fixture
|
||||
console.log(`\nAdding '@${flagName}:false' pragma to failing fixtures...`);
|
||||
|
||||
const notFound = [];
|
||||
let notFoundCount = 0;
|
||||
|
||||
for (const testName of failingTests) {
|
||||
const fixturePath = findFixtureFile(testName);
|
||||
|
||||
if (!fixturePath) {
|
||||
console.warn(`Could not find fixture file for: ${testName}`);
|
||||
notFound.push(fixturePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
const updated = addPragmaToFixture(fixturePath, flagName);
|
||||
if (updated) {
|
||||
updatedCount++;
|
||||
console.log(` Updated: ${testName}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`\nSummary: Updated ${updatedCount} fixtures, ${notFoundCount} not found`
|
||||
);
|
||||
|
||||
if (notFoundCount.length !== 0) {
|
||||
console.error(
|
||||
'\nFailed to update snapshots, could not find:\n' + notFound.join('\n')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Step 4: Update snapshots
|
||||
if (!updateSnapshots()) {
|
||||
console.error('\nFailed to update snapshots');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Step 5: Verify all tests pass
|
||||
if (!verifyAllTestsPass()) {
|
||||
console.error('\nVerification failed: Some tests are still failing');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\nSuccess! Feature flag enabled and all tests passing.');
|
||||
console.log(`\nSummary:`);
|
||||
console.log(` - Enabled "${flagName}" in Environment.ts`);
|
||||
console.log(` - Updated ${updatedCount} fixture files with pragma`);
|
||||
console.log(` - All tests passing`);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('\nFatal error:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
main();
|
||||
@@ -22,7 +22,7 @@ import {isDeepStrictEqual} from 'util';
|
||||
import type {ParseResult} from '@babel/parser';
|
||||
|
||||
const COMPILER_OPTIONS: PluginOptions = {
|
||||
noEmit: true,
|
||||
outputMode: 'lint',
|
||||
panicThreshold: 'none',
|
||||
// Don't emit errors on Flow suppressions--Flow already gave a signal
|
||||
flowSuppressions: false,
|
||||
|
||||
15
packages/react-client/src/ReactFlightClient.js
vendored
15
packages/react-client/src/ReactFlightClient.js
vendored
@@ -813,6 +813,12 @@ 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
|
||||
@@ -3075,7 +3081,6 @@ 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;
|
||||
|
||||
@@ -3375,6 +3380,14 @@ 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);
|
||||
|
||||
@@ -3884,4 +3884,19 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user