compiler/packages/babel-plugin-react-compiler/scripts/babel-plugin-annotate-react-code.ts TYPESCRIPT 294 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 type * as BabelCore from '@babel/core';9import {NodePath} from '@babel/core';10import * as t from '@babel/types';1112export default function AnnotateReactCodeBabelPlugin(13  _babel: typeof BabelCore,14): BabelCore.PluginObj {15  return {16    name: 'annotate-react-code',17    visitor: {18      Program(prog): void {19        annotate(prog);20      },21    },22  };23}2425function annotate(program: NodePath<t.Program>): void {26  function traverseFn(fn: BabelFn): void {27    if (!shouldVisit(fn)) {28      return;29    }3031    fn.skip();3233    const body = fn.node.body;34    if (t.isBlockStatement(body)) {35      body.body.unshift(buildTypeOfReactForget());36    }37  }3839  program.traverse({40    FunctionDeclaration: traverseFn,41    FunctionExpression: traverseFn,42    ArrowFunctionExpression: traverseFn,43  });44}4546function shouldVisit(fn: BabelFn): boolean {47  return (48    // Component declarations are known components49    (fn.isFunctionDeclaration() && isComponentDeclaration(fn.node)) ||50    // Otherwise check if this is a component or hook-like function51    isComponentOrHookLike(fn)52  );53}5455function buildTypeOfReactForget(): t.Statement {56  // typeof globalThis[Symbol.for("react_forget")]57  return t.expressionStatement(58    t.unaryExpression(59      'typeof',60      t.memberExpression(61        t.identifier('globalThis'),62        t.callExpression(63          t.memberExpression(64            t.identifier('Symbol'),65            t.identifier('for'),66            false,67            false,68          ),69          [t.stringLiteral('react_forget')],70        ),71        true,72        false,73      ),74      true,75    ),76  );77}7879/**80 * COPIED FROM babel-plugin-react-compiler/src/Entrypoint/BabelUtils.ts81 */82type ComponentDeclaration = t.FunctionDeclaration & {83  __componentDeclaration: boolean;84};8586type BabelFn =87  | NodePath<t.FunctionDeclaration>88  | NodePath<t.FunctionExpression>89  | NodePath<t.ArrowFunctionExpression>;9091export function isComponentDeclaration(92  node: t.FunctionDeclaration,93): node is ComponentDeclaration {94  return Object.prototype.hasOwnProperty.call(node, '__componentDeclaration');95}9697/*98 * Adapted from the ESLint rule at99 * https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#L90-L103100 */101function isComponentOrHookLike(102  node: NodePath<103    t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression104  >,105): boolean {106  const functionName = getFunctionName(node);107  // Check if the name is component or hook like:108  if (functionName !== null && isComponentName(functionName)) {109    return (110      // As an added check we also look for hook invocations or JSX111      callsHooksOrCreatesJsx(node) &&112      /*113       * and avoid helper functions that take more than one argument114       * helpers are _usually_ named with lowercase, but some code may115       * violate this rule116       */117      node.get('params').length <= 1118    );119  } else if (functionName !== null && isHook(functionName)) {120    // Hooks have hook invocations or JSX, but can take any # of arguments121    return callsHooksOrCreatesJsx(node);122  }123124  /*125   * Otherwise for function or arrow function expressions, check if they126   * appear as the argument to React.forwardRef() or React.memo():127   */128  if (node.isFunctionExpression() || node.isArrowFunctionExpression()) {129    if (isForwardRefCallback(node) || isMemoCallback(node)) {130      // As an added check we also look for hook invocations or JSX131      return callsHooksOrCreatesJsx(node);132    } else {133      return false;134    }135  }136  return false;137}138139function isHookName(s: string): boolean {140  return /^use[A-Z0-9]/.test(s);141}142143/*144 * We consider hooks to be a hook name identifier or a member expression145 * containing a hook name.146 */147148function isHook(path: NodePath<t.Expression | t.PrivateName>): boolean {149  if (path.isIdentifier()) {150    return isHookName(path.node.name);151  } else if (152    path.isMemberExpression() &&153    !path.node.computed &&154    isHook(path.get('property'))155  ) {156    const obj = path.get('object').node;157    const isPascalCaseNameSpace = /^[A-Z].*/;158    return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name);159  } else {160    return false;161  }162}163164/*165 * Checks if the node is a React component name. React component names must166 * always start with an uppercase letter.167 */168169function isComponentName(path: NodePath<t.Expression>): boolean {170  return path.isIdentifier() && /^[A-Z]/.test(path.node.name);171}172/*173 * Checks if the node is a callback argument of forwardRef. This render function174 * should follow the rules of hooks.175 */176177function isForwardRefCallback(path: NodePath<t.Expression>): boolean {178  return !!(179    path.parentPath.isCallExpression() &&180    path.parentPath.get('callee').isExpression() &&181    isReactAPI(path.parentPath.get('callee'), 'forwardRef')182  );183}184185/*186 * Checks if the node is a callback argument of React.memo. This anonymous187 * functional component should follow the rules of hooks.188 */189190function isMemoCallback(path: NodePath<t.Expression>): boolean {191  return (192    path.parentPath.isCallExpression() &&193    path.parentPath.get('callee').isExpression() &&194    isReactAPI(path.parentPath.get('callee'), 'memo')195  );196}197198function isReactAPI(199  path: NodePath<t.Expression | t.PrivateName | t.V8IntrinsicIdentifier>,200  functionName: string,201): boolean {202  const node = path.node;203  return (204    (node.type === 'Identifier' && node.name === functionName) ||205    (node.type === 'MemberExpression' &&206      node.object.type === 'Identifier' &&207      node.object.name === 'React' &&208      node.property.type === 'Identifier' &&209      node.property.name === functionName)210  );211}212213function callsHooksOrCreatesJsx(node: NodePath<t.Node>): boolean {214  let invokesHooks = false;215  let createsJsx = false;216  node.traverse({217    JSX() {218      createsJsx = true;219    },220    CallExpression(call) {221      const callee = call.get('callee');222      if (callee.isExpression() && isHook(callee)) {223        invokesHooks = true;224      }225    },226  });227228  return invokesHooks || createsJsx;229}230231/*232 * Gets the static name of a function AST node. For function declarations it is233 * easy. For anonymous function expressions it is much harder. If you search for234 * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places235 * where JS gives anonymous function expressions names. We roughly detect the236 * same AST nodes with some exceptions to better fit our use case.237 */238239function getFunctionName(240  path: NodePath<241    t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression242  >,243): NodePath<t.Expression> | null {244  if (path.isFunctionDeclaration()) {245    const id = path.get('id');246    if (id.isIdentifier()) {247      return id;248    }249    return null;250  }251  let id: NodePath<t.LVal | t.Expression | t.PrivateName> | null = null;252  const parent = path.parentPath;253  if (parent.isVariableDeclarator() && parent.get('init').node === path.node) {254    // const useHook = () => {};255    id = parent.get('id');256  } else if (257    parent.isAssignmentExpression() &&258    parent.get('right').node === path.node &&259    parent.get('operator') === '='260  ) {261    // useHook = () => {};262    id = parent.get('left');263  } else if (264    parent.isProperty() &&265    parent.get('value').node === path.node &&266    !parent.get('computed') &&267    parent.get('key').isLVal()268  ) {269    /*270     * {useHook: () => {}}271     * {useHook() {}}272     */273    id = parent.get('key');274  } else if (275    parent.isAssignmentPattern() &&276    parent.get('right').node === path.node &&277    !parent.get('computed')278  ) {279    /*280     * const {useHook = () => {}} = {};281     * ({useHook = () => {}} = {});282     *283     * Kinda clowny, but we'd said we'd follow spec convention for284     * `IsAnonymousFunctionDefinition()` usage.285     */286    id = parent.get('left');287  }288  if (id !== null && (id.isIdentifier() || id.isMemberExpression())) {289    return id;290  } else {291    return null;292  }293}

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.