mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Small fix to make all descriptions consistently printed with a single period at the end. Ran `grep -rn "description:" packages/babel-plugin-react-compiler/src --include="*.ts" --exclude-dir="__tests__" | grep '\.\s*["\`]'` to find all descriptions ending in a period and manually fixed them. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34404). * #34409 * __->__ #34404
167 lines
5.8 KiB
TypeScript
167 lines
5.8 KiB
TypeScript
/**
|
|
* 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 {CompilerDiagnostic, CompilerError, Effect} from '..';
|
|
import {ErrorCategory} from '../CompilerError';
|
|
import {
|
|
HIRFunction,
|
|
IdentifierId,
|
|
isRefOrRefLikeMutableType,
|
|
Place,
|
|
} from '../HIR';
|
|
import {
|
|
eachInstructionValueOperand,
|
|
eachTerminalOperand,
|
|
} from '../HIR/visitors';
|
|
import {AliasingEffect} from '../Inference/AliasingEffects';
|
|
import {Result} from '../Utils/Result';
|
|
|
|
/**
|
|
* Validates that functions with known mutations (ie due to types) cannot be passed
|
|
* where a frozen value is expected. Example:
|
|
*
|
|
* ```
|
|
* function Component() {
|
|
* const cache = new Map();
|
|
* const onClick = () => {
|
|
* cache.set(...);
|
|
* }
|
|
* useHook(onClick); // ERROR: cannot pass a mutable value
|
|
* return <Foo onClick={onClick} /> // ERROR: cannot pass a mutable value
|
|
* }
|
|
* ```
|
|
*
|
|
* Because `onClick` function mutates `cache` when called, `onClick` is equivalent to a mutable
|
|
* variables. But unlike other mutables values like an array, the receiver of the function has
|
|
* no way to avoid mutation — for example, a function can receive an array and choose not to mutate
|
|
* it, but there's no way to know that a function is mutable and avoid calling it.
|
|
*
|
|
* This pass detects functions with *known* mutations (Store or Mutate, not ConditionallyMutate)
|
|
* that are passed where a frozen value is expected and rejects them.
|
|
*/
|
|
export function validateNoFreezingKnownMutableFunctions(
|
|
fn: HIRFunction,
|
|
): Result<void, CompilerError> {
|
|
const errors = new CompilerError();
|
|
const contextMutationEffects: Map<
|
|
IdentifierId,
|
|
Extract<AliasingEffect, {kind: 'Mutate'} | {kind: 'MutateTransitive'}>
|
|
> = new Map();
|
|
|
|
function visitOperand(operand: Place): void {
|
|
if (operand.effect === Effect.Freeze) {
|
|
const effect = contextMutationEffects.get(operand.identifier.id);
|
|
if (effect != null) {
|
|
const place = effect.value;
|
|
const variable =
|
|
place != null &&
|
|
place.identifier.name != null &&
|
|
place.identifier.name.kind === 'named'
|
|
? `\`${place.identifier.name.value}\``
|
|
: 'a local variable';
|
|
errors.pushDiagnostic(
|
|
CompilerDiagnostic.create({
|
|
category: ErrorCategory.Immutability,
|
|
reason: 'Cannot modify local variables after render completes',
|
|
description: `This argument is a function which may reassign or mutate ${variable} after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead`,
|
|
})
|
|
.withDetails({
|
|
kind: 'error',
|
|
loc: operand.loc,
|
|
message: `This function may (indirectly) reassign or modify ${variable} after render`,
|
|
})
|
|
.withDetails({
|
|
kind: 'error',
|
|
loc: effect.value.loc,
|
|
message: `This modifies ${variable}`,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const block of fn.body.blocks.values()) {
|
|
for (const instr of block.instructions) {
|
|
const {lvalue, value} = instr;
|
|
switch (value.kind) {
|
|
case 'LoadLocal': {
|
|
const effect = contextMutationEffects.get(value.place.identifier.id);
|
|
if (effect != null) {
|
|
contextMutationEffects.set(lvalue.identifier.id, effect);
|
|
}
|
|
break;
|
|
}
|
|
case 'StoreLocal': {
|
|
const effect = contextMutationEffects.get(value.value.identifier.id);
|
|
if (effect != null) {
|
|
contextMutationEffects.set(lvalue.identifier.id, effect);
|
|
contextMutationEffects.set(
|
|
value.lvalue.place.identifier.id,
|
|
effect,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case 'FunctionExpression': {
|
|
if (value.loweredFunc.func.aliasingEffects != null) {
|
|
const context = new Set(
|
|
value.loweredFunc.func.context.map(p => p.identifier.id),
|
|
);
|
|
effects: for (const effect of value.loweredFunc.func
|
|
.aliasingEffects) {
|
|
switch (effect.kind) {
|
|
case 'Mutate':
|
|
case 'MutateTransitive': {
|
|
const knownMutation = contextMutationEffects.get(
|
|
effect.value.identifier.id,
|
|
);
|
|
if (knownMutation != null) {
|
|
contextMutationEffects.set(
|
|
lvalue.identifier.id,
|
|
knownMutation,
|
|
);
|
|
} else if (
|
|
context.has(effect.value.identifier.id) &&
|
|
!isRefOrRefLikeMutableType(effect.value.identifier.type)
|
|
) {
|
|
contextMutationEffects.set(lvalue.identifier.id, effect);
|
|
break effects;
|
|
}
|
|
break;
|
|
}
|
|
case 'MutateConditionally':
|
|
case 'MutateTransitiveConditionally': {
|
|
const knownMutation = contextMutationEffects.get(
|
|
effect.value.identifier.id,
|
|
);
|
|
if (knownMutation != null) {
|
|
contextMutationEffects.set(
|
|
lvalue.identifier.id,
|
|
knownMutation,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
for (const operand of eachInstructionValueOperand(value)) {
|
|
visitOperand(operand);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (const operand of eachTerminalOperand(block.terminal)) {
|
|
visitOperand(operand);
|
|
}
|
|
}
|
|
return errors.asResult();
|
|
}
|