-
Notifications
You must be signed in to change notification settings - Fork 51k
Expand file tree
/
Copy pathValidateHooksUsage.ts
More file actions
456 lines (436 loc) · 15.7 KB
/
ValidateHooksUsage.ts
File metadata and controls
456 lines (436 loc) · 15.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
/**
* 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 * as t from '@babel/types';
import {CompilerErrorDetail, ErrorCategory} from '../CompilerError';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
import {Environment, isHookName} from '../HIR/Environment';
import {
HIRFunction,
IdentifierId,
Place,
SourceLocation,
getHookKind,
} from '../HIR/HIR';
import {
eachInstructionLValue,
eachInstructionOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive} from '../Utils/utils';
/**
* Represents the possible kinds of value which may be stored at a given Place during
* abstract interpretation. The kinds form a lattice, with earlier items taking
* precedence over later items (see joinKinds()).
*/
enum Kind {
// A potential/known hook which was already used in an invalid way
Error = 'Error',
/*
* A known hook. Sources include:
* - LoadGlobal instructions whose type was inferred as a hook
* - PropertyLoad, ComputedLoad, and Destructuring instructions
* where the object is a KnownHook
* - PropertyLoad, ComputedLoad, and Destructuring instructions
* where the object is a Global and the property name is hook-like
*/
KnownHook = 'KnownHook',
/*
* A potential hook. Sources include:
* - LValues (other than LoadGlobal) where the name is hook-like
* - PropertyLoad, ComputedLoad, and Destructuring instructions
* where the object is a potential hook or the property name
* is hook-like
*/
PotentialHook = 'PotentialHook',
// LoadGlobal values whose type was not inferred as a hook
Global = 'Global',
// All other values, ie local variables
Local = 'Local',
}
function joinKinds(a: Kind, b: Kind): Kind {
if (a === Kind.Error || b === Kind.Error) {
return Kind.Error;
} else if (a === Kind.KnownHook || b === Kind.KnownHook) {
return Kind.KnownHook;
} else if (a === Kind.PotentialHook || b === Kind.PotentialHook) {
return Kind.PotentialHook;
} else if (a === Kind.Global || b === Kind.Global) {
return Kind.Global;
} else {
return Kind.Local;
}
}
/*
* Validates that the function honors the [Rules of Hooks](https://react.dev/warnings/invalid-hook-call-warning)
* rule that hooks may only be called and not otherwise referenced as first-class values.
*
* Specifically this pass implements the following rules:
* - Known hooks may only be called unconditionally, and cannot be used as first-class values.
* See the note for Kind.KnownHook for sources of known hooks
* - Potential hooks may be referenced as first-class values, with the exception that they
* may not appear as the callee of a conditional call.
* See the note for Kind.PotentialHook for sources of potential hooks
*/
export function validateHooksUsage(fn: HIRFunction): void {
const unconditionalBlocks = computeUnconditionalBlocks(fn);
const errorsByPlace = new Map<t.SourceLocation, CompilerErrorDetail>();
function trackError(
loc: SourceLocation,
errorDetail: CompilerErrorDetail,
): void {
if (typeof loc === 'symbol') {
fn.env.recordError(errorDetail);
} else {
errorsByPlace.set(loc, errorDetail);
}
}
function recordConditionalHookError(place: Place): void {
// Once a particular hook has a conditional call error, don't report any further issues for this hook
setKind(place, Kind.Error);
const reason =
'Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)';
const previousError =
typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;
/*
* In some circumstances such as optional calls, we may first encounter a "hook may not be referenced as normal values" error.
* If that same place is also used as a conditional call, upgrade the error to a conditonal hook error
*/
if (previousError === undefined || previousError.reason !== reason) {
trackError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
description: null,
reason,
loc: place.loc,
suggestions: null,
}),
);
}
}
function recordInvalidHookUsageError(place: Place): void {
const previousError =
typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;
if (previousError === undefined) {
trackError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
description: null,
reason:
'Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values',
loc: place.loc,
suggestions: null,
}),
);
}
}
function recordDynamicHookUsageError(place: Place): void {
const previousError =
typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;
if (previousError === undefined) {
trackError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
description: null,
reason:
'Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks',
loc: place.loc,
suggestions: null,
}),
);
}
}
const valueKinds = new Map<IdentifierId, Kind>();
function getKindForPlace(place: Place): Kind {
const knownKind = valueKinds.get(place.identifier.id);
if (
place.identifier.name !== null &&
isHookName(place.identifier.name.value)
) {
return joinKinds(knownKind ?? Kind.Local, Kind.PotentialHook);
} else {
return knownKind ?? Kind.Local;
}
}
function visitPlace(place: Place): void {
const kind = valueKinds.get(place.identifier.id);
if (kind === Kind.KnownHook) {
recordInvalidHookUsageError(place);
}
}
function setKind(place: Place, kind: Kind): void {
valueKinds.set(place.identifier.id, kind);
}
for (const param of fn.params) {
const place = param.kind === 'Identifier' ? param : param.place;
const kind = getKindForPlace(place);
setKind(place, kind);
}
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
let kind: Kind =
phi.place.identifier.name !== null &&
isHookName(phi.place.identifier.name.value)
? Kind.PotentialHook
: Kind.Local;
for (const [, operand] of phi.operands) {
const operandKind = valueKinds.get(operand.identifier.id);
/*
* NOTE: we currently skip operands whose value is unknown
* (which can only occur for functions with loops), we may
* cause us to miss invalid code in some cases. We should
* expand this to a fixpoint iteration in a follow-up.
*/
if (operandKind !== undefined) {
kind = joinKinds(kind, operandKind);
}
}
valueKinds.set(phi.place.identifier.id, kind);
}
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'LoadGlobal': {
/*
* Globals are the one source of known hooks: they are either
* directly a hook, or infer a Global kind from which knownhooks
* can be derived later via property access (PropertyLoad etc)
*/
if (getHookKind(fn.env, instr.lvalue.identifier) != null) {
setKind(instr.lvalue, Kind.KnownHook);
} else {
setKind(instr.lvalue, Kind.Global);
}
break;
}
case 'LoadContext':
case 'LoadLocal': {
visitPlace(instr.value.place);
const kind = getKindForPlace(instr.value.place);
setKind(instr.lvalue, kind);
break;
}
case 'StoreLocal':
case 'StoreContext': {
visitPlace(instr.value.value);
const kind = joinKinds(
getKindForPlace(instr.value.value),
getKindForPlace(instr.value.lvalue.place),
);
setKind(instr.value.lvalue.place, kind);
setKind(instr.lvalue, kind);
break;
}
case 'ComputedLoad': {
visitPlace(instr.value.object);
const kind = getKindForPlace(instr.value.object);
setKind(instr.lvalue, joinKinds(getKindForPlace(instr.lvalue), kind));
break;
}
case 'PropertyLoad': {
const objectKind = getKindForPlace(instr.value.object);
const isHookProperty =
typeof instr.value.property === 'string' &&
isHookName(instr.value.property);
let kind: Kind;
switch (objectKind) {
case Kind.Error: {
kind = Kind.Error;
break;
}
case Kind.KnownHook: {
/**
* const useFoo;
* function Component() {
* let x = useFoo.useBar; // useFoo is KnownHook, any property from it inherits KnownHook
* }
*/
kind = isHookProperty ? Kind.KnownHook : Kind.Local;
break;
}
case Kind.PotentialHook: {
/**
* function Component(props) {
* let useFoo;
* let x = useFoo.useBar; // useFoo is PotentialHook, any property from it inherits PotentialHook
* }
*/
kind = Kind.PotentialHook;
break;
}
case Kind.Global: {
/**
* function Component() {
* let x = React.useState; // hook-named property of global is knownhook
* let y = React.foo; // else inherit Global
* }
*/
kind = isHookProperty ? Kind.KnownHook : Kind.Global;
break;
}
case Kind.Local: {
/**
* function Component() {
* let o = createObject();
* let x = o.useState; // hook-named property of local is potentialhook
* let y = o.foo; // else inherit local
* }
*/
kind = isHookProperty ? Kind.PotentialHook : Kind.Local;
break;
}
default: {
assertExhaustive(objectKind, `Unexpected kind \`${objectKind}\``);
}
}
setKind(instr.lvalue, kind);
break;
}
case 'CallExpression': {
const calleeKind = getKindForPlace(instr.value.callee);
const isHookCallee =
calleeKind === Kind.KnownHook || calleeKind === Kind.PotentialHook;
if (isHookCallee && !unconditionalBlocks.has(block.id)) {
recordConditionalHookError(instr.value.callee);
} else if (calleeKind === Kind.PotentialHook) {
recordDynamicHookUsageError(instr.value.callee);
}
/**
* We intentionally skip the callee because it's validated above
*/
for (const operand of eachInstructionOperand(instr)) {
if (operand === instr.value.callee) {
continue;
}
visitPlace(operand);
}
break;
}
case 'MethodCall': {
const calleeKind = getKindForPlace(instr.value.property);
const isHookCallee =
calleeKind === Kind.KnownHook || calleeKind === Kind.PotentialHook;
if (isHookCallee && !unconditionalBlocks.has(block.id)) {
recordConditionalHookError(instr.value.property);
} else if (calleeKind === Kind.PotentialHook) {
recordDynamicHookUsageError(instr.value.property);
}
/*
* We intentionally skip the property because it's validated above
*/
for (const operand of eachInstructionOperand(instr)) {
if (operand === instr.value.property) {
continue;
}
visitPlace(operand);
}
break;
}
case 'Destructure': {
visitPlace(instr.value.value);
const objectKind = getKindForPlace(instr.value.value);
for (const lvalue of eachInstructionLValue(instr)) {
const isHookProperty =
lvalue.identifier.name !== null &&
isHookName(lvalue.identifier.name.value);
let kind: Kind;
switch (objectKind) {
case Kind.Error: {
kind = Kind.Error;
break;
}
case Kind.KnownHook: {
kind = Kind.KnownHook;
break;
}
case Kind.PotentialHook: {
kind = Kind.PotentialHook;
break;
}
case Kind.Global: {
kind = isHookProperty ? Kind.KnownHook : Kind.Global;
break;
}
case Kind.Local: {
kind = isHookProperty ? Kind.PotentialHook : Kind.Local;
break;
}
default: {
assertExhaustive(
objectKind,
`Unexpected kind \`${objectKind}\``,
);
}
}
setKind(lvalue, kind);
}
break;
}
case 'ObjectMethod':
case 'FunctionExpression': {
visitFunctionExpression(fn.env, instr.value.loweredFunc.func);
break;
}
default: {
/*
* Else check usages of operands, but do *not* flow properties
* from operands into the lvalues. For example, `let x = identity(y)`
* does not infer `x` as a potential hook even if `y` is a potential hook.
*/
for (const operand of eachInstructionOperand(instr)) {
visitPlace(operand);
}
for (const lvalue of eachInstructionLValue(instr)) {
const kind = getKindForPlace(lvalue);
setKind(lvalue, kind);
}
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
visitPlace(operand);
}
}
for (const [, error] of errorsByPlace) {
fn.env.recordError(error);
}
}
function visitFunctionExpression(env: Environment, fn: HIRFunction): void {
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'ObjectMethod':
case 'FunctionExpression': {
visitFunctionExpression(env, instr.value.loweredFunc.func);
break;
}
case 'MethodCall':
case 'CallExpression': {
const callee =
instr.value.kind === 'CallExpression'
? instr.value.callee
: instr.value.property;
const hookKind = getHookKind(fn.env, callee.identifier);
if (hookKind != null) {
env.recordError(
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
reason:
'Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)',
loc: callee.loc,
description: `Cannot call ${hookKind === 'Custom' ? 'hook' : hookKind} within a function expression`,
suggestions: null,
}),
);
}
break;
}
}
}
}
}