Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49afd49939 | ||
|
|
724b324b96 | ||
|
|
45a6532a08 | ||
|
|
8dba9311e5 | ||
|
|
2d98b45d92 | ||
|
|
2ba7b07ce1 | ||
|
|
a96a0f3903 | ||
|
|
02a8811864 | ||
|
|
379a083b9a | ||
|
|
534bed5fa7 | ||
|
|
db06f6b751 | ||
|
|
9433fe357a | ||
|
|
0032b2a3ee | ||
|
|
14c50e344c | ||
|
|
f1222f7652 |
@@ -33,9 +33,7 @@ import {findContextIdentifiers} from '../HIR/FindContextIdentifiers';
|
||||
import {
|
||||
analyseFunctions,
|
||||
dropManualMemoization,
|
||||
inferMutableRanges,
|
||||
inferReactivePlaces,
|
||||
inferReferenceEffects,
|
||||
inlineImmediatelyInvokedFunctionExpressions,
|
||||
inferEffectDependencies,
|
||||
} from '../Inference';
|
||||
@@ -100,7 +98,6 @@ import {outlineJSX} from '../Optimization/OutlineJsx';
|
||||
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
|
||||
import {transformFire} from '../Transform';
|
||||
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
|
||||
import {CompilerError} from '..';
|
||||
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
|
||||
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
|
||||
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
|
||||
@@ -229,26 +226,12 @@ function runWithEnvironment(
|
||||
analyseFunctions(hir);
|
||||
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
|
||||
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
const fnEffectErrors = inferReferenceEffects(hir);
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (fnEffectErrors.length > 0) {
|
||||
CompilerError.throw(fnEffectErrors[0]);
|
||||
}
|
||||
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
|
||||
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
|
||||
} else {
|
||||
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
|
||||
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
// Note: Has to come after infer reference effects because "dead" code may still affect inference
|
||||
@@ -263,20 +246,15 @@ function runWithEnvironment(
|
||||
pruneMaybeThrows(hir);
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
inferMutableRanges(hir);
|
||||
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
|
||||
} else {
|
||||
const mutabilityAliasingErrors = inferMutationAliasingRanges(hir, {
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, {
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingRangeErrors.isErr()) {
|
||||
throw mutabilityAliasingRangeErrors.unwrapErr();
|
||||
}
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
@@ -308,12 +286,7 @@ function runWithEnvironment(
|
||||
validateNoImpureFunctionsInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (
|
||||
env.config.validateNoFreezingKnownMutableFunctions ||
|
||||
env.config.enableNewMutationAliasingModel
|
||||
) {
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
}
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
}
|
||||
|
||||
inferReactivePlaces(hir);
|
||||
|
||||
@@ -250,11 +250,6 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
|
||||
|
||||
/**
|
||||
* Enable a new model for mutability and aliasing inference
|
||||
*/
|
||||
enableNewMutationAliasingModel: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Enables inference of optional dependency chains. Without this flag
|
||||
* a property chain such as `props?.items?.foo` will infer as a dep on
|
||||
|
||||
@@ -50,7 +50,7 @@ export type AliasingEffect =
|
||||
/**
|
||||
* Mutate the value and any direct aliases (not captures). Errors if the value is not mutable.
|
||||
*/
|
||||
| {kind: 'Mutate'; value: Place}
|
||||
| {kind: 'Mutate'; value: Place; reason?: MutationReason | null}
|
||||
/**
|
||||
* Mutate the value and any direct aliases (not captures), but only if the value is known mutable.
|
||||
* This should be rare.
|
||||
@@ -174,6 +174,8 @@ export type AliasingEffect =
|
||||
place: Place;
|
||||
};
|
||||
|
||||
export type MutationReason = {kind: 'AssignCurrentProperty'};
|
||||
|
||||
export function hashEffect(effect: AliasingEffect): string {
|
||||
switch (effect.kind) {
|
||||
case 'Apply': {
|
||||
|
||||
@@ -6,20 +6,10 @@
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
LoweredFunction,
|
||||
isRefOrRefValue,
|
||||
makeInstructionId,
|
||||
} from '../HIR';
|
||||
import {Effect, HIRFunction, IdentifierId, makeInstructionId} from '../HIR';
|
||||
import {deadCodeElimination} from '../Optimization';
|
||||
import {inferReactiveScopeVariables} from '../ReactiveScopes';
|
||||
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
|
||||
import {inferMutableRanges} from './InferMutableRanges';
|
||||
import inferReferenceEffects from './InferReferenceEffects';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
|
||||
@@ -30,12 +20,7 @@ export default function analyseFunctions(func: HIRFunction): void {
|
||||
switch (instr.value.kind) {
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
if (!func.env.config.enableNewMutationAliasingModel) {
|
||||
lower(instr.value.loweredFunc.func);
|
||||
infer(instr.value.loweredFunc);
|
||||
} else {
|
||||
lowerWithMutationAliasing(instr.value.loweredFunc.func);
|
||||
}
|
||||
lowerWithMutationAliasing(instr.value.loweredFunc.func);
|
||||
|
||||
/**
|
||||
* Reset mutable range for outer inferReferenceEffects
|
||||
@@ -140,58 +125,3 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
value: fn,
|
||||
});
|
||||
}
|
||||
|
||||
function lower(func: HIRFunction): void {
|
||||
analyseFunctions(func);
|
||||
inferReferenceEffects(func, {isFunctionExpression: true});
|
||||
deadCodeElimination(func);
|
||||
inferMutableRanges(func);
|
||||
rewriteInstructionKindsBasedOnReassignment(func);
|
||||
inferReactiveScopeVariables(func);
|
||||
func.env.logger?.debugLogIRs?.({
|
||||
kind: 'hir',
|
||||
name: 'AnalyseFunction (inner)',
|
||||
value: func,
|
||||
});
|
||||
}
|
||||
|
||||
function infer(loweredFunc: LoweredFunction): void {
|
||||
for (const operand of loweredFunc.func.context) {
|
||||
const identifier = operand.identifier;
|
||||
CompilerError.invariant(operand.effect === Effect.Unknown, {
|
||||
reason:
|
||||
'[AnalyseFunctions] Expected Function context effects to not have been set',
|
||||
loc: operand.loc,
|
||||
});
|
||||
if (isRefOrRefValue(identifier)) {
|
||||
/*
|
||||
* TODO: this is a hack to ensure we treat functions which reference refs
|
||||
* as having a capture and therefore being considered mutable. this ensures
|
||||
* the function gets a mutable range which accounts for anywhere that it
|
||||
* could be called, and allows us to help ensure it isn't called during
|
||||
* render
|
||||
*/
|
||||
operand.effect = Effect.Capture;
|
||||
} else if (isMutatedOrReassigned(identifier)) {
|
||||
/**
|
||||
* Reflects direct reassignments, PropertyStores, and ConditionallyMutate
|
||||
* (directly or through maybe-aliases)
|
||||
*/
|
||||
operand.effect = Effect.Capture;
|
||||
} else {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isMutatedOrReassigned(id: Identifier): boolean {
|
||||
/*
|
||||
* This check checks for mutation and reassingnment, so the usual check for
|
||||
* mutation (ie, `mutableRange.end - mutableRange.start > 1`) isn't quite
|
||||
* enough.
|
||||
*
|
||||
* We need to track re-assignments in context refs as we need to reflect the
|
||||
* re-assignment back to the captured refs.
|
||||
*/
|
||||
return id.mutableRange.end > id.mutableRange.start;
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
isMutableEffect,
|
||||
isRefOrRefLikeMutableType,
|
||||
makeInstructionId,
|
||||
} from '../HIR/HIR';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
/**
|
||||
* If a function captures a mutable value but never gets called, we don't infer a
|
||||
* mutable range for that function. This means that we also don't alias the function
|
||||
* with its mutable captures.
|
||||
*
|
||||
* This case is tricky, because we don't generally know for sure what is a mutation
|
||||
* and what may just be a normal function call. For example:
|
||||
*
|
||||
* ```
|
||||
* hook useFoo() {
|
||||
* const x = makeObject();
|
||||
* return () => {
|
||||
* return readObject(x); // could be a mutation!
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* If we pessimistically assume that all such cases are mutations, we'd have to group
|
||||
* lots of memo scopes together unnecessarily. However, if there is definitely a mutation:
|
||||
*
|
||||
* ```
|
||||
* hook useFoo(createEntryForKey) {
|
||||
* const cache = new WeakMap();
|
||||
* return (key) => {
|
||||
* let entry = cache.get(key);
|
||||
* if (entry == null) {
|
||||
* entry = createEntryForKey(key);
|
||||
* cache.set(key, entry); // known mutation!
|
||||
* }
|
||||
* return entry;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Then we have to ensure that the function and its mutable captures alias together and
|
||||
* end up in the same scope. However, aliasing together isn't enough if the function
|
||||
* and operands all have empty mutable ranges (end = start + 1).
|
||||
*
|
||||
* This pass finds function expressions and object methods that have an empty mutable range
|
||||
* and known-mutable operands which also don't have a mutable range, and ensures that the
|
||||
* function and those operands are aliased together *and* that their ranges are updated to
|
||||
* end after the function expression. This is sufficient to ensure that a reactive scope is
|
||||
* created for the alias set.
|
||||
*/
|
||||
export function inferAliasForUncalledFunctions(
|
||||
fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
instrs: for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
if (
|
||||
value.kind !== 'ObjectMethod' &&
|
||||
value.kind !== 'FunctionExpression'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* If the function is known to be mutated, we will have
|
||||
* already aliased any mutable operands with it
|
||||
*/
|
||||
const range = lvalue.identifier.mutableRange;
|
||||
if (range.end > range.start + 1) {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* If the function already has operands with an active mutable range,
|
||||
* then we don't need to do anything — the function will have already
|
||||
* been visited and included in some mutable alias set. This case can
|
||||
* also occur due to visiting the same function in an earlier iteration
|
||||
* of the outer fixpoint loop.
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (isMutable(instr, operand)) {
|
||||
continue instrs;
|
||||
}
|
||||
}
|
||||
const operands: Set<Identifier> = new Set();
|
||||
for (const effect of value.loweredFunc.func.effects ?? []) {
|
||||
if (effect.kind !== 'ContextMutation') {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* We're looking for known-mutations only, so we look at the effects
|
||||
* rather than function context
|
||||
*/
|
||||
if (effect.effect === Effect.Store || effect.effect === Effect.Mutate) {
|
||||
for (const operand of effect.places) {
|
||||
/*
|
||||
* It's possible that function effect analysis thinks there was a context mutation,
|
||||
* but then InferReferenceEffects figures out some operands are globals and therefore
|
||||
* creates a non-mutable effect for those operands.
|
||||
* We should change InferReferenceEffects to swap the ContextMutation for a global
|
||||
* mutation in that case, but for now we just filter them out here
|
||||
*/
|
||||
if (
|
||||
isMutableEffect(operand.effect, operand.loc) &&
|
||||
!isRefOrRefLikeMutableType(operand.identifier.type)
|
||||
) {
|
||||
operands.add(operand.identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (operands.size !== 0) {
|
||||
operands.add(lvalue.identifier);
|
||||
aliases.union([...operands]);
|
||||
// Update mutable ranges, if the ranges are empty then a reactive scope isn't created
|
||||
for (const operand of operands) {
|
||||
operand.mutableRange.end = makeInstructionId(instr.id + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
Instruction,
|
||||
isPrimitiveType,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export type AliasSet = Set<Identifier>;
|
||||
|
||||
export function inferAliases(func: HIRFunction): DisjointSet<Identifier> {
|
||||
const aliases = new DisjointSet<Identifier>();
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
inferInstr(instr, aliases);
|
||||
}
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
|
||||
function inferInstr(
|
||||
instr: Instruction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
const {lvalue, value: instrValue} = instr;
|
||||
let alias: Place | null = null;
|
||||
switch (instrValue.kind) {
|
||||
case 'LoadLocal':
|
||||
case 'LoadContext': {
|
||||
if (isPrimitiveType(instrValue.place.identifier)) {
|
||||
return;
|
||||
}
|
||||
alias = instrValue.place;
|
||||
break;
|
||||
}
|
||||
case 'StoreLocal':
|
||||
case 'StoreContext': {
|
||||
alias = instrValue.value;
|
||||
break;
|
||||
}
|
||||
case 'Destructure': {
|
||||
alias = instrValue.value;
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
case 'PropertyLoad': {
|
||||
alias = instrValue.object;
|
||||
break;
|
||||
}
|
||||
case 'TypeCastExpression': {
|
||||
alias = instrValue.value;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
aliases.union([lvalue.identifier, alias.identifier]);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {HIRFunction, Identifier} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export function inferAliasForPhis(
|
||||
func: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
const isPhiMutatedAfterCreation: boolean =
|
||||
phi.place.identifier.mutableRange.end >
|
||||
(block.instructions.at(0)?.id ?? block.terminal.id);
|
||||
if (isPhiMutatedAfterCreation) {
|
||||
for (const [, operand] of phi.operands) {
|
||||
aliases.union([phi.place.identifier, operand.identifier]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
} from '../HIR/visitors';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export function inferAliasForStores(
|
||||
func: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
const {value, lvalue} = instr;
|
||||
const isStore =
|
||||
lvalue.effect === Effect.Store ||
|
||||
/*
|
||||
* Some typed functions annotate callees or arguments
|
||||
* as Effect.Store.
|
||||
*/
|
||||
![...eachInstructionValueOperand(value)].every(
|
||||
operand => operand.effect !== Effect.Store,
|
||||
);
|
||||
|
||||
if (!isStore) {
|
||||
continue;
|
||||
}
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
maybeAlias(aliases, lvalue, operand, instr.id);
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (
|
||||
operand.effect === Effect.Capture ||
|
||||
operand.effect === Effect.Store
|
||||
) {
|
||||
maybeAlias(aliases, lvalue, operand, instr.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function maybeAlias(
|
||||
aliases: DisjointSet<Identifier>,
|
||||
lvalue: Place,
|
||||
rvalue: Place,
|
||||
id: InstructionId,
|
||||
): void {
|
||||
if (
|
||||
lvalue.identifier.mutableRange.end > id + 1 ||
|
||||
rvalue.identifier.mutableRange.end > id
|
||||
) {
|
||||
aliases.union([lvalue.identifier, rvalue.identifier]);
|
||||
}
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerErrorDetailOptions,
|
||||
ErrorSeverity,
|
||||
ValueKind,
|
||||
} from '..';
|
||||
import {
|
||||
AbstractValue,
|
||||
BasicBlock,
|
||||
Effect,
|
||||
Environment,
|
||||
FunctionEffect,
|
||||
Instruction,
|
||||
InstructionValue,
|
||||
Place,
|
||||
ValueReason,
|
||||
getHookKind,
|
||||
isRefOrRefValue,
|
||||
} from '../HIR';
|
||||
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
interface State {
|
||||
kind(place: Place): AbstractValue;
|
||||
values(place: Place): Array<InstructionValue>;
|
||||
isDefined(place: Place): boolean;
|
||||
}
|
||||
|
||||
function inferOperandEffect(state: State, place: Place): null | FunctionEffect {
|
||||
const value = state.kind(place);
|
||||
CompilerError.invariant(value != null, {
|
||||
reason: 'Expected operand to have a kind',
|
||||
loc: null,
|
||||
});
|
||||
|
||||
switch (place.effect) {
|
||||
case Effect.Store:
|
||||
case Effect.Mutate: {
|
||||
if (isRefOrRefValue(place.identifier)) {
|
||||
break;
|
||||
} else if (value.kind === ValueKind.Context) {
|
||||
CompilerError.invariant(value.context.size > 0, {
|
||||
reason:
|
||||
"[InferFunctionEffects] Expected Context-kind value's capture list to be non-empty.",
|
||||
loc: place.loc,
|
||||
});
|
||||
return {
|
||||
kind: 'ContextMutation',
|
||||
loc: place.loc,
|
||||
effect: place.effect,
|
||||
places: value.context,
|
||||
};
|
||||
} else if (
|
||||
value.kind !== ValueKind.Mutable &&
|
||||
// We ignore mutations of primitives since this is not a React-specific problem
|
||||
value.kind !== ValueKind.Primitive
|
||||
) {
|
||||
let reason = getWriteErrorReason(value);
|
||||
return {
|
||||
kind:
|
||||
value.reason.size === 1 && value.reason.has(ValueReason.Global)
|
||||
? 'GlobalMutation'
|
||||
: 'ReactMutation',
|
||||
error: {
|
||||
reason,
|
||||
description:
|
||||
place.identifier.name !== null &&
|
||||
place.identifier.name.kind === 'named'
|
||||
? `Found mutation of \`${place.identifier.name.value}\``
|
||||
: null,
|
||||
loc: place.loc,
|
||||
suggestions: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function inheritFunctionEffects(
|
||||
state: State,
|
||||
place: Place,
|
||||
): Array<FunctionEffect> {
|
||||
const effects = inferFunctionInstrEffects(state, place);
|
||||
|
||||
return effects
|
||||
.flatMap(effect => {
|
||||
if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') {
|
||||
return [effect];
|
||||
} else {
|
||||
const effects: Array<FunctionEffect | null> = [];
|
||||
CompilerError.invariant(effect.kind === 'ContextMutation', {
|
||||
reason: 'Expected ContextMutation',
|
||||
loc: null,
|
||||
});
|
||||
/**
|
||||
* Contextual effects need to be replayed against the current inference
|
||||
* state, which may know more about the value to which the effect applied.
|
||||
* The main cases are:
|
||||
* 1. The mutated context value is _still_ a context value in the current scope,
|
||||
* so we have to continue propagating the original context mutation.
|
||||
* 2. The mutated context value is a mutable value in the current scope,
|
||||
* so the context mutation was fine and we can skip propagating the effect.
|
||||
* 3. The mutated context value is an immutable value in the current scope,
|
||||
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
|
||||
* more detailed effect to the current function context.
|
||||
*/
|
||||
for (const place of effect.places) {
|
||||
if (state.isDefined(place)) {
|
||||
const replayedEffect = inferOperandEffect(state, {
|
||||
...place,
|
||||
loc: effect.loc,
|
||||
effect: effect.effect,
|
||||
});
|
||||
if (replayedEffect != null) {
|
||||
if (replayedEffect.kind === 'ContextMutation') {
|
||||
// Case 1, still a context value so propagate the original effect
|
||||
effects.push(effect);
|
||||
} else {
|
||||
// Case 3, immutable value so propagate the more precise effect
|
||||
effects.push(replayedEffect);
|
||||
}
|
||||
} // else case 2, local mutable value so this effect was fine
|
||||
}
|
||||
}
|
||||
return effects;
|
||||
}
|
||||
})
|
||||
.filter((effect): effect is FunctionEffect => effect != null);
|
||||
}
|
||||
|
||||
function inferFunctionInstrEffects(
|
||||
state: State,
|
||||
place: Place,
|
||||
): Array<FunctionEffect> {
|
||||
const effects: Array<FunctionEffect> = [];
|
||||
const instrs = state.values(place);
|
||||
CompilerError.invariant(instrs != null, {
|
||||
reason: 'Expected operand to have instructions',
|
||||
loc: null,
|
||||
});
|
||||
|
||||
for (const instr of instrs) {
|
||||
if (
|
||||
(instr.kind === 'FunctionExpression' || instr.kind === 'ObjectMethod') &&
|
||||
instr.loweredFunc.func.effects != null
|
||||
) {
|
||||
effects.push(...instr.loweredFunc.func.effects);
|
||||
}
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
function operandEffects(
|
||||
state: State,
|
||||
place: Place,
|
||||
filterRenderSafe: boolean,
|
||||
): Array<FunctionEffect> {
|
||||
const functionEffects: Array<FunctionEffect> = [];
|
||||
const effect = inferOperandEffect(state, place);
|
||||
effect && functionEffects.push(effect);
|
||||
functionEffects.push(...inheritFunctionEffects(state, place));
|
||||
if (filterRenderSafe) {
|
||||
return functionEffects.filter(effect => !isEffectSafeOutsideRender(effect));
|
||||
} else {
|
||||
return functionEffects;
|
||||
}
|
||||
}
|
||||
|
||||
export function inferInstructionFunctionEffects(
|
||||
env: Environment,
|
||||
state: State,
|
||||
instr: Instruction,
|
||||
): Array<FunctionEffect> {
|
||||
const functionEffects: Array<FunctionEffect> = [];
|
||||
switch (instr.value.kind) {
|
||||
case 'JsxExpression': {
|
||||
if (instr.value.tag.kind === 'Identifier') {
|
||||
functionEffects.push(...operandEffects(state, instr.value.tag, false));
|
||||
}
|
||||
instr.value.children?.forEach(child =>
|
||||
functionEffects.push(...operandEffects(state, child, false)),
|
||||
);
|
||||
for (const attr of instr.value.props) {
|
||||
if (attr.kind === 'JsxSpreadAttribute') {
|
||||
functionEffects.push(...operandEffects(state, attr.argument, false));
|
||||
} else {
|
||||
functionEffects.push(...operandEffects(state, attr.place, true));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
/**
|
||||
* If this function references other functions, propagate the referenced function's
|
||||
* effects to this function.
|
||||
*
|
||||
* ```
|
||||
* let f = () => global = true;
|
||||
* let g = () => f();
|
||||
* g();
|
||||
* ```
|
||||
*
|
||||
* In this example, because `g` references `f`, we propagate the GlobalMutation from
|
||||
* `f` to `g`. Thus, referencing `g` in `g()` will evaluate the GlobalMutation in the outer
|
||||
* function effect context and report an error. But if instead we do:
|
||||
*
|
||||
* ```
|
||||
* let f = () => global = true;
|
||||
* let g = () => f();
|
||||
* useEffect(() => g(), [g])
|
||||
* ```
|
||||
*
|
||||
* Now `g`'s effects will be discarded since they're in a useEffect.
|
||||
*/
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
instr.value.loweredFunc.func.effects ??= [];
|
||||
instr.value.loweredFunc.func.effects.push(
|
||||
...inferFunctionInstrEffects(state, operand),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MethodCall':
|
||||
case 'CallExpression': {
|
||||
let callee;
|
||||
if (instr.value.kind === 'MethodCall') {
|
||||
callee = instr.value.property;
|
||||
functionEffects.push(
|
||||
...operandEffects(state, instr.value.receiver, false),
|
||||
);
|
||||
} else {
|
||||
callee = instr.value.callee;
|
||||
}
|
||||
functionEffects.push(...operandEffects(state, callee, false));
|
||||
let isHook = getHookKind(env, callee.identifier) != null;
|
||||
for (const arg of instr.value.args) {
|
||||
const place = arg.kind === 'Identifier' ? arg : arg.place;
|
||||
/*
|
||||
* Join the effects of the argument with the effects of the enclosing function,
|
||||
* unless the we're detecting a global mutation inside a useEffect hook
|
||||
*/
|
||||
functionEffects.push(...operandEffects(state, place, isHook));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'StartMemoize':
|
||||
case 'FinishMemoize':
|
||||
case 'LoadLocal':
|
||||
case 'StoreLocal': {
|
||||
break;
|
||||
}
|
||||
case 'StoreGlobal': {
|
||||
functionEffects.push({
|
||||
kind: 'GlobalMutation',
|
||||
error: {
|
||||
reason:
|
||||
'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
|
||||
loc: instr.loc,
|
||||
suggestions: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
functionEffects.push(...operandEffects(state, operand, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
return functionEffects;
|
||||
}
|
||||
|
||||
export function inferTerminalFunctionEffects(
|
||||
state: State,
|
||||
block: BasicBlock,
|
||||
): Array<FunctionEffect> {
|
||||
const functionEffects: Array<FunctionEffect> = [];
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
functionEffects.push(...operandEffects(state, operand, true));
|
||||
}
|
||||
return functionEffects;
|
||||
}
|
||||
|
||||
export function transformFunctionEffectErrors(
|
||||
functionEffects: Array<FunctionEffect>,
|
||||
): Array<CompilerErrorDetailOptions> {
|
||||
return functionEffects.map(eff => {
|
||||
switch (eff.kind) {
|
||||
case 'ReactMutation':
|
||||
case 'GlobalMutation': {
|
||||
return eff.error;
|
||||
}
|
||||
case 'ContextMutation': {
|
||||
return {
|
||||
severity: ErrorSeverity.Invariant,
|
||||
reason: `Unexpected ContextMutation in top-level function effects`,
|
||||
loc: eff.loc,
|
||||
};
|
||||
}
|
||||
default:
|
||||
assertExhaustive(
|
||||
eff,
|
||||
`Unexpected function effect kind \`${(eff as any).kind}\``,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
|
||||
return effect.kind === 'GlobalMutation';
|
||||
}
|
||||
|
||||
export function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
if (abstractValue.reason.has(ValueReason.Global)) {
|
||||
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
|
||||
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
|
||||
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
|
||||
} else if (abstractValue.reason.has(ValueReason.Context)) {
|
||||
return `Modifying a value returned from 'useContext()' is not allowed.`;
|
||||
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
|
||||
return 'Modifying a value returned from a function whose return value should not be mutated';
|
||||
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
|
||||
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
|
||||
} else if (abstractValue.reason.has(ValueReason.State)) {
|
||||
return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
|
||||
return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.Effect)) {
|
||||
return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
|
||||
return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
|
||||
return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
|
||||
} else {
|
||||
return 'This modifies a variable that React considers immutable';
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
isArrayType,
|
||||
isMapType,
|
||||
isRefOrRefValue,
|
||||
isSetType,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import {printPlace} from '../HIR/PrintHIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
/*
|
||||
* For each usage of a value in the given function, determines if the usage
|
||||
* may be succeeded by a mutable usage of that same value and if so updates
|
||||
* the usage to be mutable.
|
||||
*
|
||||
* Stated differently, this inference ensures that inferred capabilities of
|
||||
* each reference are as follows:
|
||||
* - freeze: the value is frozen at this point
|
||||
* - readonly: the value is not modified at this point *or any subsequent
|
||||
* point*
|
||||
* - mutable: the value is modified at this point *or some subsequent point*.
|
||||
*
|
||||
* Note that this refines the capabilities inferered by InferReferenceCapability,
|
||||
* which looks at individual references and not the lifetime of a value's mutability.
|
||||
*
|
||||
* == Algorithm
|
||||
*
|
||||
* TODO:
|
||||
* 1. Forward data-flow analysis to determine aliasing. Unlike InferReferenceCapability
|
||||
* which only tracks aliasing of top-level variables (`y = x`), this analysis needs
|
||||
* to know if a value is aliased anywhere (`y.x = x`). The forward data flow tracks
|
||||
* all possible locations which may have aliased a value. The concrete result is
|
||||
* a mapping of each Place to the set of possibly-mutable values it may alias.
|
||||
*
|
||||
* ```
|
||||
* const x = []; // {x: v0; v0: mutable []}
|
||||
* const y = {}; // {x: v0, y: v1; v0: mutable [], v1: mutable []}
|
||||
* y.x = x; // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
|
||||
* read(x); // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
|
||||
* mutate(y); // can infer that y mutates v0 and v1
|
||||
* ```
|
||||
*
|
||||
* DONE:
|
||||
* 2. Forward data-flow analysis to compute mutability liveness. Walk forwards over
|
||||
* the CFG and track which values are mutated in a successor.
|
||||
*
|
||||
* ```
|
||||
* mutate(y); // mutable y => v0, v1 mutated
|
||||
* read(x); // x maps to v0, v1, those are in the mutated-later set, so x is mutable here
|
||||
* ...
|
||||
* ```
|
||||
*/
|
||||
|
||||
function infer(place: Place, instrId: InstructionId): void {
|
||||
if (!isRefOrRefValue(place.identifier)) {
|
||||
place.identifier.mutableRange.end = makeInstructionId(instrId + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function inferPlace(
|
||||
place: Place,
|
||||
instrId: InstructionId,
|
||||
inferMutableRangeForStores: boolean,
|
||||
): void {
|
||||
switch (place.effect) {
|
||||
case Effect.Unknown: {
|
||||
throw new Error(`Found an unknown place ${printPlace(place)}}!`);
|
||||
}
|
||||
case Effect.Capture:
|
||||
case Effect.Read:
|
||||
case Effect.Freeze:
|
||||
return;
|
||||
case Effect.Store:
|
||||
if (inferMutableRangeForStores) {
|
||||
infer(place, instrId);
|
||||
}
|
||||
return;
|
||||
case Effect.ConditionallyMutateIterator: {
|
||||
const identifier = place.identifier;
|
||||
if (
|
||||
!isArrayType(identifier) &&
|
||||
!isSetType(identifier) &&
|
||||
!isMapType(identifier)
|
||||
) {
|
||||
infer(place, instrId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.Mutate: {
|
||||
infer(place, instrId);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
assertExhaustive(place.effect, `Unexpected ${printPlace(place)} effect`);
|
||||
}
|
||||
}
|
||||
|
||||
export function inferMutableLifetimes(
|
||||
func: HIRFunction,
|
||||
inferMutableRangeForStores: boolean,
|
||||
): void {
|
||||
/*
|
||||
* Context variables only appear to mutate where they are assigned, but we need
|
||||
* to force their range to start at their declaration. Track the declaring instruction
|
||||
* id so that the ranges can be extended if/when they are reassigned
|
||||
*/
|
||||
const contextVariableDeclarationInstructions = new Map<
|
||||
Identifier,
|
||||
InstructionId
|
||||
>();
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
const isPhiMutatedAfterCreation: boolean =
|
||||
phi.place.identifier.mutableRange.end >
|
||||
(block.instructions.at(0)?.id ?? block.terminal.id);
|
||||
if (
|
||||
inferMutableRangeForStores &&
|
||||
isPhiMutatedAfterCreation &&
|
||||
phi.place.identifier.mutableRange.start === 0
|
||||
) {
|
||||
for (const [, operand] of phi.operands) {
|
||||
if (phi.place.identifier.mutableRange.start === 0) {
|
||||
phi.place.identifier.mutableRange.start =
|
||||
operand.identifier.mutableRange.start;
|
||||
} else {
|
||||
phi.place.identifier.mutableRange.start = makeInstructionId(
|
||||
Math.min(
|
||||
phi.place.identifier.mutableRange.start,
|
||||
operand.identifier.mutableRange.start,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
const lvalueId = operand.identifier;
|
||||
|
||||
/*
|
||||
* lvalue start being mutable when they're initially assigned a
|
||||
* value.
|
||||
*/
|
||||
lvalueId.mutableRange.start = instr.id;
|
||||
|
||||
/*
|
||||
* Let's be optimistic and assume this lvalue is not mutable by
|
||||
* default.
|
||||
*/
|
||||
lvalueId.mutableRange.end = makeInstructionId(instr.id + 1);
|
||||
}
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
inferPlace(operand, instr.id, inferMutableRangeForStores);
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'DeclareContext' ||
|
||||
(instr.value.kind === 'StoreContext' &&
|
||||
instr.value.lvalue.kind !== InstructionKind.Reassign &&
|
||||
!contextVariableDeclarationInstructions.has(
|
||||
instr.value.lvalue.place.identifier,
|
||||
))
|
||||
) {
|
||||
/**
|
||||
* Save declarations of context variables if they hasn't already been
|
||||
* declared (due to hoisted declarations).
|
||||
*/
|
||||
contextVariableDeclarationInstructions.set(
|
||||
instr.value.lvalue.place.identifier,
|
||||
instr.id,
|
||||
);
|
||||
} else if (instr.value.kind === 'StoreContext') {
|
||||
/*
|
||||
* Else this is a reassignment, extend the range from the declaration (if present).
|
||||
* Note that declarations may not be present for context variables that are reassigned
|
||||
* within a function expression before (or without) a read of the same variable
|
||||
*/
|
||||
const declaration = contextVariableDeclarationInstructions.get(
|
||||
instr.value.lvalue.place.identifier,
|
||||
);
|
||||
if (
|
||||
declaration != null &&
|
||||
!isRefOrRefValue(instr.value.lvalue.place.identifier)
|
||||
) {
|
||||
const range = instr.value.lvalue.place.identifier.mutableRange;
|
||||
if (range.start === 0) {
|
||||
range.start = declaration;
|
||||
} else {
|
||||
range.start = makeInstructionId(Math.min(range.start, declaration));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
inferPlace(operand, block.terminal.id, inferMutableRangeForStores);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {HIRFunction, Identifier} from '../HIR/HIR';
|
||||
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
|
||||
import {inferAliases} from './InferAlias';
|
||||
import {inferAliasForPhis} from './InferAliasForPhis';
|
||||
import {inferAliasForStores} from './InferAliasForStores';
|
||||
import {inferMutableLifetimes} from './InferMutableLifetimes';
|
||||
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
|
||||
import {inferTryCatchAliases} from './InferTryCatchAliases';
|
||||
|
||||
export function inferMutableRanges(ir: HIRFunction): void {
|
||||
// Infer mutable ranges for non fields
|
||||
inferMutableLifetimes(ir, false);
|
||||
|
||||
// Calculate aliases
|
||||
const aliases = inferAliases(ir);
|
||||
/*
|
||||
* Calculate aliases for try/catch, where any value created
|
||||
* in the try block could be aliased to the catch param
|
||||
*/
|
||||
inferTryCatchAliases(ir, aliases);
|
||||
|
||||
/*
|
||||
* Eagerly canonicalize so that if nothing changes we can bail out
|
||||
* after a single iteration
|
||||
*/
|
||||
let prevAliases: Map<Identifier, Identifier> = aliases.canonicalize();
|
||||
while (true) {
|
||||
// Infer mutable ranges for aliases that are not fields
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
|
||||
// Update aliasing information of fields
|
||||
inferAliasForStores(ir, aliases);
|
||||
|
||||
// Update aliasing information of phis
|
||||
inferAliasForPhis(ir, aliases);
|
||||
|
||||
const nextAliases = aliases.canonicalize();
|
||||
if (areEqualMaps(prevAliases, nextAliases)) {
|
||||
break;
|
||||
}
|
||||
prevAliases = nextAliases;
|
||||
}
|
||||
|
||||
// Re-infer mutable ranges for all values
|
||||
inferMutableLifetimes(ir, true);
|
||||
|
||||
/**
|
||||
* The second inferMutableLifetimes() call updates mutable ranges
|
||||
* of values to account for Store effects. Now we need to update
|
||||
* all aliases of such values to extend their ranges as well. Note
|
||||
* that the store only mutates the the directly aliased value and
|
||||
* not any of its inner captured references. For example:
|
||||
*
|
||||
* ```
|
||||
* let y;
|
||||
* if (cond) {
|
||||
* y = [];
|
||||
* } else {
|
||||
* y = [{}];
|
||||
* }
|
||||
* y.push(z);
|
||||
* ```
|
||||
*
|
||||
* The Store effect from the `y.push` modifies the values that `y`
|
||||
* directly aliases - the two arrays from the if/else branches -
|
||||
* but does not modify values that `y` "contains" such as the
|
||||
* object literal or `z`.
|
||||
*/
|
||||
prevAliases = aliases.canonicalize();
|
||||
while (true) {
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
inferAliasForPhis(ir, aliases);
|
||||
inferAliasForUncalledFunctions(ir, aliases);
|
||||
const nextAliases = aliases.canonicalize();
|
||||
if (areEqualMaps(prevAliases, nextAliases)) {
|
||||
break;
|
||||
}
|
||||
prevAliases = nextAliases;
|
||||
}
|
||||
}
|
||||
|
||||
function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
for (const [key, value] of a) {
|
||||
if (!b.has(key)) {
|
||||
return false;
|
||||
}
|
||||
if (b.get(key) !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
isRefOrRefValue,
|
||||
} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export function inferMutableRangesForAlias(
|
||||
_fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
const aliasSets = aliases.buildSets();
|
||||
for (const aliasSet of aliasSets) {
|
||||
/*
|
||||
* Update mutableRange.end only if the identifiers have actually been
|
||||
* mutated.
|
||||
*/
|
||||
const mutatingIdentifiers = [...aliasSet].filter(
|
||||
id =>
|
||||
id.mutableRange.end - id.mutableRange.start > 1 && !isRefOrRefValue(id),
|
||||
);
|
||||
|
||||
if (mutatingIdentifiers.length > 0) {
|
||||
// Find final instruction which mutates this alias set.
|
||||
let lastMutatingInstructionId = 0;
|
||||
for (const id of mutatingIdentifiers) {
|
||||
if (id.mutableRange.end > lastMutatingInstructionId) {
|
||||
lastMutatingInstructionId = id.mutableRange.end;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Update mutableRange.end for all aliases in this set ending before the
|
||||
* last mutation.
|
||||
*/
|
||||
for (const alias of aliasSet) {
|
||||
if (
|
||||
alias.mutableRange.end < lastMutatingInstructionId &&
|
||||
!isRefOrRefValue(alias)
|
||||
) {
|
||||
alias.mutableRange.end = lastMutatingInstructionId as InstructionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
DeclarationId,
|
||||
Environment,
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
Hole,
|
||||
IdentifierId,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
Phi,
|
||||
Place,
|
||||
SpreadPattern,
|
||||
Type,
|
||||
ValueReason,
|
||||
} from '../HIR';
|
||||
import {
|
||||
@@ -43,12 +45,6 @@ import {
|
||||
eachTerminalSuccessor,
|
||||
} from '../HIR/visitors';
|
||||
import {Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
getArgumentEffect,
|
||||
getFunctionCallSignature,
|
||||
isKnownMutableEffect,
|
||||
mergeValueKinds,
|
||||
} from './InferReferenceEffects';
|
||||
import {
|
||||
assertExhaustive,
|
||||
getOrInsertDefault,
|
||||
@@ -65,10 +61,14 @@ import {
|
||||
printSourceLocation,
|
||||
} from '../HIR/PrintHIR';
|
||||
import {FunctionSignature} from '../HIR/ObjectShape';
|
||||
import {getWriteErrorReason} from './InferFunctionEffects';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {createTemporaryPlace} from '../HIR/HIRBuilder';
|
||||
import {AliasingEffect, AliasingSignature, hashEffect} from './AliasingEffects';
|
||||
import {
|
||||
AliasingEffect,
|
||||
AliasingSignature,
|
||||
hashEffect,
|
||||
MutationReason,
|
||||
} from './AliasingEffects';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
@@ -445,25 +445,35 @@ function applySignature(
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
const variable =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
});
|
||||
if (
|
||||
effect.kind === 'Mutate' &&
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
}
|
||||
effects.push({
|
||||
kind: 'MutateFrozen',
|
||||
place: effect.value,
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
}),
|
||||
error: diagnostic,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1059,13 +1069,31 @@ function applyEffect(
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
const variable =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
});
|
||||
if (
|
||||
effect.kind === 'Mutate' &&
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
}
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
@@ -1075,15 +1103,7 @@ function applyEffect(
|
||||
? 'MutateFrozen'
|
||||
: 'MutateGlobal',
|
||||
place: effect.value,
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
}),
|
||||
error: diagnostic,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
@@ -1680,7 +1700,15 @@ function computeSignatureForInstruction(
|
||||
}
|
||||
case 'PropertyStore':
|
||||
case 'ComputedStore': {
|
||||
effects.push({kind: 'Mutate', value: value.object});
|
||||
const mutationReason: MutationReason | null =
|
||||
value.kind === 'PropertyStore' && value.property === 'current'
|
||||
? {kind: 'AssignCurrentProperty'}
|
||||
: null;
|
||||
effects.push({
|
||||
kind: 'Mutate',
|
||||
value: value.object,
|
||||
reason: mutationReason,
|
||||
});
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: value.value,
|
||||
@@ -2534,3 +2562,196 @@ export type AbstractValue = {
|
||||
kind: ValueKind;
|
||||
reason: ReadonlySet<ValueReason>;
|
||||
};
|
||||
|
||||
export function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
if (abstractValue.reason.has(ValueReason.Global)) {
|
||||
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
|
||||
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
|
||||
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
|
||||
} else if (abstractValue.reason.has(ValueReason.Context)) {
|
||||
return `Modifying a value returned from 'useContext()' is not allowed.`;
|
||||
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
|
||||
return 'Modifying a value returned from a function whose return value should not be mutated';
|
||||
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
|
||||
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
|
||||
} else if (abstractValue.reason.has(ValueReason.State)) {
|
||||
return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
|
||||
return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.Effect)) {
|
||||
return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
|
||||
return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
|
||||
return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
|
||||
} else {
|
||||
return 'This modifies a variable that React considers immutable';
|
||||
}
|
||||
}
|
||||
|
||||
function getArgumentEffect(
|
||||
signatureEffect: Effect | null,
|
||||
arg: Place | SpreadPattern,
|
||||
): Effect {
|
||||
if (signatureEffect != null) {
|
||||
if (arg.kind === 'Identifier') {
|
||||
return signatureEffect;
|
||||
} else if (
|
||||
signatureEffect === Effect.Mutate ||
|
||||
signatureEffect === Effect.ConditionallyMutate
|
||||
) {
|
||||
return signatureEffect;
|
||||
} else {
|
||||
// see call-spread-argument-mutable-iterator test fixture
|
||||
if (signatureEffect === Effect.Freeze) {
|
||||
CompilerError.throwTodo({
|
||||
reason: 'Support spread syntax for hook arguments',
|
||||
loc: arg.place.loc,
|
||||
});
|
||||
}
|
||||
// effects[i] is Effect.Capture | Effect.Read | Effect.Store
|
||||
return Effect.ConditionallyMutateIterator;
|
||||
}
|
||||
} else {
|
||||
return Effect.ConditionallyMutate;
|
||||
}
|
||||
}
|
||||
|
||||
export function getFunctionCallSignature(
|
||||
env: Environment,
|
||||
type: Type,
|
||||
): FunctionSignature | null {
|
||||
if (type.kind !== 'Function') {
|
||||
return null;
|
||||
}
|
||||
return env.getFunctionSignature(type);
|
||||
}
|
||||
|
||||
export function isKnownMutableEffect(effect: Effect): boolean {
|
||||
switch (effect) {
|
||||
case Effect.Store:
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
return true;
|
||||
}
|
||||
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
loc: GeneratedSource,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
case Effect.Read:
|
||||
case Effect.Capture:
|
||||
case Effect.Freeze: {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(effect, `Unexpected effect \`${effect}\``);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins two values using the following rules:
|
||||
* == Effect Transitions ==
|
||||
*
|
||||
* Freezing an immutable value has not effect:
|
||||
* ┌───────────────┐
|
||||
* │ │
|
||||
* ▼ │ Freeze
|
||||
* ┌──────────────────────────┐ │
|
||||
* │ Immutable │──┘
|
||||
* └──────────────────────────┘
|
||||
*
|
||||
* Freezing a mutable or maybe-frozen value makes it frozen. Freezing a frozen
|
||||
* value has no effect:
|
||||
* ┌───────────────┐
|
||||
* ┌─────────────────────────┐ Freeze │ │
|
||||
* │ MaybeFrozen │────┐ ▼ │ Freeze
|
||||
* └─────────────────────────┘ │ ┌──────────────────────────┐ │
|
||||
* ├────▶│ Frozen │──┘
|
||||
* │ └──────────────────────────┘
|
||||
* ┌─────────────────────────┐ │
|
||||
* │ Mutable │────┘
|
||||
* └─────────────────────────┘
|
||||
*
|
||||
* == Join Lattice ==
|
||||
* - immutable | mutable => mutable
|
||||
* The justification is that immutable and mutable values are different types,
|
||||
* and functions can introspect them to tell the difference (if the argument
|
||||
* is null return early, else if its an object mutate it).
|
||||
* - frozen | mutable => maybe-frozen
|
||||
* Frozen values are indistinguishable from mutable values at runtime, so callers
|
||||
* cannot dynamically avoid mutation of "frozen" values. If a value could be
|
||||
* frozen we have to distinguish it from a mutable value. But it also isn't known
|
||||
* frozen yet, so we distinguish as maybe-frozen.
|
||||
* - immutable | frozen => frozen
|
||||
* This is subtle and falls out of the above rules. If a value could be any of
|
||||
* immutable, mutable, or frozen, then at runtime it could either be a primitive
|
||||
* or a reference type, and callers can't distinguish frozen or not for reference
|
||||
* types. To ensure that any sequence of joins btw those three states yields the
|
||||
* correct maybe-frozen, these two have to produce a frozen value.
|
||||
* - <any> | maybe-frozen => maybe-frozen
|
||||
* - immutable | context => context
|
||||
* - mutable | context => context
|
||||
* - frozen | context => maybe-frozen
|
||||
*
|
||||
* ┌──────────────────────────┐
|
||||
* │ Immutable │───┐
|
||||
* └──────────────────────────┘ │
|
||||
* │ ┌─────────────────────────┐
|
||||
* ├───▶│ Frozen │──┐
|
||||
* ┌──────────────────────────┐ │ └─────────────────────────┘ │
|
||||
* │ Frozen │───┤ │ ┌─────────────────────────┐
|
||||
* └──────────────────────────┘ │ ├─▶│ MaybeFrozen │
|
||||
* │ ┌─────────────────────────┐ │ └─────────────────────────┘
|
||||
* ├───▶│ MaybeFrozen │──┘
|
||||
* ┌──────────────────────────┐ │ └─────────────────────────┘
|
||||
* │ Mutable │───┘
|
||||
* └──────────────────────────┘
|
||||
*/
|
||||
function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
|
||||
if (a === b) {
|
||||
return a;
|
||||
} else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
|
||||
return ValueKind.MaybeFrozen;
|
||||
// after this a and b differ and neither are MaybeFrozen
|
||||
} else if (a === ValueKind.Mutable || b === ValueKind.Mutable) {
|
||||
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
|
||||
// frozen | mutable
|
||||
return ValueKind.MaybeFrozen;
|
||||
} else if (a === ValueKind.Context || b === ValueKind.Context) {
|
||||
// context | mutable
|
||||
return ValueKind.Context;
|
||||
} else {
|
||||
// mutable | immutable
|
||||
return ValueKind.Mutable;
|
||||
}
|
||||
} else if (a === ValueKind.Context || b === ValueKind.Context) {
|
||||
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
|
||||
// frozen | context
|
||||
return ValueKind.MaybeFrozen;
|
||||
} else {
|
||||
// context | immutable
|
||||
return ValueKind.Context;
|
||||
}
|
||||
} else if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
|
||||
return ValueKind.Frozen;
|
||||
} else if (a === ValueKind.Global || b === ValueKind.Global) {
|
||||
return ValueKind.Global;
|
||||
} else {
|
||||
CompilerError.invariant(
|
||||
a === ValueKind.Primitive && b == ValueKind.Primitive,
|
||||
{
|
||||
reason: `Unexpected value kind in mergeValues()`,
|
||||
description: `Found kinds ${a} and ${b}`,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
return ValueKind.Primitive;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {BlockId, HIRFunction, Identifier} from '../HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
/*
|
||||
* Any values created within a try/catch block could be aliased to the try handler.
|
||||
* Our lowering ensures that every instruction within a try block will be lowered into a
|
||||
* basic block ending in a maybe-throw terminal that points to its catch block, so we can
|
||||
* iterate such blocks and alias their instruction lvalues to the handler's param (if present).
|
||||
*/
|
||||
export function inferTryCatchAliases(
|
||||
fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
const handlerParams: Map<BlockId, Identifier> = new Map();
|
||||
for (const [_, block] of fn.body.blocks) {
|
||||
if (
|
||||
block.terminal.kind === 'try' &&
|
||||
block.terminal.handlerBinding !== null
|
||||
) {
|
||||
handlerParams.set(
|
||||
block.terminal.handler,
|
||||
block.terminal.handlerBinding.identifier,
|
||||
);
|
||||
} else if (block.terminal.kind === 'maybe-throw') {
|
||||
const handlerParam = handlerParams.get(block.terminal.handler);
|
||||
if (handlerParam === undefined) {
|
||||
/*
|
||||
* There's no catch clause param, nothing to alias to so
|
||||
* skip this block
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* Otherwise alias all values created in this block to the
|
||||
* catch clause param
|
||||
*/
|
||||
for (const instr of block.instructions) {
|
||||
aliases.union([handlerParam, instr.lvalue.identifier]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
export {default as analyseFunctions} from './AnalyseFunctions';
|
||||
export {dropManualMemoization} from './DropManualMemoization';
|
||||
export {inferMutableRanges} from './InferMutableRanges';
|
||||
export {inferReactivePlaces} from './InferReactivePlaces';
|
||||
export {default as inferReferenceEffects} from './InferReferenceEffects';
|
||||
export {inlineImmediatelyInvokedFunctionExpressions} from './InlineImmediatelyInvokedFunctionExpressions';
|
||||
export {inferEffectDependencies} from './InferEffectDependencies';
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
getHookKind,
|
||||
isMutableEffect,
|
||||
} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
|
||||
import {getPlaceScope, ReactiveScope} from '../HIR/HIR';
|
||||
import {
|
||||
@@ -35,6 +34,7 @@ import {
|
||||
visitReactiveFunction,
|
||||
} from './visitors';
|
||||
import {printPlace} from '../HIR/PrintHIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
|
||||
/*
|
||||
* This pass prunes reactive scopes that are not necessary to bound downstream computation.
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
|
||||
/**
|
||||
* Validates that local variables cannot be reassigned after render.
|
||||
|
||||
@@ -12,21 +12,14 @@ import {
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Place,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
} from '../HIR';
|
||||
import {printInstruction, printPlace} from '../HIR/PrintHIR';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
|
||||
type SetStateCall = {
|
||||
loc: SourceLocation;
|
||||
propsSource: Place | null; // null means state-derived, non-null means props-derived
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
* be performed in render.
|
||||
@@ -54,96 +47,12 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||
const derivedFromProps: Map<IdentifierId, Place> = new Map();
|
||||
|
||||
const errors = new CompilerError();
|
||||
|
||||
if (fn.fnType === 'Hook') {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
derivedFromProps.set(param.identifier.id, param);
|
||||
}
|
||||
}
|
||||
} else if (fn.fnType === 'Component') {
|
||||
const props = fn.params[0];
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
derivedFromProps.set(props.identifier.id, props);
|
||||
}
|
||||
}
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
|
||||
// Track props derivation through instruction effects
|
||||
if (instr.effects != null) {
|
||||
for (const effect of instr.effects) {
|
||||
switch (effect.kind) {
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'MaybeAlias':
|
||||
case 'Capture': {
|
||||
const source = derivedFromProps.get(effect.from.identifier.id);
|
||||
if (source != null) {
|
||||
derivedFromProps.set(effect.into.identifier.id, source);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: figure out why property access off of props does not create an Assign or Alias/Maybe
|
||||
* Alias
|
||||
*
|
||||
* import {useEffect, useState} from 'react'
|
||||
*
|
||||
* function Component(props) {
|
||||
* const [displayValue, setDisplayValue] = useState('');
|
||||
*
|
||||
* useEffect(() => {
|
||||
* const computed = props.prefix + props.value + props.suffix;
|
||||
* ^^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^^
|
||||
* we want to track that these are from props
|
||||
* setDisplayValue(computed);
|
||||
* }, [props.prefix, props.value, props.suffix]);
|
||||
*
|
||||
* return <div>{displayValue}</div>;
|
||||
* }
|
||||
*/
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
for (const [, block] of value.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
if (instr.effects != null) {
|
||||
console.group(printInstruction(instr));
|
||||
for (const effect of instr.effects) {
|
||||
console.log(effect);
|
||||
switch (effect.kind) {
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'MaybeAlias':
|
||||
case 'Capture': {
|
||||
const source = derivedFromProps.get(
|
||||
effect.from.identifier.id,
|
||||
);
|
||||
if (source != null) {
|
||||
derivedFromProps.set(effect.into.identifier.id, source);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [, place] of derivedFromProps) {
|
||||
console.log(printPlace(place));
|
||||
}
|
||||
|
||||
if (value.kind === 'LoadLocal') {
|
||||
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
@@ -180,7 +89,6 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
dependencies,
|
||||
derivedFromProps,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
@@ -196,7 +104,6 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
derivedFromProps: Map<IdentifierId, Place>,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
for (const operand of effectFunction.context) {
|
||||
@@ -204,22 +111,16 @@ function validateEffect(
|
||||
continue;
|
||||
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
|
||||
continue;
|
||||
} else if (derivedFromProps.has(operand.identifier.id)) {
|
||||
continue;
|
||||
} else {
|
||||
// Captured something other than the effect dep or setState
|
||||
console.log('early return 1');
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const dep of effectDeps) {
|
||||
console.log({dep});
|
||||
if (
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) ==
|
||||
null ||
|
||||
derivedFromProps.has(dep) === false
|
||||
null
|
||||
) {
|
||||
console.log('early return 2');
|
||||
// effect dep wasn't actually used in the function
|
||||
return;
|
||||
}
|
||||
@@ -227,18 +128,11 @@ function validateEffect(
|
||||
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
const effectDerivedFromProps: Map<IdentifierId, Place> = new Map();
|
||||
|
||||
for (const dep of effectDeps) {
|
||||
console.log({dep});
|
||||
values.set(dep, [dep]);
|
||||
const propsSource = derivedFromProps.get(dep);
|
||||
if (propsSource != null) {
|
||||
effectDerivedFromProps.set(dep, propsSource);
|
||||
}
|
||||
}
|
||||
|
||||
const setStateCalls: Array<SetStateCall> = [];
|
||||
const setStateLocations: Array<SourceLocation> = [];
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
@@ -248,8 +142,6 @@ function validateEffect(
|
||||
}
|
||||
for (const phi of block.phis) {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
let propsSource: Place | null = null;
|
||||
|
||||
for (const operand of phi.operands.values()) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
@@ -257,18 +149,10 @@ function validateEffect(
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
const source = effectDerivedFromProps.get(operand.identifier.id);
|
||||
if (source != null) {
|
||||
propsSource = source;
|
||||
}
|
||||
}
|
||||
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
if (propsSource != null) {
|
||||
effectDerivedFromProps.set(phi.place.identifier.id, propsSource);
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
switch (instr.value.kind) {
|
||||
@@ -311,16 +195,9 @@ function validateEffect(
|
||||
) {
|
||||
const deps = values.get(instr.value.args[0].identifier.id);
|
||||
if (deps != null && new Set(deps).size === effectDeps.length) {
|
||||
const propsSource = effectDerivedFromProps.get(
|
||||
instr.value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
setStateCalls.push({
|
||||
loc: instr.value.callee.loc,
|
||||
propsSource: propsSource ?? null,
|
||||
});
|
||||
setStateLocations.push(instr.value.callee.loc);
|
||||
} else {
|
||||
// doesn't depend on all deps
|
||||
// doesn't depend on any deps
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -330,26 +207,6 @@ function validateEffect(
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Track props derivation through instruction effects
|
||||
if (instr.effects != null) {
|
||||
for (const effect of instr.effects) {
|
||||
switch (effect.kind) {
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'MaybeAlias':
|
||||
case 'Capture': {
|
||||
const source = effectDerivedFromProps.get(
|
||||
effect.from.identifier.id,
|
||||
);
|
||||
if (source != null) {
|
||||
effectDerivedFromProps.set(effect.into.identifier.id, source);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (values.has(operand.identifier.id)) {
|
||||
@@ -360,29 +217,14 @@ function validateEffect(
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
for (const call of setStateCalls) {
|
||||
if (call.propsSource != null) {
|
||||
const propName = call.propsSource.identifier.name?.value;
|
||||
const propInfo = propName != null ? ` (from prop '${propName}')` : '';
|
||||
|
||||
errors.push({
|
||||
reason: `Consider lifting state up to the parent component to make this a controlled component. (https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)`,
|
||||
description: `You are using props${propInfo} to update local state in an effect.`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: call.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
} else {
|
||||
errors.push({
|
||||
reason:
|
||||
'You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
|
||||
description:
|
||||
'This effect updates state based on other state values. ' +
|
||||
'Consider calculating this value directly during render',
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: call.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
for (const loc of setStateLocations) {
|
||||
errors.push({
|
||||
reason:
|
||||
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
|
||||
description: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,10 +125,7 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
);
|
||||
if (knownMutation && knownMutation.kind === 'ContextMutation') {
|
||||
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
|
||||
} else if (
|
||||
fn.env.config.enableNewMutationAliasingModel &&
|
||||
value.loweredFunc.func.aliasingEffects != null
|
||||
) {
|
||||
} else if (value.loweredFunc.func.aliasingEffects != null) {
|
||||
const context = new Set(
|
||||
value.loweredFunc.func.context.map(p => p.identifier.id),
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
|
||||
import {HIRFunction} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
};
|
||||
// spreading a function is weird, but it doesn't call the function so this is allowed
|
||||
return <div {...foo} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
const foo = _temp;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div {...foo} />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _temp() {
|
||||
someGlobal = true;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -3,5 +3,6 @@ function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
};
|
||||
// spreading a function is weird, but it doesn't call the function so this is allowed
|
||||
return <div {...foo} />;
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
|
||||
* aliases from `LoadLocal`, `PropertyLoad`, etc
|
||||
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
|
||||
* - We avoid assigning mutable ranges to loads after y's mutable range, as
|
||||
* these are working with an immutable value. As a result, `LoadLocal y` and
|
||||
* `PropertyLoad y` do not get mutable ranges
|
||||
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
|
||||
* as according to the 'co-mutation' of different values
|
||||
* - Here, we infer that
|
||||
* - `arrayPush(y, x)` might alias `x` and `y` to each other
|
||||
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
|
||||
* - This pass correctly extends the mutable range of `y`
|
||||
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
|
||||
* PropertyLoads still don't have a mutable range
|
||||
*
|
||||
* Note that the this bug is an edge case. Compiler output is only invalid for:
|
||||
* - function expressions with
|
||||
* `enableTransitivelyFreezeFunctionExpressions:false`
|
||||
* - functions that throw and get retried without clearing the memocache
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
function useFoo({a, b}: {a: number, b: number}) {
|
||||
const x = [];
|
||||
const y = {value: a};
|
||||
|
||||
arrayPush(x, y); // x and y co-mutate
|
||||
const y_alias = y;
|
||||
const cb = () => y_alias.value;
|
||||
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2, b: 10}],
|
||||
sequentialRenders: [
|
||||
{a: 2, b: 10},
|
||||
{a: 2, b: 11},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";
|
||||
|
||||
function useFoo(t0) {
|
||||
const $ = _c(5);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = [];
|
||||
const y = { value: a };
|
||||
|
||||
arrayPush(x, y);
|
||||
const y_alias = y;
|
||||
let t2;
|
||||
if ($[3] !== y_alias.value) {
|
||||
t2 = () => y_alias.value;
|
||||
$[3] = y_alias.value;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
const cb = t2;
|
||||
setPropertyByKey(x[0], "value", b);
|
||||
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{ a: 2, b: 10 }],
|
||||
sequentialRenders: [
|
||||
{ a: 2, b: 10 },
|
||||
{ a: 2, b: 11 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
|
||||
* aliases from `LoadLocal`, `PropertyLoad`, etc
|
||||
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
|
||||
* - We avoid assigning mutable ranges to loads after y's mutable range, as
|
||||
* these are working with an immutable value. As a result, `LoadLocal y` and
|
||||
* `PropertyLoad y` do not get mutable ranges
|
||||
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
|
||||
* as according to the 'co-mutation' of different values
|
||||
* - Here, we infer that
|
||||
* - `arrayPush(y, x)` might alias `x` and `y` to each other
|
||||
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
|
||||
* - This pass correctly extends the mutable range of `y`
|
||||
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
|
||||
* PropertyLoads still don't have a mutable range
|
||||
*
|
||||
* Note that the this bug is an edge case. Compiler output is only invalid for:
|
||||
* - function expressions with
|
||||
* `enableTransitivelyFreezeFunctionExpressions:false`
|
||||
* - functions that throw and get retried without clearing the memocache
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
function useFoo({a, b}: {a: number, b: number}) {
|
||||
const x = [];
|
||||
const y = {value: a};
|
||||
|
||||
arrayPush(x, y); // x and y co-mutate
|
||||
const y_alias = y;
|
||||
const cb = () => y_alias.value;
|
||||
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2, b: 10}],
|
||||
sequentialRenders: [
|
||||
{a: 2, b: 10},
|
||||
{a: 2, b: 11},
|
||||
],
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Variation of bug in `bug-aliased-capture-aliased-mutate`
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
|
||||
function useFoo({a}: {a: number, b: number}) {
|
||||
const arr = [];
|
||||
const obj = {value: a};
|
||||
|
||||
setPropertyByKey(obj, 'arr', arr);
|
||||
const obj_alias = obj;
|
||||
const cb = () => obj_alias.arr.length;
|
||||
for (let i = 0; i < a; i++) {
|
||||
arr.push(i);
|
||||
}
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2}],
|
||||
sequentialRenders: [{a: 2}, {a: 3}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { setPropertyByKey, Stringify } from "shared-runtime";
|
||||
|
||||
function useFoo(t0) {
|
||||
const $ = _c(4);
|
||||
const { a } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a) {
|
||||
const arr = [];
|
||||
const obj = { value: a };
|
||||
|
||||
setPropertyByKey(obj, "arr", arr);
|
||||
const obj_alias = obj;
|
||||
let t2;
|
||||
if ($[2] !== obj_alias.arr.length) {
|
||||
t2 = () => obj_alias.arr.length;
|
||||
$[2] = obj_alias.arr.length;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const cb = t2;
|
||||
for (let i = 0; i < a; i++) {
|
||||
arr.push(i);
|
||||
}
|
||||
|
||||
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
$[0] = a;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{ a: 2 }],
|
||||
sequentialRenders: [{ a: 2 }, { a: 3 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Variation of bug in `bug-aliased-capture-aliased-mutate`
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
|
||||
function useFoo({a}: {a: number, b: number}) {
|
||||
const arr = [];
|
||||
const obj = {value: a};
|
||||
|
||||
setPropertyByKey(obj, 'arr', arr);
|
||||
const obj_alias = obj;
|
||||
const cb = () => obj_alias.arr.length;
|
||||
for (let i = 0; i < a; i++) {
|
||||
arr.push(i);
|
||||
}
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2}],
|
||||
sequentialRenders: [{a: 2}, {a: 3}],
|
||||
};
|
||||
@@ -85,19 +85,11 @@ import { makeArray, mutate } from "shared-runtime";
|
||||
* used when we analyze CallExpressions.
|
||||
*/
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const $ = _c(3);
|
||||
const { foo, bar } = t0;
|
||||
let t1;
|
||||
if ($[0] !== foo) {
|
||||
t1 = { foo };
|
||||
$[0] = foo;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const x = t1;
|
||||
let y;
|
||||
if ($[2] !== bar || $[3] !== x) {
|
||||
if ($[0] !== bar || $[1] !== foo) {
|
||||
const x = { foo };
|
||||
y = { bar };
|
||||
const f0 = function () {
|
||||
const a = makeArray(y);
|
||||
@@ -108,11 +100,11 @@ function Component(t0) {
|
||||
|
||||
f0();
|
||||
mutate(y.x);
|
||||
$[2] = bar;
|
||||
$[3] = x;
|
||||
$[4] = y;
|
||||
$[0] = bar;
|
||||
$[1] = foo;
|
||||
$[2] = y;
|
||||
} else {
|
||||
y = $[4];
|
||||
y = $[2];
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Fixture showing an edge case for ReactiveScope variable propagation.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* Forget:
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* [[ (exception in render) Error: invariant broken ]]
|
||||
*
|
||||
*/
|
||||
function Component() {
|
||||
const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
|
||||
const boxedInner = [obj?.inner];
|
||||
useIdentity(null);
|
||||
mutate(obj);
|
||||
if (boxedInner[0] !== obj?.inner) {
|
||||
throw new Error('invariant broken');
|
||||
}
|
||||
return <Stringify obj={obj} inner={boxedInner} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{arg: 0}],
|
||||
sequentialRenders: [{arg: 0}, {arg: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Fixture showing an edge case for ReactiveScope variable propagation.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* Forget:
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* [[ (exception in render) Error: invariant broken ]]
|
||||
*
|
||||
*/
|
||||
function Component() {
|
||||
const $ = _c(4);
|
||||
const obj = CONST_TRUE ? { inner: { value: "hello" } } : null;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = [obj?.inner];
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const boxedInner = t0;
|
||||
useIdentity(null);
|
||||
mutate(obj);
|
||||
if (boxedInner[0] !== obj?.inner) {
|
||||
throw new Error("invariant broken");
|
||||
}
|
||||
let t1;
|
||||
if ($[1] !== boxedInner || $[2] !== obj) {
|
||||
t1 = <Stringify obj={obj} inner={boxedInner} />;
|
||||
$[1] = boxedInner;
|
||||
$[2] = obj;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ arg: 0 }],
|
||||
sequentialRenders: [{ arg: 0 }, { arg: 1 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Fixture showing an edge case for ReactiveScope variable propagation.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* Forget:
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* [[ (exception in render) Error: invariant broken ]]
|
||||
*
|
||||
*/
|
||||
function Component() {
|
||||
const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
|
||||
const boxedInner = [obj?.inner];
|
||||
useIdentity(null);
|
||||
mutate(obj);
|
||||
if (boxedInner[0] !== obj?.inner) {
|
||||
throw new Error('invariant broken');
|
||||
}
|
||||
return <Stringify obj={obj} inner={boxedInner} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{arg: 0}],
|
||||
sequentialRenders: [{arg: 0}, {arg: 1}],
|
||||
};
|
||||
@@ -1,110 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {identity, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
|
||||
* with the mutation hoisted to a named variable instead of being directly
|
||||
* inlined into the Object key.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* Forget:
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
|
||||
*/
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const tmp = (mutate(key), key);
|
||||
const context = {
|
||||
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
|
||||
[tmp]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return [context, key];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { identity, mutate } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
|
||||
* with the mutation hoisted to a named variable instead of being directly
|
||||
* inlined into the Object key.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* Forget:
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
|
||||
*/
|
||||
function Component(props) {
|
||||
const $ = _c(8);
|
||||
let key;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
key = {};
|
||||
t0 = (mutate(key), key);
|
||||
$[0] = key;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
key = $[0];
|
||||
t0 = $[1];
|
||||
}
|
||||
const tmp = t0;
|
||||
let t1;
|
||||
if ($[2] !== props.value) {
|
||||
t1 = identity([props.value]);
|
||||
$[2] = props.value;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== t1) {
|
||||
t2 = { [tmp]: t1 };
|
||||
$[4] = t1;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const context = t2;
|
||||
|
||||
mutate(key);
|
||||
let t3;
|
||||
if ($[6] !== context) {
|
||||
t3 = [context, key];
|
||||
$[6] = context;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
sequentialRenders: [{ value: 42 }, { value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {identity, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
|
||||
* with the mutation hoisted to a named variable instead of being directly
|
||||
* inlined into the Object key.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* Forget:
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
|
||||
*/
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const tmp = (mutate(key), key);
|
||||
const context = {
|
||||
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
|
||||
[tmp]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return [context, key];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
};
|
||||
return <div {...foo} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
|
||||
|
||||
error.assign-global-in-jsx-spread-attribute.ts:4:4
|
||||
2 | function Component() {
|
||||
3 | const foo = () => {
|
||||
> 4 | someGlobal = true;
|
||||
| ^^^^^^^^^^ Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
|
||||
5 | };
|
||||
6 | return <div {...foo} />;
|
||||
7 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
|
||||
|
||||
import {useCallback, useEffect, useRef} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const params = useHook();
|
||||
const update = useCallback(
|
||||
partialParams => {
|
||||
const nextParams = {
|
||||
...params,
|
||||
...partialParams,
|
||||
};
|
||||
nextParams.param = 'value';
|
||||
console.log(nextParams);
|
||||
},
|
||||
[params]
|
||||
);
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
if (ref.current === null) {
|
||||
update();
|
||||
}
|
||||
}, [update]);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot modify local variables after render completes
|
||||
|
||||
This argument is a function which may reassign or mutate a local variable after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
|
||||
|
||||
error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:20:12
|
||||
18 | );
|
||||
19 | const ref = useRef(null);
|
||||
> 20 | useEffect(() => {
|
||||
| ^^^^^^^
|
||||
> 21 | if (ref.current === null) {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 22 | update();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 23 | }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 24 | }, [update]);
|
||||
| ^^^^ This function may (indirectly) reassign or modify a local variable after render
|
||||
25 |
|
||||
26 | return 'ok';
|
||||
27 | }
|
||||
|
||||
error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:14:6
|
||||
12 | ...partialParams,
|
||||
13 | };
|
||||
> 14 | nextParams.param = 'value';
|
||||
| ^^^^^^^^^^ This modifies a local variable
|
||||
15 | console.log(nextParams);
|
||||
16 | },
|
||||
17 | [params]
|
||||
```
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
|
||||
|
||||
import {useCallback, useEffect, useRef} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const params = useHook();
|
||||
const update = useCallback(
|
||||
partialParams => {
|
||||
const nextParams = {
|
||||
...params,
|
||||
...partialParams,
|
||||
};
|
||||
nextParams.param = 'value';
|
||||
console.log(nextParams);
|
||||
},
|
||||
[params]
|
||||
);
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
if (ref.current === null) {
|
||||
update();
|
||||
}
|
||||
}, [update]);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow
|
||||
|
||||
component Foo() {
|
||||
const foo = useFoo();
|
||||
foo.current = true;
|
||||
return <div />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed.
|
||||
|
||||
3 | component Foo() {
|
||||
4 | const foo = useFoo();
|
||||
> 5 | foo.current = true;
|
||||
| ^^^ value cannot be modified
|
||||
6 | return <div />;
|
||||
7 | }
|
||||
8 |
|
||||
|
||||
3 | component Foo() {
|
||||
4 | const foo = useFoo();
|
||||
> 5 | foo.current = true;
|
||||
| ^^^ Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in "Ref".
|
||||
6 | return <div />;
|
||||
7 | }
|
||||
8 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// @flow
|
||||
|
||||
component Foo() {
|
||||
const foo = useFoo();
|
||||
foo.current = true;
|
||||
return <div />;
|
||||
}
|
||||
@@ -24,15 +24,13 @@ function BadExample() {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
This effect updates state based on other state values. Consider calculating this value directly during render.
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.invalid-derived-computation-in-effect.ts:9:4
|
||||
7 | const [fullName, setFullName] = useState('');
|
||||
8 | useEffect(() => {
|
||||
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
|
||||
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
10 | }, [firstName, lastName]);
|
||||
11 |
|
||||
12 | return <div>{fullName}</div>;
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Foo() {
|
||||
const x = () => {
|
||||
window.href = 'foo';
|
||||
};
|
||||
const y = {x};
|
||||
return <Bar y={y} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Modifying a variable defined outside a component or hook is not allowed. Consider using an effect
|
||||
|
||||
error.object-capture-global-mutation.ts:4:4
|
||||
2 | function Foo() {
|
||||
3 | const x = () => {
|
||||
> 4 | window.href = 'foo';
|
||||
| ^^^^^^ Modifying a variable defined outside a component or hook is not allowed. Consider using an effect
|
||||
5 | };
|
||||
6 | const y = {x};
|
||||
7 | return <Bar y={y} />;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Foo() {
|
||||
const x = () => {
|
||||
window.href = 'foo';
|
||||
};
|
||||
const y = {x};
|
||||
return <Bar y={y} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Foo() {
|
||||
const $ = _c(1);
|
||||
const x = _temp;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const y = { x };
|
||||
t0 = <Bar y={y} />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _temp() {
|
||||
window.href = "foo";
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Bar is not defined
|
||||
@@ -1,4 +1,3 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Foo() {
|
||||
const x = () => {
|
||||
window.href = 'foo';
|
||||
@@ -1,87 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{initialName: 'John'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { initialName } = t0;
|
||||
const [name, setName] = useState("");
|
||||
let t1;
|
||||
if ($[0] !== initialName) {
|
||||
t1 = () => {
|
||||
setName(initialName);
|
||||
};
|
||||
$[0] = initialName;
|
||||
$[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] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = (e) => setName(e.target.value);
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] !== name) {
|
||||
t4 = (
|
||||
<div>
|
||||
<input value={name} onChange={t3} />
|
||||
</div>
|
||||
);
|
||||
$[4] = name;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ initialName: "John" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div><input value="John"></div>
|
||||
@@ -1,21 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{initialName: 'John'}],
|
||||
};
|
||||
@@ -1,79 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { value, enabled } = t0;
|
||||
const [localValue, setLocalValue] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== enabled || $[1] !== value) {
|
||||
t1 = () => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue("disabled");
|
||||
}
|
||||
};
|
||||
|
||||
t2 = [value, enabled];
|
||||
$[0] = enabled;
|
||||
$[1] = value;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] !== localValue) {
|
||||
t3 = <div>{localValue}</div>;
|
||||
$[4] = localValue;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: "test", enabled: true }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
@@ -1,21 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Value changed:', value);
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { value } = t0;
|
||||
const [localValue, setLocalValue] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== value) {
|
||||
t1 = () => {
|
||||
console.log("Value changed:", value);
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
};
|
||||
t2 = [value];
|
||||
$[0] = value;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== localValue) {
|
||||
t3 = <div>{localValue}</div>;
|
||||
$[3] = localValue;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
logs: ['Value changed:','test']
|
||||
@@ -1,19 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Value changed:', value);
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({prefix}) {
|
||||
const [name, setName] = useState('');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayName(prefix + name);
|
||||
}, [prefix, name]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
<div>{displayName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: 'Hello, '}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
This effect updates state based on other state values. Consider calculating this value directly during render.
|
||||
|
||||
error.bug-derived-state-from-mixed-deps.ts:9:4
|
||||
7 |
|
||||
8 | useEffect(() => {
|
||||
> 9 | setDisplayName(prefix + name);
|
||||
| ^^^^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
10 | }, [prefix, name]);
|
||||
11 |
|
||||
12 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({prefix}) {
|
||||
const [name, setName] = useState('');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayName(prefix + name);
|
||||
}, [prefix, name]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
<div>{displayName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: 'Hello, '}],
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({user: {firstName, lastName}}) {
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{user: {firstName: 'John', lastName: 'Doe'}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
This effect updates state based on other state values. Consider calculating this value directly during render.
|
||||
|
||||
error.invalid-derived-state-from-props-destructured.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
9 | }, [firstName, lastName]);
|
||||
10 |
|
||||
11 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName, lastName}) {
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName, lastName}) {
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
This effect updates state based on other state values. Consider calculating this value directly during render.
|
||||
|
||||
error.invalid-derived-state-from-props-in-effect.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
9 | }, [firstName, lastName]);
|
||||
10 |
|
||||
11 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName, lastName}) {
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('John');
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
This effect updates state based on other state values. Consider calculating this value directly during render.
|
||||
|
||||
error.invalid-derived-state-from-state-in-effect.ts:10:4
|
||||
8 |
|
||||
9 | useEffect(() => {
|
||||
> 10 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
11 | }, [firstName, lastName]);
|
||||
12 |
|
||||
13 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('John');
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(7);
|
||||
const [displayValue, setDisplayValue] = useState("");
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== props.prefix || $[1] !== props.suffix || $[2] !== props.value) {
|
||||
t0 = () => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
};
|
||||
t1 = [props.prefix, props.value, props.suffix];
|
||||
$[0] = props.prefix;
|
||||
$[1] = props.suffix;
|
||||
$[2] = props.value;
|
||||
$[3] = t0;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t0 = $[3];
|
||||
t1 = $[4];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[5] !== displayValue) {
|
||||
t2 = <div>{displayValue}</div>;
|
||||
$[5] = displayValue;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ prefix: "[", value: "test", suffix: "]" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>[test]</div>
|
||||
@@ -1,18 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
@@ -71,10 +71,10 @@
|
||||
"scripts": {
|
||||
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/ && rm -rf node_modules/.cache;",
|
||||
"prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/ && rm -rf node_modules/.cache;",
|
||||
"dev": "concurrently \"npm run dev:region\" \"npm run dev:global\"",
|
||||
"dev": "concurrently \"yarn run dev:region\" \"yarn run dev:global\"",
|
||||
"dev:global": "NODE_ENV=development BUILD_PATH=dist node --experimental-loader ./loader/global.js --inspect=127.0.0.1:9230 server/global",
|
||||
"dev:region": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --enable-source-maps --experimental-loader ./loader/region.js --conditions=react-server --inspect=127.0.0.1:9229 server/region",
|
||||
"start": "node scripts/build.js && concurrently \"npm run start:region\" \"npm run start:global\"",
|
||||
"start": "node scripts/build.js && concurrently \"yarn run start:region\" \"yarn run start:global\"",
|
||||
"start:global": "NODE_ENV=production node --experimental-loader ./loader/global.js server/global",
|
||||
"start:region": "NODE_ENV=production node --experimental-loader ./loader/region.js --conditions=react-server server/region",
|
||||
"build": "node scripts/build.js",
|
||||
|
||||
@@ -2169,6 +2169,29 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
+ Bye in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('fails if last received error containing "undefined" is not included', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hi');
|
||||
console.error(
|
||||
"TypeError: Cannot read properties of undefined (reading 'stack')\n" +
|
||||
' in Foo (at **)'
|
||||
);
|
||||
assertConsoleErrorDev([['Hi', {withoutStack: true}]]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Unexpected error(s) recorded.
|
||||
|
||||
- Expected errors
|
||||
+ Received errors
|
||||
|
||||
Hi
|
||||
+ TypeError: Cannot read properties of undefined (reading 'stack') in Foo (at **)"
|
||||
`);
|
||||
});
|
||||
// @gate __DEV__
|
||||
it('fails if only error does not contain a stack', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
|
||||
@@ -355,7 +355,7 @@ export function createLogAssertion(
|
||||
let argIndex = 0;
|
||||
// console.* could have been called with a non-string e.g. `console.error(new Error())`
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(format).replace(/%s|%c/g, () => argIndex++);
|
||||
String(format).replace(/%s|%c|%o/g, () => argIndex++);
|
||||
if (argIndex !== args.length) {
|
||||
if (format.includes('%c%s')) {
|
||||
// We intentionally use mismatching formatting when printing badging because we don't know
|
||||
@@ -382,8 +382,9 @@ export function createLogAssertion(
|
||||
|
||||
// Main logic to check if log is expected, with the component stack.
|
||||
if (
|
||||
normalizedMessage === expectedMessage ||
|
||||
normalizedMessage.includes(expectedMessage)
|
||||
typeof expectedMessage === 'string' &&
|
||||
(normalizedMessage === expectedMessage ||
|
||||
normalizedMessage.includes(expectedMessage))
|
||||
) {
|
||||
if (isLikelyAComponentStack(normalizedMessage)) {
|
||||
if (expectedWithoutStack === true) {
|
||||
|
||||
@@ -30,7 +30,7 @@ export function injectInternals(internals: Object): boolean {
|
||||
} catch (err) {
|
||||
// Catch all errors because it is unsafe to throw during initialization.
|
||||
if (__DEV__) {
|
||||
console.error('React instrumentation encountered an error: %s.', err);
|
||||
console.error('React instrumentation encountered an error: %o.', err);
|
||||
}
|
||||
}
|
||||
if (hook.checkDCE) {
|
||||
|
||||
@@ -141,7 +141,7 @@ function patchConsoleForTestingBeforeHookInstallation() {
|
||||
// if they use this code path.
|
||||
firstArg = firstArg.slice(9);
|
||||
}
|
||||
if (firstArg === 'React instrumentation encountered an error: %s') {
|
||||
if (firstArg === 'React instrumentation encountered an error: %o') {
|
||||
// Rethrow errors from React.
|
||||
throw args[1];
|
||||
} else if (
|
||||
|
||||
@@ -2696,4 +2696,83 @@ describe('Store', () => {
|
||||
<ClientComponent key="D">
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
it('can reconcile Suspense in fallback positions', async () => {
|
||||
let resolveFallback;
|
||||
const fallbackPromise = new Promise(resolve => {
|
||||
resolveFallback = resolve;
|
||||
});
|
||||
let resolveContent;
|
||||
const contentPromise = new Promise(resolve => {
|
||||
resolveContent = resolve;
|
||||
});
|
||||
|
||||
function Component({children, promise}) {
|
||||
if (promise) {
|
||||
React.use(promise);
|
||||
}
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
await actAsync(() =>
|
||||
render(
|
||||
<React.Suspense
|
||||
name="content"
|
||||
fallback={
|
||||
<React.Suspense
|
||||
name="fallback"
|
||||
fallback={
|
||||
<Component key="fallback-fallback">
|
||||
Loading fallback...
|
||||
</Component>
|
||||
}>
|
||||
<Component key="fallback-content" promise={fallbackPromise}>
|
||||
Loading...
|
||||
</Component>
|
||||
</React.Suspense>
|
||||
}>
|
||||
<Component key="content" promise={contentPromise}>
|
||||
done
|
||||
</Component>
|
||||
</React.Suspense>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Suspense name="content">
|
||||
▾ <Suspense name="fallback">
|
||||
<Component key="fallback-fallback">
|
||||
[shell]
|
||||
<Suspense name="content" rects={null}>
|
||||
<Suspense name="fallback" rects={null}>
|
||||
`);
|
||||
|
||||
await actAsync(() => {
|
||||
resolveFallback();
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Suspense name="content">
|
||||
▾ <Suspense name="fallback">
|
||||
<Component key="fallback-content">
|
||||
[shell]
|
||||
<Suspense name="content" rects={null}>
|
||||
<Suspense name="fallback" rects={[{x:1,y:2,width:10,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(() => {
|
||||
resolveContent();
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Suspense name="content">
|
||||
<Component key="content">
|
||||
[shell]
|
||||
<Suspense name="content" rects={[{x:1,y:2,width:4,height:1}]}>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -367,6 +367,7 @@ export function getInternalReactConstants(version: string): {
|
||||
ReactPriorityLevels: ReactPriorityLevelsType,
|
||||
ReactTypeOfWork: WorkTagMap,
|
||||
StrictModeBits: number,
|
||||
SuspenseyImagesMode: number,
|
||||
} {
|
||||
// **********************************************************
|
||||
// The section below is copied from files in React repo.
|
||||
@@ -407,6 +408,8 @@ export function getInternalReactConstants(version: string): {
|
||||
StrictModeBits = 0b10;
|
||||
}
|
||||
|
||||
const SuspenseyImagesMode = 0b0100000;
|
||||
|
||||
let ReactTypeOfWork: WorkTagMap = ((null: any): WorkTagMap);
|
||||
|
||||
// **********************************************************
|
||||
@@ -820,6 +823,7 @@ export function getInternalReactConstants(version: string): {
|
||||
ReactPriorityLevels,
|
||||
ReactTypeOfWork,
|
||||
StrictModeBits,
|
||||
SuspenseyImagesMode,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -988,6 +992,7 @@ export function attach(
|
||||
ReactPriorityLevels,
|
||||
ReactTypeOfWork,
|
||||
StrictModeBits,
|
||||
SuspenseyImagesMode,
|
||||
} = getInternalReactConstants(version);
|
||||
const {
|
||||
ActivityComponent,
|
||||
@@ -2930,7 +2935,7 @@ export function attach(
|
||||
}
|
||||
if (suspenseNode.parent !== parentNode) {
|
||||
throw new Error(
|
||||
'Cannot remove a node from a different parent than is being reconciled.',
|
||||
'Cannot remove a Suspense node from a different parent than is being reconciled.',
|
||||
);
|
||||
}
|
||||
let previousSuspenseSibling = remainingReconcilingChildrenSuspenseNodes;
|
||||
@@ -3345,6 +3350,114 @@ export function attach(
|
||||
insertSuspendedBy(asyncInfo);
|
||||
}
|
||||
|
||||
function trackDebugInfoFromHostComponent(
|
||||
devtoolsInstance: DevToolsInstance,
|
||||
fiber: Fiber,
|
||||
): void {
|
||||
if (fiber.tag !== HostComponent) {
|
||||
return;
|
||||
}
|
||||
if ((fiber.mode & SuspenseyImagesMode) === 0) {
|
||||
// In any released version, Suspensey Images are only enabled inside a ViewTransition
|
||||
// subtree, which is enabled by the SuspenseyImagesMode.
|
||||
// TODO: If we ever enable the enableSuspenseyImages flag then it would be enabled for
|
||||
// all images and we'd need some other check for if the version of React has that enabled.
|
||||
return;
|
||||
}
|
||||
|
||||
const type = fiber.type;
|
||||
const props: {
|
||||
src?: string,
|
||||
onLoad?: (event: any) => void,
|
||||
loading?: 'eager' | 'lazy',
|
||||
...
|
||||
} = fiber.memoizedProps;
|
||||
|
||||
const maySuspendCommit =
|
||||
type === 'img' &&
|
||||
props.src != null &&
|
||||
props.src !== '' &&
|
||||
props.onLoad == null &&
|
||||
props.loading !== 'lazy';
|
||||
|
||||
// Note: We don't track "maySuspendCommitOnUpdate" separately because it doesn't matter if
|
||||
// it didn't suspend this particular update if it would've suspended if it mounted in this
|
||||
// state, since we're tracking the dependencies inside the current state.
|
||||
|
||||
if (!maySuspendCommit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = fiber.stateNode;
|
||||
if (instance == null) {
|
||||
// Should never happen.
|
||||
return;
|
||||
}
|
||||
|
||||
// Unlike props.src, currentSrc will be fully qualified which we need for comparison below.
|
||||
// Unlike instance.src it will be resolved into the media queries currently matching which is
|
||||
// the state we're inspecting.
|
||||
const src = instance.currentSrc;
|
||||
if (typeof src !== 'string' || src === '') {
|
||||
return;
|
||||
}
|
||||
let start = -1;
|
||||
let end = -1;
|
||||
let fileSize = 0;
|
||||
// $FlowFixMe[method-unbinding]
|
||||
if (typeof performance.getEntriesByType === 'function') {
|
||||
// We may be able to collect the start and end time of this resource from Performance Observer.
|
||||
const resourceEntries = performance.getEntriesByType('resource');
|
||||
for (let i = 0; i < resourceEntries.length; i++) {
|
||||
const resourceEntry = resourceEntries[i];
|
||||
if (resourceEntry.name === src) {
|
||||
start = resourceEntry.startTime;
|
||||
end = start + resourceEntry.duration;
|
||||
// $FlowFixMe[prop-missing]
|
||||
fileSize = (resourceEntry.encodedBodySize: any) || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// A representation of the image data itself.
|
||||
// TODO: We could render a little preview in the front end from the resource API.
|
||||
const value: {
|
||||
currentSrc: string,
|
||||
naturalWidth?: number,
|
||||
naturalHeight?: number,
|
||||
fileSize?: number,
|
||||
} = {
|
||||
currentSrc: src,
|
||||
};
|
||||
if (instance.naturalWidth > 0 && instance.naturalHeight > 0) {
|
||||
// The intrinsic size of the file value itself, if it's loaded
|
||||
value.naturalWidth = instance.naturalWidth;
|
||||
value.naturalHeight = instance.naturalHeight;
|
||||
}
|
||||
if (fileSize > 0) {
|
||||
// Cross-origin images won't have a file size that we can access.
|
||||
value.fileSize = fileSize;
|
||||
}
|
||||
const promise = Promise.resolve(value);
|
||||
(promise: any).status = 'fulfilled';
|
||||
(promise: any).value = value;
|
||||
const ioInfo: ReactIOInfo = {
|
||||
name: 'img',
|
||||
start,
|
||||
end,
|
||||
value: promise,
|
||||
// $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file.
|
||||
owner: fiber, // Allow linking to the <link> if it's not filtered.
|
||||
};
|
||||
const asyncInfo: ReactAsyncInfo = {
|
||||
awaited: ioInfo,
|
||||
// $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file.
|
||||
owner: fiber._debugOwner == null ? null : fiber._debugOwner,
|
||||
debugStack: fiber._debugStack == null ? null : fiber._debugStack,
|
||||
debugTask: fiber._debugTask == null ? null : fiber._debugTask,
|
||||
};
|
||||
insertSuspendedBy(asyncInfo);
|
||||
}
|
||||
|
||||
function mountVirtualChildrenRecursively(
|
||||
firstChild: Fiber,
|
||||
lastChild: null | Fiber, // non-inclusive
|
||||
@@ -3619,6 +3732,7 @@ export function attach(
|
||||
throw new Error('Did not expect a host hoistable to be the root');
|
||||
}
|
||||
aquireHostInstance(nearestInstance, fiber.stateNode);
|
||||
trackDebugInfoFromHostComponent(nearestInstance, fiber);
|
||||
}
|
||||
|
||||
if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) {
|
||||
@@ -4447,20 +4561,22 @@ export function attach(
|
||||
aquireHostResource(nearestInstance, nextFiber.memoizedState);
|
||||
trackDebugInfoFromHostResource(nearestInstance, nextFiber);
|
||||
} else if (
|
||||
(nextFiber.tag === HostComponent ||
|
||||
nextFiber.tag === HostText ||
|
||||
nextFiber.tag === HostSingleton) &&
|
||||
prevFiber.stateNode !== nextFiber.stateNode
|
||||
nextFiber.tag === HostComponent ||
|
||||
nextFiber.tag === HostText ||
|
||||
nextFiber.tag === HostSingleton
|
||||
) {
|
||||
// In persistent mode, it's possible for the stateNode to update with
|
||||
// a new clone. In that case we need to release the old one and aquire
|
||||
// new one instead.
|
||||
const nearestInstance = reconcilingParent;
|
||||
if (nearestInstance === null) {
|
||||
throw new Error('Did not expect a host hoistable to be the root');
|
||||
}
|
||||
releaseHostInstance(nearestInstance, prevFiber.stateNode);
|
||||
aquireHostInstance(nearestInstance, nextFiber.stateNode);
|
||||
if (prevFiber.stateNode !== nextFiber.stateNode) {
|
||||
// In persistent mode, it's possible for the stateNode to update with
|
||||
// a new clone. In that case we need to release the old one and aquire
|
||||
// new one instead.
|
||||
releaseHostInstance(nearestInstance, prevFiber.stateNode);
|
||||
aquireHostInstance(nearestInstance, nextFiber.stateNode);
|
||||
}
|
||||
trackDebugInfoFromHostComponent(nearestInstance, nextFiber);
|
||||
}
|
||||
|
||||
let updateFlags = NoUpdate;
|
||||
@@ -4620,26 +4736,30 @@ export function attach(
|
||||
);
|
||||
|
||||
shouldMeasureSuspenseNode = false;
|
||||
if (nextFallbackFiber !== null) {
|
||||
if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
|
||||
const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
|
||||
const fallbackStashedSuspensePrevious =
|
||||
previouslyReconciledSiblingSuspenseNode;
|
||||
const fallbackStashedSuspenseRemaining =
|
||||
remainingReconcilingChildrenSuspenseNodes;
|
||||
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
|
||||
// reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
|
||||
// reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
|
||||
// Since the fallback conceptually blocks the parent.
|
||||
reconcilingParentSuspenseNode = stashedSuspenseParent;
|
||||
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
|
||||
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
|
||||
try {
|
||||
updateFlags |= updateVirtualChildrenRecursively(
|
||||
nextFallbackFiber,
|
||||
null,
|
||||
prevFallbackFiber,
|
||||
traceNearestHostComponentUpdate,
|
||||
0,
|
||||
);
|
||||
if (nextFallbackFiber === null) {
|
||||
unmountRemainingChildren();
|
||||
} else {
|
||||
updateFlags |= updateVirtualChildrenRecursively(
|
||||
nextFallbackFiber,
|
||||
null,
|
||||
prevFallbackFiber,
|
||||
traceNearestHostComponentUpdate,
|
||||
0,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
|
||||
previouslyReconciledSiblingSuspenseNode =
|
||||
@@ -4647,7 +4767,8 @@ export function attach(
|
||||
remainingReconcilingChildrenSuspenseNodes =
|
||||
fallbackStashedSuspenseRemaining;
|
||||
}
|
||||
} else if (nextFiber.memoizedState === null) {
|
||||
}
|
||||
if (nextFiber.memoizedState === null) {
|
||||
// Measure this Suspense node in case it changed. We don't update the rect while
|
||||
// we're inside a disconnected subtree nor if we are the Suspense boundary that
|
||||
// is suspended. This lets us keep the rectangle of the displayed content while
|
||||
@@ -5152,6 +5273,18 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function getNearestSuspenseNode(instance: DevToolsInstance): SuspenseNode {
|
||||
while (instance.suspenseNode === null) {
|
||||
if (instance.parent === null) {
|
||||
throw new Error(
|
||||
'There should always be a SuspenseNode parent on a mounted instance.',
|
||||
);
|
||||
}
|
||||
instance = instance.parent;
|
||||
}
|
||||
return instance.suspenseNode;
|
||||
}
|
||||
|
||||
function getNearestMountedDOMNode(publicInstance: Element): null | Element {
|
||||
let domNode: null | Element = publicInstance;
|
||||
while (domNode && !publicInstanceToDevToolsInstanceMap.has(domNode)) {
|
||||
@@ -5440,6 +5573,56 @@ export function attach(
|
||||
return result;
|
||||
}
|
||||
|
||||
const FALLBACK_THROTTLE_MS: number = 300;
|
||||
|
||||
function getSuspendedByRange(
|
||||
suspenseNode: SuspenseNode,
|
||||
): null | [number, number] {
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
suspenseNode.suspendedBy.forEach((_, ioInfo) => {
|
||||
if (ioInfo.end > max) {
|
||||
max = ioInfo.end;
|
||||
}
|
||||
if (ioInfo.start < min) {
|
||||
min = ioInfo.start;
|
||||
}
|
||||
});
|
||||
const parentSuspenseNode = suspenseNode.parent;
|
||||
if (parentSuspenseNode !== null) {
|
||||
let parentMax = -Infinity;
|
||||
parentSuspenseNode.suspendedBy.forEach((_, ioInfo) => {
|
||||
if (ioInfo.end > parentMax) {
|
||||
parentMax = ioInfo.end;
|
||||
}
|
||||
});
|
||||
// The parent max is theoretically the earlier the parent could've committed.
|
||||
// Therefore, the theoretical max that the child could be throttled is that plus 300ms.
|
||||
const throttleTime = parentMax + FALLBACK_THROTTLE_MS;
|
||||
if (throttleTime > max) {
|
||||
// If the theoretical throttle time is later than the earliest reveal then we extend
|
||||
// the max time to show that this is timespan could possibly get throttled.
|
||||
max = throttleTime;
|
||||
}
|
||||
|
||||
// We use the end of the previous boundary as the start time for this boundary unless,
|
||||
// that's earlier than we'd need to expand to the full fallback throttle range. It
|
||||
// suggests that the parent was loaded earlier than this one.
|
||||
let startTime = max - FALLBACK_THROTTLE_MS;
|
||||
if (parentMax > startTime) {
|
||||
startTime = parentMax;
|
||||
}
|
||||
// If the first fetch of this boundary starts before that, then we use that as the start.
|
||||
if (startTime < min) {
|
||||
min = startTime;
|
||||
}
|
||||
}
|
||||
if (min < Infinity && max > -Infinity) {
|
||||
return [min, max];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getAwaitStackFromHooks(
|
||||
hooks: HooksTree,
|
||||
asyncInfo: ReactAsyncInfo,
|
||||
@@ -5893,6 +6076,11 @@ export function attach(
|
||||
nativeTag = getNativeTag(fiber.stateNode);
|
||||
}
|
||||
|
||||
let isSuspended: boolean | null = null;
|
||||
if (tag === SuspenseComponent) {
|
||||
isSuspended = memoizedState !== null;
|
||||
}
|
||||
|
||||
const suspendedBy =
|
||||
fiberInstance.suspenseNode !== null
|
||||
? // If this is a Suspense boundary, then we include everything in the subtree that might suspend
|
||||
@@ -5908,6 +6096,10 @@ export function attach(
|
||||
: fiberInstance.suspendedBy.map(info =>
|
||||
serializeAsyncInfo(info, fiberInstance, hooks),
|
||||
);
|
||||
const suspendedByRange = getSuspendedByRange(
|
||||
getNearestSuspenseNode(fiberInstance),
|
||||
);
|
||||
|
||||
return {
|
||||
id: fiberInstance.id,
|
||||
|
||||
@@ -5939,6 +6131,7 @@ export function attach(
|
||||
forceFallbackForFibers.has(fiber) ||
|
||||
(fiber.alternate !== null &&
|
||||
forceFallbackForFibers.has(fiber.alternate))),
|
||||
isSuspended: isSuspended,
|
||||
|
||||
source,
|
||||
|
||||
@@ -5970,6 +6163,7 @@ export function attach(
|
||||
: Array.from(componentLogsEntry.warnings.entries()),
|
||||
|
||||
suspendedBy: suspendedBy,
|
||||
suspendedByRange: suspendedByRange,
|
||||
|
||||
// List of owners
|
||||
owners,
|
||||
@@ -6026,8 +6220,12 @@ export function attach(
|
||||
const componentLogsEntry =
|
||||
componentInfoToComponentLogsMap.get(componentInfo);
|
||||
|
||||
const isSuspended = null;
|
||||
// Things that Suspended this Server Component (use(), awaits and direct child promises)
|
||||
const suspendedBy = virtualInstance.suspendedBy;
|
||||
const suspendedByRange = getSuspendedByRange(
|
||||
getNearestSuspenseNode(virtualInstance),
|
||||
);
|
||||
|
||||
return {
|
||||
id: virtualInstance.id,
|
||||
@@ -6044,6 +6242,7 @@ export function attach(
|
||||
isErrored: false,
|
||||
|
||||
canToggleSuspense: supportsTogglingSuspense && hasSuspenseBoundary,
|
||||
isSuspended: isSuspended,
|
||||
|
||||
source,
|
||||
|
||||
@@ -6080,6 +6279,7 @@ export function attach(
|
||||
: suspendedBy.map(info =>
|
||||
serializeAsyncInfo(info, virtualInstance, null),
|
||||
),
|
||||
suspendedByRange: suspendedByRange,
|
||||
|
||||
// List of owners
|
||||
owners,
|
||||
|
||||
@@ -836,6 +836,7 @@ export function attach(
|
||||
|
||||
// Suspense did not exist in legacy versions
|
||||
canToggleSuspense: false,
|
||||
isSuspended: null,
|
||||
|
||||
source: null,
|
||||
|
||||
@@ -858,6 +859,7 @@ export function attach(
|
||||
|
||||
// Not supported in legacy renderers.
|
||||
suspendedBy: [],
|
||||
suspendedByRange: null,
|
||||
|
||||
// List of owners
|
||||
owners,
|
||||
|
||||
@@ -285,6 +285,8 @@ export type InspectedElement = {
|
||||
|
||||
// Is this Suspense, and can its value be overridden now?
|
||||
canToggleSuspense: boolean,
|
||||
// If this Element is suspended. Currently only set on Suspense boundaries.
|
||||
isSuspended: boolean | null,
|
||||
|
||||
// Does the component have legacy context attached to it.
|
||||
hasLegacyContext: boolean,
|
||||
@@ -300,6 +302,7 @@ export type InspectedElement = {
|
||||
|
||||
// Things that suspended this Instances
|
||||
suspendedBy: Object, // DehydratedData or Array<SerializedAsyncInfo>
|
||||
suspendedByRange: null | [number, number],
|
||||
|
||||
// List of owners
|
||||
owners: Array<SerializedElement> | null,
|
||||
|
||||
@@ -251,6 +251,7 @@ export function convertInspectedElementBackendToFrontend(
|
||||
canToggleError,
|
||||
isErrored,
|
||||
canToggleSuspense,
|
||||
isSuspended,
|
||||
hasLegacyContext,
|
||||
id,
|
||||
type,
|
||||
@@ -270,6 +271,7 @@ export function convertInspectedElementBackendToFrontend(
|
||||
errors,
|
||||
warnings,
|
||||
suspendedBy,
|
||||
suspendedByRange,
|
||||
nativeTag,
|
||||
} = inspectedElementBackend;
|
||||
|
||||
@@ -286,6 +288,7 @@ export function convertInspectedElementBackendToFrontend(
|
||||
canToggleError,
|
||||
isErrored,
|
||||
canToggleSuspense,
|
||||
isSuspended,
|
||||
hasLegacyContext,
|
||||
id,
|
||||
key,
|
||||
@@ -313,6 +316,7 @@ export function convertInspectedElementBackendToFrontend(
|
||||
hydratedSuspendedBy == null // backwards compat
|
||||
? []
|
||||
: hydratedSuspendedBy.map(backendToFrontendSerializedAsyncInfo),
|
||||
suspendedByRange,
|
||||
nativeTag,
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import FetchFileWithCachingContext from './FetchFileWithCachingContext';
|
||||
import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';
|
||||
import OpenInEditorButton from './OpenInEditorButton';
|
||||
import InspectedElementViewSourceButton from './InspectedElementViewSourceButton';
|
||||
import Skeleton from './Skeleton';
|
||||
import useEditorURL from '../useEditorURL';
|
||||
|
||||
import styles from './InspectedElement.css';
|
||||
@@ -114,7 +113,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
|
||||
element !== null &&
|
||||
element.type === ElementTypeSuspense &&
|
||||
inspectedElement != null &&
|
||||
inspectedElement.state != null;
|
||||
inspectedElement.isSuspended;
|
||||
|
||||
const canToggleError =
|
||||
!hideToggleErrorAction &&
|
||||
@@ -203,7 +202,9 @@ export default function InspectedElementWrapper(_: Props): React.Node {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.InspectedElement}>
|
||||
<div
|
||||
className={styles.InspectedElement}
|
||||
key={inspectedElementID /* Force reset when selected Element changes */}>
|
||||
<div className={styles.TitleRow} data-testname="InspectedElement-Title">
|
||||
{strictModeBadge}
|
||||
|
||||
@@ -232,13 +233,11 @@ export default function InspectedElementWrapper(_: Props): React.Node {
|
||||
!!editorURL &&
|
||||
source != null &&
|
||||
symbolicatedSourcePromise != null && (
|
||||
<React.Suspense fallback={<Skeleton height={16} width={24} />}>
|
||||
<OpenInEditorButton
|
||||
editorURL={editorURL}
|
||||
source={source}
|
||||
symbolicatedSourcePromise={symbolicatedSourcePromise}
|
||||
/>
|
||||
</React.Suspense>
|
||||
<OpenInEditorButton
|
||||
editorURL={editorURL}
|
||||
source={source}
|
||||
symbolicatedSourcePromise={symbolicatedSourcePromise}
|
||||
/>
|
||||
)}
|
||||
|
||||
{canToggleError && (
|
||||
@@ -294,9 +293,6 @@ export default function InspectedElementWrapper(_: Props): React.Node {
|
||||
|
||||
{inspectedElement !== null && symbolicatedSourcePromise != null && (
|
||||
<InspectedElementView
|
||||
key={
|
||||
inspectedElementID /* Force reset when selected Element changes */
|
||||
}
|
||||
element={element}
|
||||
hookNames={hookNames}
|
||||
inspectedElement={inspectedElement}
|
||||
|
||||
@@ -36,7 +36,12 @@ function InspectedElementSourcePanel({
|
||||
<div className={styles.SourceHeaderRow}>
|
||||
<div className={styles.SourceHeader}>source</div>
|
||||
|
||||
<React.Suspense fallback={<Skeleton height={16} width={16} />}>
|
||||
<React.Suspense
|
||||
fallback={
|
||||
<Button disabled={true} title="Loading source maps...">
|
||||
<ButtonIcon type="copy" />
|
||||
</Button>
|
||||
}>
|
||||
<CopySourceButton
|
||||
source={source}
|
||||
symbolicatedSourcePromise={symbolicatedSourcePromise}
|
||||
|
||||
@@ -292,7 +292,7 @@ export default function InspectedElementSuspendedBy({
|
||||
inspectedElement,
|
||||
store,
|
||||
}: Props): React.Node {
|
||||
const {suspendedBy} = inspectedElement;
|
||||
const {suspendedBy, suspendedByRange} = inspectedElement;
|
||||
|
||||
// Skip the section if nothing suspended this component.
|
||||
if (suspendedBy == null || suspendedBy.length === 0) {
|
||||
@@ -306,6 +306,11 @@ export default function InspectedElementSuspendedBy({
|
||||
|
||||
let minTime = Infinity;
|
||||
let maxTime = -Infinity;
|
||||
if (suspendedByRange !== null) {
|
||||
// The range of the whole suspense boundary.
|
||||
minTime = suspendedByRange[0];
|
||||
maxTime = suspendedByRange[1];
|
||||
}
|
||||
for (let i = 0; i < suspendedBy.length; i++) {
|
||||
const asyncInfo: SerializedAsyncInfo = suspendedBy[i];
|
||||
if (asyncInfo.awaited.start < minTime) {
|
||||
|
||||
@@ -30,15 +30,13 @@ export default function InspectedElementSuspenseToggle({
|
||||
}: Props): React.Node {
|
||||
const {readOnly} = React.useContext(OptionsContext);
|
||||
|
||||
const {id, state, type} = inspectedElement;
|
||||
const {id, isSuspended, type} = inspectedElement;
|
||||
const canToggleSuspense = !readOnly && inspectedElement.canToggleSuspense;
|
||||
|
||||
if (type !== ElementTypeSuspense) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isSuspended = state !== null;
|
||||
|
||||
const toggleSuspense = (path: any, value: boolean) => {
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
if (rendererID !== null) {
|
||||
|
||||
@@ -24,3 +24,7 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.RenderedBySkeleton {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {enableStyleXFeatures} from 'react-devtools-feature-flags';
|
||||
import InspectedElementSourcePanel from './InspectedElementSourcePanel';
|
||||
import StackTraceView from './StackTraceView';
|
||||
import OwnerView from './OwnerView';
|
||||
import Skeleton from './Skeleton';
|
||||
|
||||
import styles from './InspectedElementView.css';
|
||||
|
||||
@@ -170,34 +171,40 @@ export default function InspectedElementView({
|
||||
className={styles.InspectedElementSection}
|
||||
data-testname="InspectedElementView-Owners">
|
||||
<div className={styles.OwnersHeader}>rendered by</div>
|
||||
<React.Suspense
|
||||
fallback={
|
||||
<div className={styles.RenderedBySkeleton}>
|
||||
<Skeleton height={16} width="40%" />
|
||||
</div>
|
||||
}>
|
||||
{showStack ? <StackTraceView stack={stack} /> : null}
|
||||
{showOwnersList &&
|
||||
owners?.map(owner => (
|
||||
<Fragment key={owner.id}>
|
||||
<OwnerView
|
||||
displayName={owner.displayName || 'Anonymous'}
|
||||
hocDisplayNames={owner.hocDisplayNames}
|
||||
environmentName={
|
||||
inspectedElement.env === owner.env ? null : owner.env
|
||||
}
|
||||
compiledWithForget={owner.compiledWithForget}
|
||||
id={owner.id}
|
||||
isInStore={store.containsElement(owner.id)}
|
||||
type={owner.type}
|
||||
/>
|
||||
{owner.stack != null && owner.stack.length > 0 ? (
|
||||
<StackTraceView stack={owner.stack} />
|
||||
) : null}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{showStack ? <StackTraceView stack={stack} /> : null}
|
||||
{showOwnersList &&
|
||||
owners?.map(owner => (
|
||||
<Fragment key={owner.id}>
|
||||
<OwnerView
|
||||
displayName={owner.displayName || 'Anonymous'}
|
||||
hocDisplayNames={owner.hocDisplayNames}
|
||||
environmentName={
|
||||
inspectedElement.env === owner.env ? null : owner.env
|
||||
}
|
||||
compiledWithForget={owner.compiledWithForget}
|
||||
id={owner.id}
|
||||
isInStore={store.containsElement(owner.id)}
|
||||
type={owner.type}
|
||||
/>
|
||||
{owner.stack != null && owner.stack.length > 0 ? (
|
||||
<StackTraceView stack={owner.stack} />
|
||||
) : null}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{rootType !== null && (
|
||||
<div className={styles.OwnersMetaField}>{rootType}</div>
|
||||
)}
|
||||
{rendererLabel !== null && (
|
||||
<div className={styles.OwnersMetaField}>{rendererLabel}</div>
|
||||
)}
|
||||
{rootType !== null && (
|
||||
<div className={styles.OwnersMetaField}>{rootType}</div>
|
||||
)}
|
||||
{rendererLabel !== null && (
|
||||
<div className={styles.OwnersMetaField}>{rendererLabel}</div>
|
||||
)}
|
||||
</React.Suspense>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import * as React from 'react';
|
||||
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import Button from '../Button';
|
||||
import Skeleton from './Skeleton';
|
||||
|
||||
import type {ReactFunctionLocation} from 'shared/ReactTypes';
|
||||
|
||||
@@ -27,7 +26,12 @@ function InspectedElementViewSourceButton({
|
||||
symbolicatedSourcePromise,
|
||||
}: Props): React.Node {
|
||||
return (
|
||||
<React.Suspense fallback={<Skeleton height={16} width={24} />}>
|
||||
<React.Suspense
|
||||
fallback={
|
||||
<Button disabled={true} title="Loading source maps...">
|
||||
<ButtonIcon type="view-source" />
|
||||
</Button>
|
||||
}>
|
||||
<ActualSourceButton
|
||||
source={source}
|
||||
symbolicatedSourcePromise={symbolicatedSourcePromise}
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
background-color: var(--color-dim);
|
||||
background-color: none;
|
||||
}
|
||||
50% {
|
||||
background-color: var(--color-dimmest)
|
||||
background-color: var(--color-dimmest);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,11 @@ type Props = {
|
||||
className?: string,
|
||||
};
|
||||
|
||||
function OpenInEditorButton({editorURL, source, className}: Props): React.Node {
|
||||
function ActualOpenInEditorButton({
|
||||
editorURL,
|
||||
source,
|
||||
className,
|
||||
}: Props): React.Node {
|
||||
let disable;
|
||||
if (source == null) {
|
||||
disable = true;
|
||||
@@ -68,4 +72,22 @@ function OpenInEditorButton({editorURL, source, className}: Props): React.Node {
|
||||
);
|
||||
}
|
||||
|
||||
function OpenInEditorButton({editorURL, source, className}: Props): React.Node {
|
||||
return (
|
||||
<React.Suspense
|
||||
fallback={
|
||||
<Button disabled={true} className={className}>
|
||||
<ButtonIcon type="editor" />
|
||||
<ButtonLabel>Loading source maps...</ButtonLabel>
|
||||
</Button>
|
||||
}>
|
||||
<ActualOpenInEditorButton
|
||||
editorURL={editorURL}
|
||||
source={source}
|
||||
className={className}
|
||||
/>
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
export default OpenInEditorButton;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.SuspenseRectsContainer {
|
||||
padding: .25rem;
|
||||
}
|
||||
|
||||
.SuspenseRect {
|
||||
fill: transparent;
|
||||
stroke: var(--color-background-selected);
|
||||
stroke-width: 1px;
|
||||
vector-effect: non-scaling-stroke;
|
||||
paint-order: stroke;
|
||||
}
|
||||
|
||||
[data-highlighted='true'] > .SuspenseRect {
|
||||
fill: var(--color-selected-tree-highlight-active);
|
||||
}
|
||||
200
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js
vendored
Normal file
200
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type Store from 'react-devtools-shared/src/devtools/store';
|
||||
import type {
|
||||
SuspenseNode,
|
||||
Rect,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
|
||||
import * as React from 'react';
|
||||
import {useContext} from 'react';
|
||||
import {
|
||||
TreeDispatcherContext,
|
||||
TreeStateContext,
|
||||
} from '../Components/TreeContext';
|
||||
import {StoreContext} from '../context';
|
||||
import {useHighlightHostInstance} from '../hooks';
|
||||
import styles from './SuspenseRects.css';
|
||||
import {SuspenseTreeStateContext} from './SuspenseTreeContext';
|
||||
|
||||
function SuspenseRect({rect}: {rect: Rect}): React$Node {
|
||||
return (
|
||||
<rect
|
||||
className={styles.SuspenseRect}
|
||||
x={rect.x}
|
||||
y={rect.y}
|
||||
width={rect.width}
|
||||
height={rect.height}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SuspenseRects({
|
||||
suspenseID,
|
||||
}: {
|
||||
suspenseID: SuspenseNode['id'],
|
||||
}): React$Node {
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const {inspectedElementID} = useContext(TreeStateContext);
|
||||
|
||||
const {highlightHostInstance, clearHighlightHostInstance} =
|
||||
useHighlightHostInstance();
|
||||
|
||||
const suspense = store.getSuspenseByID(suspenseID);
|
||||
if (suspense === null) {
|
||||
console.warn(`<Element> Could not find suspense node id ${suspenseID}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleClick(event: SyntheticMouseEvent<>) {
|
||||
if (event.defaultPrevented) {
|
||||
// Already clicked on an inner rect
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: suspenseID});
|
||||
}
|
||||
|
||||
function handlePointerOver(event: SyntheticPointerEvent<>) {
|
||||
if (event.defaultPrevented) {
|
||||
// Already hovered an inner rect
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
highlightHostInstance(suspenseID);
|
||||
}
|
||||
|
||||
function handlePointerLeave(event: SyntheticPointerEvent<>) {
|
||||
if (event.defaultPrevented) {
|
||||
// Already hovered an inner rect
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
clearHighlightHostInstance();
|
||||
}
|
||||
|
||||
// TODO: Use the nearest Suspense boundary
|
||||
const selected = inspectedElementID === suspenseID;
|
||||
|
||||
return (
|
||||
<g
|
||||
data-highlighted={selected}
|
||||
onClick={handleClick}
|
||||
onPointerOver={handlePointerOver}
|
||||
onPointerLeave={handlePointerLeave}>
|
||||
<title>{suspense.name}</title>
|
||||
{suspense.rects !== null &&
|
||||
suspense.rects.map((rect, index) => {
|
||||
return <SuspenseRect key={index} rect={rect} />;
|
||||
})}
|
||||
{suspense.children.map(childID => {
|
||||
return <SuspenseRects key={childID} suspenseID={childID} />;
|
||||
})}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
function getDocumentBoundingRect(
|
||||
store: Store,
|
||||
shells: $ReadOnlyArray<SuspenseNode['id']>,
|
||||
): Rect {
|
||||
if (shells.length === 0) {
|
||||
return {x: 0, y: 0, width: 0, height: 0};
|
||||
}
|
||||
|
||||
let minX = Number.POSITIVE_INFINITY;
|
||||
let minY = Number.POSITIVE_INFINITY;
|
||||
let maxX = Number.NEGATIVE_INFINITY;
|
||||
let maxY = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (let i = 0; i < shells.length; i++) {
|
||||
const shellID = shells[i];
|
||||
const shell = store.getSuspenseByID(shellID);
|
||||
if (shell === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rects = shell.rects;
|
||||
if (rects === null) {
|
||||
continue;
|
||||
}
|
||||
for (let j = 0; j < rects.length; j++) {
|
||||
const rect = rects[j];
|
||||
minX = Math.min(minX, rect.x);
|
||||
minY = Math.min(minY, rect.y);
|
||||
maxX = Math.max(maxX, rect.x + rect.width);
|
||||
maxY = Math.max(maxY, rect.y + rect.height);
|
||||
}
|
||||
}
|
||||
|
||||
if (minX === Number.POSITIVE_INFINITY) {
|
||||
// No rects found, return empty rect
|
||||
return {x: 0, y: 0, width: 0, height: 0};
|
||||
}
|
||||
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
};
|
||||
}
|
||||
|
||||
function SuspenseRectsShell({
|
||||
shellID,
|
||||
}: {
|
||||
shellID: SuspenseNode['id'],
|
||||
}): React$Node {
|
||||
const store = useContext(StoreContext);
|
||||
const shell = store.getSuspenseByID(shellID);
|
||||
if (shell === null) {
|
||||
console.warn(`<Element> Could not find suspense node id ${shellID}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<g>
|
||||
{shell.children.map(childID => {
|
||||
return <SuspenseRects key={childID} suspenseID={childID} />;
|
||||
})}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
function SuspenseRectsContainer(): React$Node {
|
||||
const store = useContext(StoreContext);
|
||||
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
|
||||
const {shells} = useContext(SuspenseTreeStateContext);
|
||||
|
||||
const boundingRect = getDocumentBoundingRect(store, shells);
|
||||
|
||||
const width = '100%';
|
||||
const boundingRectWidth = boundingRect.width;
|
||||
const height =
|
||||
(boundingRectWidth === 0 ? 0 : boundingRect.height / boundingRect.width) *
|
||||
100 +
|
||||
'%';
|
||||
|
||||
return (
|
||||
<div className={styles.SuspenseRectsContainer}>
|
||||
<svg
|
||||
style={{width, height}}
|
||||
viewBox={`${boundingRect.x} ${boundingRect.y} ${boundingRect.width} ${boundingRect.height}`}>
|
||||
{shells.map(shellID => {
|
||||
return <SuspenseRectsShell key={shellID} shellID={shellID} />;
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SuspenseRectsContainer;
|
||||
@@ -19,6 +19,7 @@ import InspectedElementErrorBoundary from '../Components/InspectedElementErrorBo
|
||||
import InspectedElement from '../Components/InspectedElement';
|
||||
import portaledContent from '../portaledContent';
|
||||
import styles from './SuspenseTab.css';
|
||||
import SuspenseRects from './SuspenseRects';
|
||||
import SuspenseTreeList from './SuspenseTreeList';
|
||||
import Button from '../Button';
|
||||
|
||||
@@ -48,10 +49,6 @@ function SuspenseTimeline() {
|
||||
return <div className={styles.Timeline}>timeline</div>;
|
||||
}
|
||||
|
||||
function SuspenseRects() {
|
||||
return <div>rects</div>;
|
||||
}
|
||||
|
||||
function ToggleTreeList({
|
||||
dispatch,
|
||||
state,
|
||||
|
||||
@@ -17,9 +17,12 @@ import {
|
||||
useMemo,
|
||||
useReducer,
|
||||
} from 'react';
|
||||
import type {SuspenseNode} from '../../../frontend/types';
|
||||
import {StoreContext} from '../context';
|
||||
|
||||
export type SuspenseTreeState = {};
|
||||
export type SuspenseTreeState = {
|
||||
shells: $ReadOnlyArray<SuspenseNode['id']>,
|
||||
};
|
||||
|
||||
type ACTION_HANDLE_SUSPENSE_TREE_MUTATION = {
|
||||
type: 'HANDLE_SUSPENSE_TREE_MUTATION',
|
||||
@@ -56,7 +59,7 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
||||
const {type} = action;
|
||||
switch (type) {
|
||||
case 'HANDLE_SUSPENSE_TREE_MUTATION':
|
||||
return {...state};
|
||||
return {...state, shells: store.roots};
|
||||
default:
|
||||
throw new Error(`Unrecognized action "${type}"`);
|
||||
}
|
||||
@@ -64,7 +67,10 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
||||
[],
|
||||
);
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {});
|
||||
const initialState: SuspenseTreeState = {
|
||||
shells: store.roots,
|
||||
};
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const transitionDispatch = useMemo(
|
||||
() => (action: SuspenseTreeAction) =>
|
||||
startTransition(() => {
|
||||
|
||||
@@ -264,6 +264,8 @@ export type InspectedElement = {
|
||||
|
||||
// Is this Suspense, and can its value be overridden now?
|
||||
canToggleSuspense: boolean,
|
||||
// If this Element is suspended. Currently only set on Suspense boundaries.
|
||||
isSuspended: boolean | null,
|
||||
|
||||
// Does the component have legacy context attached to it.
|
||||
hasLegacyContext: boolean,
|
||||
@@ -279,6 +281,8 @@ export type InspectedElement = {
|
||||
|
||||
// Things that suspended this Instances
|
||||
suspendedBy: Object,
|
||||
// Minimum start time to maximum end time + a potential (not actual) throttle, within the nearest boundary.
|
||||
suspendedByRange: null | [number, number],
|
||||
|
||||
// List of owners
|
||||
owners: Array<SerializedElement> | null,
|
||||
|
||||
@@ -38,9 +38,15 @@ import hasOwnProperty from 'shared/hasOwnProperty';
|
||||
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
|
||||
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
|
||||
import {
|
||||
isFiberContainedBy,
|
||||
isFiberContainedByFragment,
|
||||
isFiberFollowing,
|
||||
isFiberPreceding,
|
||||
isFragmentContainedByFiber,
|
||||
traverseFragmentInstance,
|
||||
getFragmentParentHostFiber,
|
||||
getInstanceFromHostFiber,
|
||||
traverseFragmentInstanceDeeply,
|
||||
fiberIsPortaledIntoHost,
|
||||
} from 'react-reconciler/src/ReactFiberTreeReflection';
|
||||
|
||||
export {
|
||||
@@ -63,13 +69,7 @@ import {
|
||||
markNodeAsHoistable,
|
||||
isOwnedInstance,
|
||||
} from './ReactDOMComponentTree';
|
||||
import {
|
||||
traverseFragmentInstance,
|
||||
getFragmentParentHostFiber,
|
||||
getNextSiblingHostFiber,
|
||||
getInstanceFromHostFiber,
|
||||
traverseFragmentInstanceDeeply,
|
||||
} from 'react-reconciler/src/ReactFiberTreeReflection';
|
||||
import {compareDocumentPositionForEmptyFragment} from 'shared/ReactDOMFragmentRefShared';
|
||||
|
||||
export {detachDeletedInstance};
|
||||
import {hasRole} from './DOMAccessibilityRoles';
|
||||
@@ -3052,58 +3052,71 @@ FragmentInstance.prototype.compareDocumentPosition = function (
|
||||
}
|
||||
const children: Array<Fiber> = [];
|
||||
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
|
||||
const parentHostInstance =
|
||||
getInstanceFromHostFiber<Instance>(parentHostFiber);
|
||||
|
||||
let result = Node.DOCUMENT_POSITION_DISCONNECTED;
|
||||
if (children.length === 0) {
|
||||
// If the fragment has no children, we can use the parent and
|
||||
// siblings to determine a position.
|
||||
const parentHostInstance =
|
||||
getInstanceFromHostFiber<Instance>(parentHostFiber);
|
||||
const parentResult = parentHostInstance.compareDocumentPosition(otherNode);
|
||||
result = parentResult;
|
||||
if (parentHostInstance === otherNode) {
|
||||
result = Node.DOCUMENT_POSITION_CONTAINS;
|
||||
} else {
|
||||
if (parentResult & Node.DOCUMENT_POSITION_CONTAINED_BY) {
|
||||
// otherNode is one of the fragment's siblings. Use the next
|
||||
// sibling to determine if its preceding or following.
|
||||
const nextSiblingFiber = getNextSiblingHostFiber(this._fragmentFiber);
|
||||
if (nextSiblingFiber === null) {
|
||||
result = Node.DOCUMENT_POSITION_PRECEDING;
|
||||
} else {
|
||||
const nextSiblingInstance =
|
||||
getInstanceFromHostFiber<Instance>(nextSiblingFiber);
|
||||
const nextSiblingResult =
|
||||
nextSiblingInstance.compareDocumentPosition(otherNode);
|
||||
if (
|
||||
nextSiblingResult === 0 ||
|
||||
nextSiblingResult & Node.DOCUMENT_POSITION_FOLLOWING
|
||||
) {
|
||||
result = Node.DOCUMENT_POSITION_FOLLOWING;
|
||||
} else {
|
||||
result = Node.DOCUMENT_POSITION_PRECEDING;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result |= Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
|
||||
return result;
|
||||
return compareDocumentPositionForEmptyFragment(
|
||||
this._fragmentFiber,
|
||||
parentHostInstance,
|
||||
otherNode,
|
||||
getInstanceFromHostFiber,
|
||||
);
|
||||
}
|
||||
|
||||
const firstElement = getInstanceFromHostFiber<Instance>(children[0]);
|
||||
const lastElement = getInstanceFromHostFiber<Instance>(
|
||||
children[children.length - 1],
|
||||
);
|
||||
|
||||
// If the fragment has been portaled into another host instance, we need to
|
||||
// our best guess is to use the parent of the child instance, rather than
|
||||
// the fiber tree host parent.
|
||||
const firstInstance = getInstanceFromHostFiber<Instance>(children[0]);
|
||||
const parentHostInstanceFromDOM = fiberIsPortaledIntoHost(this._fragmentFiber)
|
||||
? (firstInstance.parentElement: ?Instance)
|
||||
: parentHostInstance;
|
||||
|
||||
if (parentHostInstanceFromDOM == null) {
|
||||
return Node.DOCUMENT_POSITION_DISCONNECTED;
|
||||
}
|
||||
|
||||
// Check if first and last element are actually in the expected document position
|
||||
// before relying on them as source of truth for other contained elements
|
||||
const firstElementIsContained =
|
||||
parentHostInstanceFromDOM.compareDocumentPosition(firstElement) &
|
||||
Node.DOCUMENT_POSITION_CONTAINED_BY;
|
||||
const lastElementIsContained =
|
||||
parentHostInstanceFromDOM.compareDocumentPosition(lastElement) &
|
||||
Node.DOCUMENT_POSITION_CONTAINED_BY;
|
||||
const firstResult = firstElement.compareDocumentPosition(otherNode);
|
||||
const lastResult = lastElement.compareDocumentPosition(otherNode);
|
||||
|
||||
const otherNodeIsFirstOrLastChild =
|
||||
(firstElementIsContained && firstElement === otherNode) ||
|
||||
(lastElementIsContained && lastElement === otherNode);
|
||||
const otherNodeIsFirstOrLastChildDisconnected =
|
||||
(!firstElementIsContained && firstElement === otherNode) ||
|
||||
(!lastElementIsContained && lastElement === otherNode);
|
||||
const otherNodeIsWithinFirstOrLastChild =
|
||||
firstResult & Node.DOCUMENT_POSITION_CONTAINED_BY ||
|
||||
lastResult & Node.DOCUMENT_POSITION_CONTAINED_BY;
|
||||
const otherNodeIsBetweenFirstAndLastChildren =
|
||||
firstElementIsContained &&
|
||||
lastElementIsContained &&
|
||||
firstResult & Node.DOCUMENT_POSITION_FOLLOWING &&
|
||||
lastResult & Node.DOCUMENT_POSITION_PRECEDING;
|
||||
|
||||
let result = Node.DOCUMENT_POSITION_DISCONNECTED;
|
||||
if (
|
||||
(firstResult & Node.DOCUMENT_POSITION_FOLLOWING &&
|
||||
lastResult & Node.DOCUMENT_POSITION_PRECEDING) ||
|
||||
otherNode === firstElement ||
|
||||
otherNode === lastElement
|
||||
otherNodeIsFirstOrLastChild ||
|
||||
otherNodeIsWithinFirstOrLastChild ||
|
||||
otherNodeIsBetweenFirstAndLastChildren
|
||||
) {
|
||||
result = Node.DOCUMENT_POSITION_CONTAINED_BY;
|
||||
} else if (otherNodeIsFirstOrLastChildDisconnected) {
|
||||
// otherNode has been portaled into another container
|
||||
result = Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
|
||||
} else {
|
||||
result = firstResult;
|
||||
}
|
||||
@@ -3141,7 +3154,9 @@ function validateDocumentPositionWithFiberTree(
|
||||
): boolean {
|
||||
const otherFiber = getClosestInstanceFromNode(otherNode);
|
||||
if (documentPosition & Node.DOCUMENT_POSITION_CONTAINED_BY) {
|
||||
return !!otherFiber && isFiberContainedBy(fragmentFiber, otherFiber);
|
||||
return (
|
||||
!!otherFiber && isFiberContainedByFragment(otherFiber, fragmentFiber)
|
||||
);
|
||||
}
|
||||
if (documentPosition & Node.DOCUMENT_POSITION_CONTAINS) {
|
||||
if (otherFiber === null) {
|
||||
@@ -3149,7 +3164,7 @@ function validateDocumentPositionWithFiberTree(
|
||||
const ownerDocument = otherNode.ownerDocument;
|
||||
return otherNode === ownerDocument || otherNode === ownerDocument.body;
|
||||
}
|
||||
return isFiberContainedBy(otherFiber, fragmentFiber);
|
||||
return isFragmentContainedByFiber(fragmentFiber, otherFiber);
|
||||
}
|
||||
if (documentPosition & Node.DOCUMENT_POSITION_PRECEDING) {
|
||||
return (
|
||||
|
||||
@@ -1197,14 +1197,14 @@ describe('FragmentRefs', () => {
|
||||
|
||||
function Test() {
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<div ref={beforeRef} />
|
||||
<div ref={containerRef} id="container">
|
||||
<div ref={beforeRef} id="before" />
|
||||
<React.Fragment ref={fragmentRef}>
|
||||
<div ref={firstChildRef} />
|
||||
<div ref={middleChildRef} />
|
||||
<div ref={lastChildRef} />
|
||||
<div ref={firstChildRef} id="first" />
|
||||
<div ref={middleChildRef} id="middle" />
|
||||
<div ref={lastChildRef} id="last" />
|
||||
</React.Fragment>
|
||||
<div ref={afterRef} />
|
||||
<div ref={afterRef} id="after" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1289,7 +1289,7 @@ describe('FragmentRefs', () => {
|
||||
},
|
||||
);
|
||||
|
||||
// containerRef preceds and contains the fragment
|
||||
// containerRef precedes and contains the fragment
|
||||
expectPosition(
|
||||
fragmentRef.current.compareDocumentPosition(containerRef.current),
|
||||
{
|
||||
@@ -1328,7 +1328,7 @@ describe('FragmentRefs', () => {
|
||||
function Test() {
|
||||
return (
|
||||
<div id="container" ref={containerRef}>
|
||||
<div>
|
||||
<div id="innercontainer">
|
||||
<div ref={beforeRef} id="before" />
|
||||
<React.Fragment ref={fragmentRef}>
|
||||
<div ref={onlyChildRef} id="within" />
|
||||
@@ -1491,6 +1491,77 @@ describe('FragmentRefs', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs
|
||||
it('handles nested children', async () => {
|
||||
const fragmentRef = React.createRef();
|
||||
const nestedFragmentRef = React.createRef();
|
||||
const childARef = React.createRef();
|
||||
const childBRef = React.createRef();
|
||||
const childCRef = React.createRef();
|
||||
document.body.appendChild(container);
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
function Child() {
|
||||
return (
|
||||
<div ref={childCRef} id="C">
|
||||
C
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Test() {
|
||||
return (
|
||||
<React.Fragment ref={fragmentRef}>
|
||||
<div ref={childARef} id="A">
|
||||
A
|
||||
</div>
|
||||
<React.Fragment ref={nestedFragmentRef}>
|
||||
<div ref={childBRef} id="B">
|
||||
B
|
||||
</div>
|
||||
</React.Fragment>
|
||||
<Child />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
await act(() => root.render(<Test />));
|
||||
|
||||
expectPosition(
|
||||
fragmentRef.current.compareDocumentPosition(childARef.current),
|
||||
{
|
||||
preceding: false,
|
||||
following: false,
|
||||
contains: false,
|
||||
containedBy: true,
|
||||
disconnected: false,
|
||||
implementationSpecific: false,
|
||||
},
|
||||
);
|
||||
expectPosition(
|
||||
fragmentRef.current.compareDocumentPosition(childBRef.current),
|
||||
{
|
||||
preceding: false,
|
||||
following: false,
|
||||
contains: false,
|
||||
containedBy: true,
|
||||
disconnected: false,
|
||||
implementationSpecific: false,
|
||||
},
|
||||
);
|
||||
expectPosition(
|
||||
fragmentRef.current.compareDocumentPosition(childCRef.current),
|
||||
{
|
||||
preceding: false,
|
||||
following: false,
|
||||
contains: false,
|
||||
containedBy: true,
|
||||
disconnected: false,
|
||||
implementationSpecific: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs
|
||||
it('returns disconnected for comparison with an unmounted fragment instance', async () => {
|
||||
const fragmentRef = React.createRef();
|
||||
@@ -1551,11 +1622,11 @@ describe('FragmentRefs', () => {
|
||||
|
||||
function Test() {
|
||||
return (
|
||||
<div>
|
||||
{createPortal(<div ref={portaledSiblingRef} />, document.body)}
|
||||
<div id="wrapper">
|
||||
{createPortal(<div ref={portaledSiblingRef} id="A" />, container)}
|
||||
<Fragment ref={fragmentRef}>
|
||||
{createPortal(<div ref={portaledChildRef} />, document.body)}
|
||||
<div />
|
||||
{createPortal(<div ref={portaledChildRef} id="B" />, container)}
|
||||
<div id="C" />
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
@@ -1600,6 +1671,8 @@ describe('FragmentRefs', () => {
|
||||
const childARef = React.createRef();
|
||||
const childBRef = React.createRef();
|
||||
const childCRef = React.createRef();
|
||||
const childDRef = React.createRef();
|
||||
const childERef = React.createRef();
|
||||
|
||||
function Test() {
|
||||
const [c, setC] = React.useState(false);
|
||||
@@ -1612,23 +1685,30 @@ describe('FragmentRefs', () => {
|
||||
{createPortal(
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div id="A" ref={childARef} />
|
||||
{c ? <div id="C" ref={childCRef} /> : null}
|
||||
{c ? (
|
||||
<div id="C" ref={childCRef}>
|
||||
<div id="D" ref={childDRef} />
|
||||
</div>
|
||||
) : null}
|
||||
</Fragment>,
|
||||
document.body,
|
||||
)}
|
||||
{createPortal(<p id="B" ref={childBRef} />, document.body)}
|
||||
<div id="E" ref={childERef} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
await act(() => root.render(<Test />));
|
||||
|
||||
// Due to effect, order is A->B->C
|
||||
expect(document.body.innerHTML).toBe(
|
||||
'<div></div>' +
|
||||
// Due to effect, order is E / A->B->C->D
|
||||
expect(document.body.outerHTML).toBe(
|
||||
'<body>' +
|
||||
'<div><div id="E"></div></div>' +
|
||||
'<div id="A"></div>' +
|
||||
'<p id="B"></p>' +
|
||||
'<div id="C"></div>',
|
||||
'<div id="C"><div id="D"></div></div>' +
|
||||
'</body>',
|
||||
);
|
||||
|
||||
expectPosition(
|
||||
@@ -1642,7 +1722,6 @@ describe('FragmentRefs', () => {
|
||||
implementationSpecific: false,
|
||||
},
|
||||
);
|
||||
|
||||
expectPosition(
|
||||
fragmentRef.current.compareDocumentPosition(childARef.current),
|
||||
{
|
||||
@@ -1654,6 +1733,7 @@ describe('FragmentRefs', () => {
|
||||
implementationSpecific: false,
|
||||
},
|
||||
);
|
||||
// Contained by in DOM, but following in React tree
|
||||
expectPosition(
|
||||
fragmentRef.current.compareDocumentPosition(childBRef.current),
|
||||
{
|
||||
@@ -1676,6 +1756,29 @@ describe('FragmentRefs', () => {
|
||||
implementationSpecific: false,
|
||||
},
|
||||
);
|
||||
expectPosition(
|
||||
fragmentRef.current.compareDocumentPosition(childDRef.current),
|
||||
{
|
||||
preceding: false,
|
||||
following: false,
|
||||
contains: false,
|
||||
containedBy: true,
|
||||
disconnected: false,
|
||||
implementationSpecific: false,
|
||||
},
|
||||
);
|
||||
// Preceding DOM but following in React tree
|
||||
expectPosition(
|
||||
fragmentRef.current.compareDocumentPosition(childERef.current),
|
||||
{
|
||||
preceding: false,
|
||||
following: false,
|
||||
contains: false,
|
||||
containedBy: false,
|
||||
disconnected: false,
|
||||
implementationSpecific: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
||||
import {HostText} from 'react-reconciler/src/ReactWorkTags';
|
||||
import {
|
||||
getFragmentParentHostFiber,
|
||||
getInstanceFromHostFiber,
|
||||
traverseFragmentInstance,
|
||||
} from 'react-reconciler/src/ReactFiberTreeReflection';
|
||||
@@ -59,6 +60,7 @@ const {
|
||||
} = nativeFabricUIManager;
|
||||
|
||||
import {getClosestInstanceFromNode} from './ReactFabricComponentTree';
|
||||
import {compareDocumentPositionForEmptyFragment} from 'shared/ReactDOMFragmentRefShared';
|
||||
|
||||
import {
|
||||
getInspectorDataForViewTag,
|
||||
@@ -87,7 +89,7 @@ const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
|
||||
let nextReactTag = 2;
|
||||
|
||||
type InternalInstanceHandle = Object;
|
||||
type Node = Object;
|
||||
|
||||
export type Type = string;
|
||||
export type Props = Object;
|
||||
export type Instance = {
|
||||
@@ -344,6 +346,15 @@ export function getPublicInstanceFromInternalInstanceHandle(
|
||||
return getPublicInstance(elementInstance);
|
||||
}
|
||||
|
||||
function getPublicInstanceFromHostFiber(fiber: Fiber): PublicInstance {
|
||||
const instance = getInstanceFromHostFiber<Instance>(fiber);
|
||||
const publicInstance = getPublicInstance(instance);
|
||||
if (publicInstance == null) {
|
||||
throw new Error('Expected to find a host node. This is a bug in React.');
|
||||
}
|
||||
return publicInstance;
|
||||
}
|
||||
|
||||
export function prepareForCommit(containerInfo: Container): null | Object {
|
||||
// Noop
|
||||
return null;
|
||||
@@ -610,6 +621,7 @@ export type FragmentInstanceType = {
|
||||
_observers: null | Set<IntersectionObserver>,
|
||||
observeUsing: (observer: IntersectionObserver) => void,
|
||||
unobserveUsing: (observer: IntersectionObserver) => void,
|
||||
compareDocumentPosition: (otherNode: PublicInstance) => number,
|
||||
};
|
||||
|
||||
function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) {
|
||||
@@ -629,12 +641,8 @@ FragmentInstance.prototype.observeUsing = function (
|
||||
traverseFragmentInstance(this._fragmentFiber, observeChild, observer);
|
||||
};
|
||||
function observeChild(child: Fiber, observer: IntersectionObserver) {
|
||||
const instance = getInstanceFromHostFiber<Instance>(child);
|
||||
const publicInstance = getPublicInstance(instance);
|
||||
if (publicInstance == null) {
|
||||
throw new Error('Expected to find a host node. This is a bug in React.');
|
||||
}
|
||||
// $FlowFixMe[incompatible-call] Element types are behind a flag in RN
|
||||
const publicInstance = getPublicInstanceFromHostFiber(child);
|
||||
// $FlowFixMe[incompatible-call] DOM types expect Element
|
||||
observer.observe(publicInstance);
|
||||
return false;
|
||||
}
|
||||
@@ -656,16 +664,72 @@ FragmentInstance.prototype.unobserveUsing = function (
|
||||
}
|
||||
};
|
||||
function unobserveChild(child: Fiber, observer: IntersectionObserver) {
|
||||
const instance = getInstanceFromHostFiber<Instance>(child);
|
||||
const publicInstance = getPublicInstance(instance);
|
||||
if (publicInstance == null) {
|
||||
throw new Error('Expected to find a host node. This is a bug in React.');
|
||||
}
|
||||
// $FlowFixMe[incompatible-call] Element types are behind a flag in RN
|
||||
const publicInstance = getPublicInstanceFromHostFiber(child);
|
||||
// $FlowFixMe[incompatible-call] DOM types expect Element
|
||||
observer.unobserve(publicInstance);
|
||||
return false;
|
||||
}
|
||||
|
||||
// $FlowFixMe[prop-missing]
|
||||
FragmentInstance.prototype.compareDocumentPosition = function (
|
||||
this: FragmentInstanceType,
|
||||
otherNode: PublicInstance,
|
||||
): number {
|
||||
const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
|
||||
if (parentHostFiber === null) {
|
||||
return Node.DOCUMENT_POSITION_DISCONNECTED;
|
||||
}
|
||||
const parentHostInstance = getPublicInstanceFromHostFiber(parentHostFiber);
|
||||
const children: Array<Fiber> = [];
|
||||
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
|
||||
if (children.length === 0) {
|
||||
return compareDocumentPositionForEmptyFragment(
|
||||
this._fragmentFiber,
|
||||
parentHostInstance,
|
||||
otherNode,
|
||||
getPublicInstanceFromHostFiber,
|
||||
);
|
||||
}
|
||||
|
||||
const firstInstance = getPublicInstanceFromHostFiber(children[0]);
|
||||
const lastInstance = getPublicInstanceFromHostFiber(
|
||||
children[children.length - 1],
|
||||
);
|
||||
|
||||
// $FlowFixMe[incompatible-use] Fabric PublicInstance is opaque
|
||||
// $FlowFixMe[prop-missing]
|
||||
const firstResult = firstInstance.compareDocumentPosition(otherNode);
|
||||
// $FlowFixMe[incompatible-use] Fabric PublicInstance is opaque
|
||||
// $FlowFixMe[prop-missing]
|
||||
const lastResult = lastInstance.compareDocumentPosition(otherNode);
|
||||
|
||||
const otherNodeIsFirstOrLastChild =
|
||||
firstInstance === otherNode || lastInstance === otherNode;
|
||||
const otherNodeIsWithinFirstOrLastChild =
|
||||
firstResult & Node.DOCUMENT_POSITION_CONTAINED_BY ||
|
||||
lastResult & Node.DOCUMENT_POSITION_CONTAINED_BY;
|
||||
const otherNodeIsBetweenFirstAndLastChildren =
|
||||
firstResult & Node.DOCUMENT_POSITION_FOLLOWING &&
|
||||
lastResult & Node.DOCUMENT_POSITION_PRECEDING;
|
||||
let result;
|
||||
if (
|
||||
otherNodeIsFirstOrLastChild ||
|
||||
otherNodeIsWithinFirstOrLastChild ||
|
||||
otherNodeIsBetweenFirstAndLastChildren
|
||||
) {
|
||||
result = Node.DOCUMENT_POSITION_CONTAINED_BY;
|
||||
} else {
|
||||
result = firstResult;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
function collectChildren(child: Fiber, collection: Array<Fiber>): boolean {
|
||||
collection.push(child);
|
||||
return false;
|
||||
}
|
||||
|
||||
export function createFragmentInstance(
|
||||
fragmentFiber: Fiber,
|
||||
): FragmentInstanceType {
|
||||
|
||||
@@ -1894,7 +1894,6 @@ function attachSuspenseRetryListeners(
|
||||
const retryCache = getRetryCache(finishedWork);
|
||||
wakeables.forEach(wakeable => {
|
||||
// Memoize using the boundary fiber to prevent redundant listeners.
|
||||
const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);
|
||||
if (!retryCache.has(wakeable)) {
|
||||
retryCache.add(wakeable);
|
||||
|
||||
@@ -1911,6 +1910,7 @@ function attachSuspenseRetryListeners(
|
||||
}
|
||||
}
|
||||
|
||||
const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);
|
||||
wakeable.then(retry, retry);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@ export function injectInternals(internals: Object): boolean {
|
||||
} catch (err) {
|
||||
// Catch all errors because it is unsafe to throw during initialization.
|
||||
if (__DEV__) {
|
||||
console.error('React instrumentation encountered an error: %s.', err);
|
||||
console.error('React instrumentation encountered an error: %o.', err);
|
||||
}
|
||||
}
|
||||
if (hook.checkDCE) {
|
||||
@@ -101,7 +101,7 @@ export function onScheduleRoot(root: FiberRoot, children: ReactNodeList) {
|
||||
} catch (err) {
|
||||
if (__DEV__ && !hasLoggedError) {
|
||||
hasLoggedError = true;
|
||||
console.error('React instrumentation encountered an error: %s', err);
|
||||
console.error('React instrumentation encountered an error: %o', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,7 +144,7 @@ export function onCommitRoot(root: FiberRoot, eventPriority: EventPriority) {
|
||||
if (__DEV__) {
|
||||
if (!hasLoggedError) {
|
||||
hasLoggedError = true;
|
||||
console.error('React instrumentation encountered an error: %s', err);
|
||||
console.error('React instrumentation encountered an error: %o', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ export function onPostCommitRoot(root: FiberRoot) {
|
||||
if (__DEV__) {
|
||||
if (!hasLoggedError) {
|
||||
hasLoggedError = true;
|
||||
console.error('React instrumentation encountered an error: %s', err);
|
||||
console.error('React instrumentation encountered an error: %o', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,7 @@ export function onCommitUnmount(fiber: Fiber) {
|
||||
if (__DEV__) {
|
||||
if (!hasLoggedError) {
|
||||
hasLoggedError = true;
|
||||
console.error('React instrumentation encountered an error: %s', err);
|
||||
console.error('React instrumentation encountered an error: %o', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,7 +199,7 @@ export function setIsStrictModeForDevtools(newIsStrictMode: boolean) {
|
||||
if (__DEV__) {
|
||||
if (!hasLoggedError) {
|
||||
hasLoggedError = true;
|
||||
console.error('React instrumentation encountered an error: %s', err);
|
||||
console.error('React instrumentation encountered an error: %o', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
ActivityComponent,
|
||||
SuspenseComponent,
|
||||
OffscreenComponent,
|
||||
Fragment,
|
||||
} from './ReactWorkTags';
|
||||
import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
|
||||
|
||||
@@ -405,6 +406,21 @@ export function getFragmentParentHostFiber(fiber: Fiber): null | Fiber {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function fiberIsPortaledIntoHost(fiber: Fiber): boolean {
|
||||
let foundPortalParent = false;
|
||||
let parent = fiber.return;
|
||||
while (parent !== null) {
|
||||
if (parent.tag === HostPortal) {
|
||||
foundPortalParent = true;
|
||||
}
|
||||
if (parent.tag === HostRoot || parent.tag === HostComponent) {
|
||||
break;
|
||||
}
|
||||
parent = parent.return;
|
||||
}
|
||||
return foundPortalParent;
|
||||
}
|
||||
|
||||
export function getInstanceFromHostFiber<I>(fiber: Fiber): I {
|
||||
switch (fiber.tag) {
|
||||
case HostComponent:
|
||||
@@ -443,22 +459,38 @@ function findNextSibling(child: Fiber): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isFiberContainedBy(
|
||||
maybeChild: Fiber,
|
||||
maybeParent: Fiber,
|
||||
export function isFiberContainedByFragment(
|
||||
fiber: Fiber,
|
||||
fragmentFiber: Fiber,
|
||||
): boolean {
|
||||
let parent = maybeParent.return;
|
||||
if (parent === maybeChild || parent === maybeChild.alternate) {
|
||||
return true;
|
||||
}
|
||||
while (parent !== null && parent !== maybeChild) {
|
||||
let current: Fiber | null = fiber;
|
||||
while (current !== null) {
|
||||
if (
|
||||
(parent.tag === HostComponent || parent.tag === HostRoot) &&
|
||||
(parent.return === maybeChild || parent.return === maybeChild.alternate)
|
||||
current.tag === Fragment &&
|
||||
(current === fragmentFiber || current.alternate === fragmentFiber)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
parent = parent.return;
|
||||
current = current.return;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isFragmentContainedByFiber(
|
||||
fragmentFiber: Fiber,
|
||||
otherFiber: Fiber,
|
||||
): boolean {
|
||||
let current: Fiber | null = fragmentFiber;
|
||||
const fiberHostParent: Fiber | null =
|
||||
getFragmentParentHostFiber(fragmentFiber);
|
||||
while (current !== null) {
|
||||
if (
|
||||
(current.tag === HostComponent || current.tag === HostRoot) &&
|
||||
(current === fiberHostParent || current.alternate === fiberHostParent)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
current = current.return;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
21
packages/react-server/src/ReactFlightServer.js
vendored
21
packages/react-server/src/ReactFlightServer.js
vendored
@@ -3354,6 +3354,27 @@ function renderModelDestructive(
|
||||
task.debugOwner = element._owner;
|
||||
task.debugStack = element._debugStack;
|
||||
task.debugTask = element._debugTask;
|
||||
if (
|
||||
element._owner === undefined ||
|
||||
element._debugStack === undefined ||
|
||||
element._debugTask === undefined
|
||||
) {
|
||||
let key = '';
|
||||
if (element.key !== null) {
|
||||
key = ' key="' + element.key + '"';
|
||||
}
|
||||
|
||||
console.error(
|
||||
'Attempted to render <%s%s> without development properties. ' +
|
||||
'This is not supported. It can happen if:' +
|
||||
'\n- The element is created with a production version of React but rendered in development.' +
|
||||
'\n- The element was cloned with a custom function instead of `React.cloneElement`.\n' +
|
||||
'The props of this element may help locate this element: %o',
|
||||
element.type,
|
||||
key,
|
||||
element.props,
|
||||
);
|
||||
}
|
||||
// TODO: Pop this. Since we currently don't have a point where we can pop the stack
|
||||
// this debug information will be used for errors inside sibling properties that
|
||||
// are not elements. Leading to the wrong attribution on the server. We could fix
|
||||
|
||||
@@ -36,6 +36,7 @@ let ReactNoopFlightServer;
|
||||
let Scheduler;
|
||||
let advanceTimersByTime;
|
||||
let assertLog;
|
||||
let assertConsoleErrorDev;
|
||||
|
||||
describe('ReactFlight', () => {
|
||||
beforeEach(() => {
|
||||
@@ -64,6 +65,7 @@ describe('ReactFlight', () => {
|
||||
Scheduler = require('scheduler');
|
||||
const InternalTestUtils = require('internal-test-utils');
|
||||
assertLog = InternalTestUtils.assertLog;
|
||||
assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -175,4 +177,26 @@ describe('ReactFlight', () => {
|
||||
stackTwo: '\n in OwnerStackDelayed (at **)' + '\n in App (at **)',
|
||||
});
|
||||
});
|
||||
|
||||
it('logs an error when prod elements are rendered', async () => {
|
||||
const element = ReactServer.createElement('span', {
|
||||
key: 'one',
|
||||
children: 'Free!',
|
||||
});
|
||||
ReactNoopFlightServer.render(
|
||||
// bad clone
|
||||
{...element},
|
||||
);
|
||||
|
||||
assertConsoleErrorDev([
|
||||
[
|
||||
'Attempted to render <span key="one"> without development properties. This is not supported. It can happen if:' +
|
||||
'\n- The element is created with a production version of React but rendered in development.' +
|
||||
'\n- The element was cloned with a custom function instead of `React.cloneElement`.\n' +
|
||||
"The props of this element may help locate this element: { children: 'Free!', [key]: [Getter] }",
|
||||
{withoutStack: true},
|
||||
],
|
||||
"TypeError: Cannot read properties of undefined (reading 'stack')",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
58
packages/shared/ReactDOMFragmentRefShared.js
Normal file
58
packages/shared/ReactDOMFragmentRefShared.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Shared logic for Fragment Ref operations for DOM and Fabric configs
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
||||
|
||||
import {getNextSiblingHostFiber} from 'react-reconciler/src/ReactFiberTreeReflection';
|
||||
|
||||
export function compareDocumentPositionForEmptyFragment<TPublicInstance>(
|
||||
fragmentFiber: Fiber,
|
||||
parentHostInstance: TPublicInstance,
|
||||
otherNode: TPublicInstance,
|
||||
getPublicInstance: (fiber: Fiber) => TPublicInstance,
|
||||
): number {
|
||||
let result;
|
||||
// If the fragment has no children, we can use the parent and
|
||||
// siblings to determine a position.
|
||||
// $FlowFixMe[incompatible-use] Fabric PublicInstance is opaque
|
||||
// $FlowFixMe[prop-missing]
|
||||
const parentResult = parentHostInstance.compareDocumentPosition(otherNode);
|
||||
result = parentResult;
|
||||
if (parentHostInstance === otherNode) {
|
||||
result = Node.DOCUMENT_POSITION_CONTAINS;
|
||||
} else {
|
||||
if (parentResult & Node.DOCUMENT_POSITION_CONTAINED_BY) {
|
||||
// otherNode is one of the fragment's siblings. Use the next
|
||||
// sibling to determine if its preceding or following.
|
||||
const nextSiblingFiber = getNextSiblingHostFiber(fragmentFiber);
|
||||
if (nextSiblingFiber === null) {
|
||||
result = Node.DOCUMENT_POSITION_PRECEDING;
|
||||
} else {
|
||||
const nextSiblingInstance = getPublicInstance(nextSiblingFiber);
|
||||
const nextSiblingResult =
|
||||
// $FlowFixMe[incompatible-use] Fabric PublicInstance is opaque
|
||||
// $FlowFixMe[prop-missing]
|
||||
nextSiblingInstance.compareDocumentPosition(otherNode);
|
||||
if (
|
||||
nextSiblingResult === 0 ||
|
||||
nextSiblingResult & Node.DOCUMENT_POSITION_FOLLOWING
|
||||
) {
|
||||
result = Node.DOCUMENT_POSITION_FOLLOWING;
|
||||
} else {
|
||||
result = Node.DOCUMENT_POSITION_PRECEDING;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result |= Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
|
||||
return result;
|
||||
}
|
||||
@@ -26,6 +26,10 @@ export function getIODescription(value: any): string {
|
||||
return value.url;
|
||||
} else if (typeof value.href === 'string') {
|
||||
return value.href;
|
||||
} else if (typeof value.src === 'string') {
|
||||
return value.src;
|
||||
} else if (typeof value.currentSrc === 'string') {
|
||||
return value.currentSrc;
|
||||
} else if (typeof value.command === 'string') {
|
||||
return value.command;
|
||||
} else if (
|
||||
|
||||
Reference in New Issue
Block a user