import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
} from '../CompilerError';
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {Result} from '../Utils/Result';
export function validateNoSetStateInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const unconditionalSetStateFunctions: Set<IdentifierId> = new Set();
return validateNoSetStateInRenderImpl(fn, unconditionalSetStateFunctions);
}
function validateNoSetStateInRenderImpl(
fn: HIRFunction,
unconditionalSetStateFunctions: Set<IdentifierId>,
): Result<void, CompilerError> {
const unconditionalBlocks = computeUnconditionalBlocks(fn);
let activeManualMemoId: number | null = null;
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'LoadLocal': {
if (
unconditionalSetStateFunctions.has(instr.value.place.identifier.id)
) {
unconditionalSetStateFunctions.add(instr.lvalue.identifier.id);
}
break;
}
case 'StoreLocal': {
if (
unconditionalSetStateFunctions.has(instr.value.value.identifier.id)
) {
unconditionalSetStateFunctions.add(
instr.value.lvalue.place.identifier.id,
);
unconditionalSetStateFunctions.add(instr.lvalue.identifier.id);
}
break;
}
case 'ObjectMethod':
case 'FunctionExpression': {
if (
[...eachInstructionValueOperand(instr.value)].some(
operand =>
isSetStateType(operand.identifier) ||
unconditionalSetStateFunctions.has(operand.identifier.id),
) &&
validateNoSetStateInRenderImpl(
instr.value.loweredFunc.func,
unconditionalSetStateFunctions,
).isErr()
) {
unconditionalSetStateFunctions.add(instr.lvalue.identifier.id);
}
break;
}
case 'StartMemoize': {
CompilerError.invariant(activeManualMemoId === null, {
reason: 'Unexpected nested StartMemoize instructions',
loc: instr.value.loc,
});
activeManualMemoId = instr.value.manualMemoId;
break;
}
case 'FinishMemoize': {
CompilerError.invariant(
activeManualMemoId === instr.value.manualMemoId,
{
reason:
'Expected FinishMemoize to align with previous StartMemoize instruction',
loc: instr.value.loc,
},
);
activeManualMemoId = null;
break;
}
case 'CallExpression': {
const callee = instr.value.callee;
if (
isSetStateType(callee.identifier) ||
unconditionalSetStateFunctions.has(callee.identifier.id)
) {
if (activeManualMemoId !== null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason:
'Calling setState from useMemo may trigger an infinite loop',
description:
'Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)',
suggestions: null,
}).withDetails({
kind: 'error',
loc: callee.loc,
message: 'Found setState() within useMemo()',
}),
);
} else if (unconditionalBlocks.has(block.id)) {
const enableUseKeyedState = fn.env.config.enableUseKeyedState;
if (enableUseKeyedState) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason: 'Cannot call setState during render',
description:
'Calling setState during render may trigger an infinite loop.\n' +
'* To reset state when other state/props change, use `const [state, setState] = useKeyedState(initialState, key)` to reset `state` when `key` changes.\n' +
'* To derive data from other state/props, compute the derived data during render without using state',
suggestions: null,
}).withDetails({
kind: 'error',
loc: callee.loc,
message: 'Found setState() in render',
}),
);
} else {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason: 'Cannot call setState during render',
description:
'Calling setState during render may trigger an infinite loop.\n' +
'* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders\n' +
'* To derive data from other state/props, compute the derived data during render without using state',
suggestions: null,
}).withDetails({
kind: 'error',
loc: callee.loc,
message: 'Found setState() in render',
}),
);
}
}
}
break;
}
}
}
}
return errors.asResult();
}