import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
} from '../CompilerError';
import {
FunctionExpression,
HIRFunction,
IdentifierId,
SourceLocation,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Result} from '../Utils/Result';
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
const errors = new CompilerError();
const voidMemoErrors = new CompilerError();
const useMemos = new Set<IdentifierId>();
const react = new Set<IdentifierId>();
const functions = new Map<IdentifierId, FunctionExpression>();
const unusedUseMemos = new Map<IdentifierId, SourceLocation>();
for (const [, block] of fn.body.blocks) {
for (const {lvalue, value} of block.instructions) {
if (unusedUseMemos.size !== 0) {
for (const operand of eachInstructionValueOperand(value)) {
unusedUseMemos.delete(operand.identifier.id);
}
}
switch (value.kind) {
case 'LoadGlobal': {
if (value.binding.name === 'useMemo') {
useMemos.add(lvalue.identifier.id);
} else if (value.binding.name === 'React') {
react.add(lvalue.identifier.id);
}
break;
}
case 'PropertyLoad': {
if (react.has(value.object.identifier.id)) {
if (value.property === 'useMemo') {
useMemos.add(lvalue.identifier.id);
}
}
break;
}
case 'FunctionExpression': {
functions.set(lvalue.identifier.id, value);
break;
}
case 'MethodCall':
case 'CallExpression': {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
const isUseMemo = useMemos.has(callee.identifier.id);
if (!isUseMemo || value.args.length === 0) {
continue;
}
const [arg] = value.args;
if (arg.kind !== 'Identifier') {
continue;
}
const body = functions.get(arg.identifier.id);
if (body === undefined) {
continue;
}
if (body.loweredFunc.func.params.length > 0) {
const firstParam = body.loweredFunc.func.params[0];
const loc =
firstParam.kind === 'Identifier'
? firstParam.loc
: firstParam.place.loc;
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: 'useMemo() callbacks may not accept parameters',
description:
'useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation',
suggestions: null,
}).withDetails({
kind: 'error',
loc,
message: 'Callbacks with parameters are not supported',
}),
);
}
if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason:
'useMemo() callbacks may not be async or generator functions',
description:
'useMemo() callbacks are called once and must synchronously return a value',
suggestions: null,
}).withDetails({
kind: 'error',
loc: body.loc,
message: 'Async and generator functions are not supported',
}),
);
}
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
if (fn.env.config.validateNoVoidUseMemo) {
if (!hasNonVoidReturn(body.loweredFunc.func)) {
voidMemoErrors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.VoidUseMemo,
reason: 'useMemo() callbacks must return a value',
description: `This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: body.loc,
message: 'useMemo() callbacks must return a value',
}),
);
} else {
unusedUseMemos.set(lvalue.identifier.id, callee.loc);
}
}
break;
}
}
}
if (unusedUseMemos.size !== 0) {
for (const operand of eachTerminalOperand(block.terminal)) {
unusedUseMemos.delete(operand.identifier.id);
}
}
}
if (unusedUseMemos.size !== 0) {
for (const loc of unusedUseMemos.values()) {
voidMemoErrors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.VoidUseMemo,
reason: 'useMemo() result is unused',
description: `This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects`,
suggestions: null,
}).withDetails({
kind: 'error',
loc,
message: 'useMemo() result is unused',
}),
);
}
}
fn.env.logErrors(voidMemoErrors.asResult());
return errors.asResult();
}
function validateNoContextVariableAssignment(
fn: HIRFunction,
errors: CompilerError,
): void {
const context = new Set(fn.context.map(place => place.identifier.id));
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const value = instr.value;
switch (value.kind) {
case 'StoreContext': {
if (context.has(value.lvalue.place.identifier.id)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason:
'useMemo() callbacks may not reassign variables declared outside of the callback',
description:
'useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function',
suggestions: null,
}).withDetails({
kind: 'error',
loc: value.lvalue.place.loc,
message: 'Cannot reassign variable',
}),
);
}
break;
}
}
}
}
}
function hasNonVoidReturn(func: HIRFunction): boolean {
for (const [, block] of func.body.blocks) {
if (block.terminal.kind === 'return') {
if (
block.terminal.returnVariant === 'Explicit' ||
block.terminal.returnVariant === 'Implicit'
) {
return true;
}
}
}
return false;
}