main
ts 226 lines 7.84 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import {
9 CompilerDiagnostic,
10 CompilerError,
11 ErrorCategory,
12 } from '../CompilerError';
13 import {
14 FunctionExpression,
15 HIRFunction,
16 IdentifierId,
17 SourceLocation,
18 } from '../HIR';
19 import {Environment} from '../HIR/Environment';
20 import {
21 eachInstructionValueOperand,
22 eachTerminalOperand,
23 } from '../HIR/visitors';
24
25 export function validateUseMemo(fn: HIRFunction): void {
26 const voidMemoErrors = new CompilerError();
27 const useMemos = new Set<IdentifierId>();
28 const react = new Set<IdentifierId>();
29 const functions = new Map<IdentifierId, FunctionExpression>();
30 const unusedUseMemos = new Map<IdentifierId, SourceLocation>();
31 for (const [, block] of fn.body.blocks) {
32 for (const {lvalue, value} of block.instructions) {
33 if (unusedUseMemos.size !== 0) {
34 /**
35 * Most of the time useMemo results are referenced immediately. Don't bother
36 * scanning instruction operands for useMemos unless there is an as-yet-unused
37 * useMemo.
38 */
39 for (const operand of eachInstructionValueOperand(value)) {
40 unusedUseMemos.delete(operand.identifier.id);
41 }
42 }
43 switch (value.kind) {
44 case 'LoadGlobal': {
45 if (value.binding.name === 'useMemo') {
46 useMemos.add(lvalue.identifier.id);
47 } else if (value.binding.name === 'React') {
48 react.add(lvalue.identifier.id);
49 }
50 break;
51 }
52 case 'PropertyLoad': {
53 if (react.has(value.object.identifier.id)) {
54 if (value.property === 'useMemo') {
55 useMemos.add(lvalue.identifier.id);
56 }
57 }
58 break;
59 }
60 case 'FunctionExpression': {
61 functions.set(lvalue.identifier.id, value);
62 break;
63 }
64 case 'MethodCall':
65 case 'CallExpression': {
66 // Is the function being called useMemo, with at least 1 argument?
67 const callee =
68 value.kind === 'CallExpression' ? value.callee : value.property;
69 const isUseMemo = useMemos.has(callee.identifier.id);
70 if (!isUseMemo || value.args.length === 0) {
71 continue;
72 }
73
74 /*
75 * If yes get the first argument and if it refers to a locally defined function
76 * expression, validate the function
77 */
78 const [arg] = value.args;
79 if (arg.kind !== 'Identifier') {
80 continue;
81 }
82 const body = functions.get(arg.identifier.id);
83 if (body === undefined) {
84 continue;
85 }
86
87 if (body.loweredFunc.func.params.length > 0) {
88 const firstParam = body.loweredFunc.func.params[0];
89 const loc =
90 firstParam.kind === 'Identifier'
91 ? firstParam.loc
92 : firstParam.place.loc;
93 fn.env.recordError(
94 CompilerDiagnostic.create({
95 category: ErrorCategory.UseMemo,
96 reason: 'useMemo() callbacks may not accept parameters',
97 description:
98 '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',
99 suggestions: null,
100 }).withDetails({
101 kind: 'error',
102 loc,
103 message: 'Callbacks with parameters are not supported',
104 }),
105 );
106 }
107
108 if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
109 fn.env.recordError(
110 CompilerDiagnostic.create({
111 category: ErrorCategory.UseMemo,
112 reason:
113 'useMemo() callbacks may not be async or generator functions',
114 description:
115 'useMemo() callbacks are called once and must synchronously return a value',
116 suggestions: null,
117 }).withDetails({
118 kind: 'error',
119 loc: body.loc,
120 message: 'Async and generator functions are not supported',
121 }),
122 );
123 }
124
125 validateNoContextVariableAssignment(body.loweredFunc.func, fn.env);
126
127 if (fn.env.config.validateNoVoidUseMemo) {
128 if (!hasNonVoidReturn(body.loweredFunc.func)) {
129 voidMemoErrors.pushDiagnostic(
130 CompilerDiagnostic.create({
131 category: ErrorCategory.VoidUseMemo,
132 reason: 'useMemo() callbacks must return a value',
133 description: `This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects`,
134 suggestions: null,
135 }).withDetails({
136 kind: 'error',
137 loc: body.loc,
138 message: 'useMemo() callbacks must return a value',
139 }),
140 );
141 } else {
142 unusedUseMemos.set(lvalue.identifier.id, callee.loc);
143 }
144 }
145 break;
146 }
147 }
148 }
149 if (unusedUseMemos.size !== 0) {
150 for (const operand of eachTerminalOperand(block.terminal)) {
151 unusedUseMemos.delete(operand.identifier.id);
152 }
153 }
154 }
155 if (unusedUseMemos.size !== 0) {
156 /**
157 * Basic check for unused memos, where the result of the call is never referenced. This runs
158 * before DCE so it's more of an AST-level check that something, _anything_, cares about the value.
159 *
160 * This is easy to defeat with e.g. `const _ = useMemo(...)` but it at least gives us something to teach.
161 * Even a DCE-based version could be bypassed with `noop(useMemo(...))`.
162 */
163 for (const loc of unusedUseMemos.values()) {
164 voidMemoErrors.pushDiagnostic(
165 CompilerDiagnostic.create({
166 category: ErrorCategory.VoidUseMemo,
167 reason: 'useMemo() result is unused',
168 description: `This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects`,
169 suggestions: null,
170 }).withDetails({
171 kind: 'error',
172 loc,
173 message: 'useMemo() result is unused',
174 }),
175 );
176 }
177 }
178 fn.env.logErrors(voidMemoErrors.asResult());
179 }
180
181 function validateNoContextVariableAssignment(
182 fn: HIRFunction,
183 env: Environment,
184 ): void {
185 const context = new Set(fn.context.map(place => place.identifier.id));
186 for (const block of fn.body.blocks.values()) {
187 for (const instr of block.instructions) {
188 const value = instr.value;
189 switch (value.kind) {
190 case 'StoreContext': {
191 if (context.has(value.lvalue.place.identifier.id)) {
192 env.recordError(
193 CompilerDiagnostic.create({
194 category: ErrorCategory.UseMemo,
195 reason:
196 'useMemo() callbacks may not reassign variables declared outside of the callback',
197 description:
198 'useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function',
199 suggestions: null,
200 }).withDetails({
201 kind: 'error',
202 loc: value.lvalue.place.loc,
203 message: 'Cannot reassign variable',
204 }),
205 );
206 }
207 break;
208 }
209 }
210 }
211 }
212 }
213
214 function hasNonVoidReturn(func: HIRFunction): boolean {
215 for (const [, block] of func.body.blocks) {
216 if (block.terminal.kind === 'return') {
217 if (
218 block.terminal.returnVariant === 'Explicit' ||
219 block.terminal.returnVariant === 'Implicit'
220 ) {
221 return true;
222 }
223 }
224 }
225 return false;
226 }