Files
react/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateUseMemo.ts
T
Joe Savona f0f44b9283 compiler: distinguish globals/imports/module-locals
We currently use `LoadGlobal` and `StoreGlobal` to represent any read (or write) of a variable defined outside the component or hook that is being compiled. This is mostly fine, but for a lot of things we want to do going forward (resolving types across modules, for example) it helps to understand the actual source of a variable.

This PR is an incremental step in that direction. We continue to use LoadGlobal/StoreGlobal, but LoadGlobal now has a `binding:NonLocalBinding` instead of just the name of the global. The NonLocalBinding type tells us whether it was an import (and which kind, the source module name etc), a module-local binding, or a true global. By keeping the LoadGlobal/StoreGlobal instructions, most code that deals with "anything not declared locally" doesn't have to care about the difference. However, code that _does_ want to know the source of the value can figure it out.

ghstack-source-id: e701d4ebc0fb5681a0197198ac2c2a03b3e8aae9
Pull Request resolved: https://github.com/facebook/react/pull/29188
2024-05-24 10:20:26 +01:00

88 lines
2.7 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 { CompilerError } from "..";
import { FunctionExpression, HIRFunction, IdentifierId } from "../HIR";
export function validateUseMemo(fn: HIRFunction): void {
const useMemos = new Set<IdentifierId>();
const react = new Set<IdentifierId>();
const functions = new Map<IdentifierId, FunctionExpression>();
for (const [, block] of fn.body.blocks) {
for (const { lvalue, value } of block.instructions) {
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": {
// Is the function being called useMemo, with at least 1 argument?
const callee =
value.kind === "CallExpression"
? value.callee.identifier.id
: value.property.identifier.id;
const isUseMemo = useMemos.has(callee);
if (!isUseMemo || value.args.length === 0) {
continue;
}
/*
* If yes get the first argument and if it refers to a locally defined function
* expression, validate the function
*/
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) {
CompilerError.throwInvalidReact({
reason: "useMemo callbacks may not accept any arguments",
description: null,
loc: body.loc,
suggestions: null,
});
}
if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
CompilerError.throwInvalidReact({
reason:
"useMemo callbacks may not be async or generator functions",
description: null,
loc: body.loc,
suggestions: null,
});
}
break;
}
}
}
}
}