compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateHooksUsage.ts TYPESCRIPT 457 lines View on github.com → Search inside
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.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.