1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */78import * as t from '@babel/types';9import {CompilerErrorDetail, ErrorCategory} from '../CompilerError';10import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';11import {Environment, isHookName} from '../HIR/Environment';12import {13 HIRFunction,14 IdentifierId,15 Place,16 SourceLocation,17 getHookKind,18} from '../HIR/HIR';19import {20 eachInstructionLValue,21 eachInstructionOperand,22 eachTerminalOperand,23} from '../HIR/visitors';24import {assertExhaustive} from '../Utils/utils';2526/**27 * Represents the possible kinds of value which may be stored at a given Place during28 * abstract interpretation. The kinds form a lattice, with earlier items taking29 * precedence over later items (see joinKinds()).30 */31enum Kind {32 // A potential/known hook which was already used in an invalid way33 Error = 'Error',3435 /*36 * A known hook. Sources include:37 * - LoadGlobal instructions whose type was inferred as a hook38 * - PropertyLoad, ComputedLoad, and Destructuring instructions39 * where the object is a KnownHook40 * - PropertyLoad, ComputedLoad, and Destructuring instructions41 * where the object is a Global and the property name is hook-like42 */43 KnownHook = 'KnownHook',4445 /*46 * A potential hook. Sources include:47 * - LValues (other than LoadGlobal) where the name is hook-like48 * - PropertyLoad, ComputedLoad, and Destructuring instructions49 * where the object is a potential hook or the property name50 * is hook-like51 */52 PotentialHook = 'PotentialHook',5354 // LoadGlobal values whose type was not inferred as a hook55 Global = 'Global',5657 // All other values, ie local variables58 Local = 'Local',59}6061function joinKinds(a: Kind, b: Kind): Kind {62 if (a === Kind.Error || b === Kind.Error) {63 return Kind.Error;64 } else if (a === Kind.KnownHook || b === Kind.KnownHook) {65 return Kind.KnownHook;66 } else if (a === Kind.PotentialHook || b === Kind.PotentialHook) {67 return Kind.PotentialHook;68 } else if (a === Kind.Global || b === Kind.Global) {69 return Kind.Global;70 } else {71 return Kind.Local;72 }73}7475/*76 * Validates that the function honors the [Rules of Hooks](https://react.dev/warnings/invalid-hook-call-warning)77 * rule that hooks may only be called and not otherwise referenced as first-class values.78 *79 * Specifically this pass implements the following rules:80 * - Known hooks may only be called unconditionally, and cannot be used as first-class values.81 * See the note for Kind.KnownHook for sources of known hooks82 * - Potential hooks may be referenced as first-class values, with the exception that they83 * may not appear as the callee of a conditional call.84 * See the note for Kind.PotentialHook for sources of potential hooks85 */86export function validateHooksUsage(fn: HIRFunction): void {87 const unconditionalBlocks = computeUnconditionalBlocks(fn);8889 const errorsByPlace = new Map<t.SourceLocation, CompilerErrorDetail>();9091 function trackError(92 loc: SourceLocation,93 errorDetail: CompilerErrorDetail,94 ): void {95 if (typeof loc === 'symbol') {96 fn.env.recordError(errorDetail);97 } else {98 errorsByPlace.set(loc, errorDetail);99 }100 }101102 function recordConditionalHookError(place: Place): void {103 // Once a particular hook has a conditional call error, don't report any further issues for this hook104 setKind(place, Kind.Error);105106 const reason =107 '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)';108 const previousError =109 typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;110111 /*112 * In some circumstances such as optional calls, we may first encounter a "hook may not be referenced as normal values" error.113 * If that same place is also used as a conditional call, upgrade the error to a conditonal hook error114 */115 if (previousError === undefined || previousError.reason !== reason) {116 trackError(117 place.loc,118 new CompilerErrorDetail({119 category: ErrorCategory.Hooks,120 description: null,121 reason,122 loc: place.loc,123 suggestions: null,124 }),125 );126 }127 }128 function recordInvalidHookUsageError(place: Place): void {129 const previousError =130 typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;131 if (previousError === undefined) {132 trackError(133 place.loc,134 new CompilerErrorDetail({135 category: ErrorCategory.Hooks,136 description: null,137 reason:138 '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',139 loc: place.loc,140 suggestions: null,141 }),142 );143 }144 }145 function recordDynamicHookUsageError(place: Place): void {146 const previousError =147 typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;148 if (previousError === undefined) {149 trackError(150 place.loc,151 new CompilerErrorDetail({152 category: ErrorCategory.Hooks,153 description: null,154 reason:155 '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',156 loc: place.loc,157 suggestions: null,158 }),159 );160 }161 }162163 const valueKinds = new Map<IdentifierId, Kind>();164 function getKindForPlace(place: Place): Kind {165 const knownKind = valueKinds.get(place.identifier.id);166 if (167 place.identifier.name !== null &&168 isHookName(place.identifier.name.value)169 ) {170 return joinKinds(knownKind ?? Kind.Local, Kind.PotentialHook);171 } else {172 return knownKind ?? Kind.Local;173 }174 }175176 function visitPlace(place: Place): void {177 const kind = valueKinds.get(place.identifier.id);178 if (kind === Kind.KnownHook) {179 recordInvalidHookUsageError(place);180 }181 }182183 function setKind(place: Place, kind: Kind): void {184 valueKinds.set(place.identifier.id, kind);185 }186187 for (const param of fn.params) {188 const place = param.kind === 'Identifier' ? param : param.place;189 const kind = getKindForPlace(place);190 setKind(place, kind);191 }192193 for (const [, block] of fn.body.blocks) {194 for (const phi of block.phis) {195 let kind: Kind =196 phi.place.identifier.name !== null &&197 isHookName(phi.place.identifier.name.value)198 ? Kind.PotentialHook199 : Kind.Local;200 for (const [, operand] of phi.operands) {201 const operandKind = valueKinds.get(operand.identifier.id);202 /*203 * NOTE: we currently skip operands whose value is unknown204 * (which can only occur for functions with loops), we may205 * cause us to miss invalid code in some cases. We should206 * expand this to a fixpoint iteration in a follow-up.207 */208 if (operandKind !== undefined) {209 kind = joinKinds(kind, operandKind);210 }211 }212 valueKinds.set(phi.place.identifier.id, kind);213 }214 for (const instr of block.instructions) {215 switch (instr.value.kind) {216 case 'LoadGlobal': {217 /*218 * Globals are the one source of known hooks: they are either219 * directly a hook, or infer a Global kind from which knownhooks220 * can be derived later via property access (PropertyLoad etc)221 */222 if (getHookKind(fn.env, instr.lvalue.identifier) != null) {223 setKind(instr.lvalue, Kind.KnownHook);224 } else {225 setKind(instr.lvalue, Kind.Global);226 }227 break;228 }229 case 'LoadContext':230 case 'LoadLocal': {231 visitPlace(instr.value.place);232 const kind = getKindForPlace(instr.value.place);233 setKind(instr.lvalue, kind);234 break;235 }236 case 'StoreLocal':237 case 'StoreContext': {238 visitPlace(instr.value.value);239 const kind = joinKinds(240 getKindForPlace(instr.value.value),241 getKindForPlace(instr.value.lvalue.place),242 );243 setKind(instr.value.lvalue.place, kind);244 setKind(instr.lvalue, kind);245 break;246 }247 case 'ComputedLoad': {248 visitPlace(instr.value.object);249 const kind = getKindForPlace(instr.value.object);250 setKind(instr.lvalue, joinKinds(getKindForPlace(instr.lvalue), kind));251 break;252 }253 case 'PropertyLoad': {254 const objectKind = getKindForPlace(instr.value.object);255 const isHookProperty =256 typeof instr.value.property === 'string' &&257 isHookName(instr.value.property);258 let kind: Kind;259 switch (objectKind) {260 case Kind.Error: {261 kind = Kind.Error;262 break;263 }264 case Kind.KnownHook: {265 /**266 * const useFoo;267 * function Component() {268 * let x = useFoo.useBar; // useFoo is KnownHook, any property from it inherits KnownHook269 * }270 */271 kind = isHookProperty ? Kind.KnownHook : Kind.Local;272 break;273 }274 case Kind.PotentialHook: {275 /**276 * function Component(props) {277 * let useFoo;278 * let x = useFoo.useBar; // useFoo is PotentialHook, any property from it inherits PotentialHook279 * }280 */281 kind = Kind.PotentialHook;282 break;283 }284 case Kind.Global: {285 /**286 * function Component() {287 * let x = React.useState; // hook-named property of global is knownhook288 * let y = React.foo; // else inherit Global289 * }290 */291 kind = isHookProperty ? Kind.KnownHook : Kind.Global;292 break;293 }294 case Kind.Local: {295 /**296 * function Component() {297 * let o = createObject();298 * let x = o.useState; // hook-named property of local is potentialhook299 * let y = o.foo; // else inherit local300 * }301 */302 kind = isHookProperty ? Kind.PotentialHook : Kind.Local;303 break;304 }305 default: {306 assertExhaustive(objectKind, `Unexpected kind \`${objectKind}\``);307 }308 }309 setKind(instr.lvalue, kind);310 break;311 }312 case 'CallExpression': {313 const calleeKind = getKindForPlace(instr.value.callee);314 const isHookCallee =315 calleeKind === Kind.KnownHook || calleeKind === Kind.PotentialHook;316 if (isHookCallee && !unconditionalBlocks.has(block.id)) {317 recordConditionalHookError(instr.value.callee);318 } else if (calleeKind === Kind.PotentialHook) {319 recordDynamicHookUsageError(instr.value.callee);320 }321 /**322 * We intentionally skip the callee because it's validated above323 */324 for (const operand of eachInstructionOperand(instr)) {325 if (operand === instr.value.callee) {326 continue;327 }328 visitPlace(operand);329 }330 break;331 }332 case 'MethodCall': {333 const calleeKind = getKindForPlace(instr.value.property);334 const isHookCallee =335 calleeKind === Kind.KnownHook || calleeKind === Kind.PotentialHook;336 if (isHookCallee && !unconditionalBlocks.has(block.id)) {337 recordConditionalHookError(instr.value.property);338 } else if (calleeKind === Kind.PotentialHook) {339 recordDynamicHookUsageError(instr.value.property);340 }341 /*342 * We intentionally skip the property because it's validated above343 */344 for (const operand of eachInstructionOperand(instr)) {345 if (operand === instr.value.property) {346 continue;347 }348 visitPlace(operand);349 }350 break;351 }352 case 'Destructure': {353 visitPlace(instr.value.value);354 const objectKind = getKindForPlace(instr.value.value);355 for (const lvalue of eachInstructionLValue(instr)) {356 const isHookProperty =357 lvalue.identifier.name !== null &&358 isHookName(lvalue.identifier.name.value);359 let kind: Kind;360 switch (objectKind) {361 case Kind.Error: {362 kind = Kind.Error;363 break;364 }365 case Kind.KnownHook: {366 kind = Kind.KnownHook;367 break;368 }369 case Kind.PotentialHook: {370 kind = Kind.PotentialHook;371 break;372 }373 case Kind.Global: {374 kind = isHookProperty ? Kind.KnownHook : Kind.Global;375 break;376 }377 case Kind.Local: {378 kind = isHookProperty ? Kind.PotentialHook : Kind.Local;379 break;380 }381 default: {382 assertExhaustive(383 objectKind,384 `Unexpected kind \`${objectKind}\``,385 );386 }387 }388 setKind(lvalue, kind);389 }390 break;391 }392 case 'ObjectMethod':393 case 'FunctionExpression': {394 visitFunctionExpression(fn.env, instr.value.loweredFunc.func);395 break;396 }397 default: {398 /*399 * Else check usages of operands, but do *not* flow properties400 * from operands into the lvalues. For example, `let x = identity(y)`401 * does not infer `x` as a potential hook even if `y` is a potential hook.402 */403 for (const operand of eachInstructionOperand(instr)) {404 visitPlace(operand);405 }406 for (const lvalue of eachInstructionLValue(instr)) {407 const kind = getKindForPlace(lvalue);408 setKind(lvalue, kind);409 }410 }411 }412 }413 for (const operand of eachTerminalOperand(block.terminal)) {414 visitPlace(operand);415 }416 }417418 for (const [, error] of errorsByPlace) {419 fn.env.recordError(error);420 }421}422423function visitFunctionExpression(env: Environment, fn: HIRFunction): void {424 for (const [, block] of fn.body.blocks) {425 for (const instr of block.instructions) {426 switch (instr.value.kind) {427 case 'ObjectMethod':428 case 'FunctionExpression': {429 visitFunctionExpression(env, instr.value.loweredFunc.func);430 break;431 }432 case 'MethodCall':433 case 'CallExpression': {434 const callee =435 instr.value.kind === 'CallExpression'436 ? instr.value.callee437 : instr.value.property;438 const hookKind = getHookKind(fn.env, callee.identifier);439 if (hookKind != null) {440 env.recordError(441 new CompilerErrorDetail({442 category: ErrorCategory.Hooks,443 reason:444 '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)',445 loc: callee.loc,446 description: `Cannot call ${hookKind === 'Custom' ? 'hook' : hookKind} within a function expression`,447 suggestions: null,448 }),449 );450 }451 break;452 }453 }454 }455 }456}
Findings
✓ No findings reported for this file.