import {CompilerError, ErrorSeverity} from '..';
import {
Identifier,
Instruction,
ReactiveFunction,
ReactiveInstruction,
ReactiveScopeBlock,
ScopeId,
isUseEffectHookType,
isUseInsertionEffectHookType,
isUseLayoutEffectHookType,
} from '../HIR';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {
ReactiveFunctionVisitor,
visitReactiveFunction,
} from '../ReactiveScopes/visitors';
import {Result} from '../Utils/Result';
export function validateMemoizedEffectDependencies(
fn: ReactiveFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
visitReactiveFunction(fn, new Visitor(), errors);
return errors.asResult();
}
class Visitor extends ReactiveFunctionVisitor<CompilerError> {
scopes: Set<ScopeId> = new Set();
override visitScope(
scopeBlock: ReactiveScopeBlock,
state: CompilerError,
): void {
this.traverseScope(scopeBlock, state);
let areDependenciesMemoized = true;
for (const dep of scopeBlock.scope.dependencies) {
if (isUnmemoized(dep.identifier, this.scopes)) {
areDependenciesMemoized = false;
break;
}
}
if (areDependenciesMemoized) {
this.scopes.add(scopeBlock.scope.id);
for (const id of scopeBlock.scope.merged) {
this.scopes.add(id);
}
}
}
override visitInstruction(
instruction: ReactiveInstruction,
state: CompilerError,
): void {
this.traverseInstruction(instruction, state);
if (
instruction.value.kind === 'CallExpression' &&
isEffectHook(instruction.value.callee.identifier) &&
instruction.value.args.length >= 2
) {
const deps = instruction.value.args[1]!;
if (
deps.kind === 'Identifier' &&
(isMutable(instruction as Instruction, deps) ||
isUnmemoized(deps.identifier, this.scopes))
) {
state.push({
reason:
'React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior',
description: null,
severity: ErrorSeverity.CannotPreserveMemoization,
loc: typeof instruction.loc !== 'symbol' ? instruction.loc : null,
suggestions: null,
});
}
}
}
}
function isUnmemoized(operand: Identifier, scopes: Set<ScopeId>): boolean {
return operand.scope != null && !scopes.has(operand.scope.id);
}
export function isEffectHook(identifier: Identifier): boolean {
return (
isUseEffectHookType(identifier) ||
isUseLayoutEffectHookType(identifier) ||
isUseInsertionEffectHookType(identifier)
);
}