compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts TYPESCRIPT 1,358 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 {NodePath} from '@babel/core';9import * as t from '@babel/types';10import {11  CompilerDiagnostic,12  CompilerError,13  CompilerErrorDetail,14  ErrorCategory,15} from '../CompilerError';16import {CompileErrorDetail} from './Options';17import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';18import {CodegenFunction} from '../ReactiveScopes';19import {isComponentDeclaration} from '../Utils/ComponentDeclaration';20import {isHookDeclaration} from '../Utils/HookDeclaration';21import {assertExhaustive} from '../Utils/utils';22import {insertGatedFunctionDeclaration} from './Gating';23import {24  addImportsToProgram,25  ProgramContext,26  validateRestrictedImports,27} from './Imports';28import {29  CompilerOutputMode,30  CompilerReactTarget,31  ParsedPluginOptions,32  PluginOptions,33} from './Options';34import {compileFn} from './Pipeline';35import {36  filterSuppressionsThatAffectFunction,37  findProgramSuppressions,38  suppressionsToCompilerError,39} from './Suppression';40import {GeneratedSource} from '../HIR';41import {Err, Ok, Result} from '../Utils/Result';4243export type CompilerPass = {44  opts: ParsedPluginOptions;45  filename: string | null;46  comments: Array<t.CommentBlock | t.CommentLine>;47  code: string | null;48};49export const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']);50export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);51const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$');5253export function tryFindDirectiveEnablingMemoization(54  directives: Array<t.Directive>,55  opts: ParsedPluginOptions,56): Result<t.Directive | null, CompilerError> {57  const optIn = directives.find(directive =>58    OPT_IN_DIRECTIVES.has(directive.value.value),59  );60  if (optIn != null) {61    return Ok(optIn);62  }63  const dynamicGating = findDirectivesDynamicGating(directives, opts);64  if (dynamicGating.isOk()) {65    return Ok(dynamicGating.unwrap()?.directive ?? null);66  } else {67    return Err(dynamicGating.unwrapErr());68  }69}7071export function findDirectiveDisablingMemoization(72  directives: Array<t.Directive>,73  {customOptOutDirectives}: PluginOptions,74): t.Directive | null {75  if (customOptOutDirectives != null) {76    return (77      directives.find(78        directive =>79          customOptOutDirectives.indexOf(directive.value.value) !== -1,80      ) ?? null81    );82  }83  return (84    directives.find(directive =>85      OPT_OUT_DIRECTIVES.has(directive.value.value),86    ) ?? null87  );88}89function findDirectivesDynamicGating(90  directives: Array<t.Directive>,91  opts: ParsedPluginOptions,92): Result<93  {94    gating: ExternalFunction;95    directive: t.Directive;96  } | null,97  CompilerError98> {99  if (opts.dynamicGating === null) {100    return Ok(null);101  }102  const errors = new CompilerError();103  const result: Array<{directive: t.Directive; match: string}> = [];104105  for (const directive of directives) {106    const maybeMatch = DYNAMIC_GATING_DIRECTIVE.exec(directive.value.value);107    if (maybeMatch != null && maybeMatch[1] != null) {108      if (t.isValidIdentifier(maybeMatch[1])) {109        result.push({directive, match: maybeMatch[1]});110      } else {111        errors.push({112          reason: `Dynamic gating directive is not a valid JavaScript identifier`,113          description: `Found '${directive.value.value}'`,114          category: ErrorCategory.Gating,115          loc: directive.loc ?? null,116          suggestions: null,117        });118      }119    }120  }121  if (errors.hasAnyErrors()) {122    return Err(errors);123  } else if (result.length > 1) {124    const error = new CompilerError();125    error.push({126      reason: `Multiple dynamic gating directives found`,127      description: `Expected a single directive but found [${result128        .map(r => r.directive.value.value)129        .join(', ')}]`,130      category: ErrorCategory.Gating,131      loc: result[0].directive.loc ?? null,132      suggestions: null,133    });134    return Err(error);135  } else if (result.length === 1) {136    return Ok({137      gating: {138        source: opts.dynamicGating.source,139        importSpecifierName: result[0].match,140      },141      directive: result[0].directive,142    });143  } else {144    return Ok(null);145  }146}147148function isError(err: unknown): boolean {149  return !(err instanceof CompilerError) || err.hasErrors();150}151152function isConfigError(err: unknown): boolean {153  if (err instanceof CompilerError) {154    return err.details.some(detail => detail.category === ErrorCategory.Config);155  }156  return false;157}158159export type BabelFn =160  | NodePath<t.FunctionDeclaration>161  | NodePath<t.FunctionExpression>162  | NodePath<t.ArrowFunctionExpression>;163164export type CompileResult = {165  /**166   * Distinguishes existing functions that were compiled ('original') from167   * functions which were outlined. Only original functions need to be gated168   * if gating mode is enabled.169   */170  kind: 'original' | 'outlined';171  originalFn: BabelFn;172  compiledFn: CodegenFunction;173};174175/**176 * Format a CompilerDiagnostic or CompilerErrorDetail class instance177 * into a plain object for logEvent(). This ensures the logged value178 * has all fields as direct properties (no getters, no nested `options`).179 */180export function formatDetailForLogging(181  detail: CompilerDiagnostic | CompilerErrorDetail,182): CompileErrorDetail {183  if (detail instanceof CompilerDiagnostic) {184    return {185      category: detail.category,186      reason: detail.reason,187      description: detail.description ?? null,188      severity: detail.severity,189      suggestions: detail.suggestions ?? null,190      details: detail.options.details.map(d => {191        if (d.kind === 'error') {192          const loc = d.loc != null && typeof d.loc !== 'symbol' ? d.loc : null;193          return {kind: d.kind, loc, message: d.message};194        } else {195          return {kind: d.kind, loc: null, message: d.message};196        }197      }),198    };199  } else {200    const loc =201      detail.loc != null && typeof detail.loc !== 'symbol' ? detail.loc : null;202    return {203      category: detail.category,204      reason: detail.reason,205      description: detail.description ?? null,206      severity: detail.severity,207      suggestions: detail.suggestions ?? null,208      loc,209    };210  }211}212213function logError(214  err: unknown,215  context: {216    opts: PluginOptions;217    filename: string | null;218  },219  fnLoc: t.SourceLocation | null,220): void {221  if (context.opts.logger) {222    if (err instanceof CompilerError) {223      for (const detail of err.details) {224        context.opts.logger.logEvent(context.filename, {225          kind: 'CompileError',226          fnLoc,227          detail: formatDetailForLogging(detail),228        });229      }230    } else {231      let stringifiedError;232      if (err instanceof Error) {233        stringifiedError = err.stack ?? err.message;234      } else {235        stringifiedError = err?.toString() ?? '[ null ]';236      }237238      context.opts.logger.logEvent(context.filename, {239        kind: 'PipelineError',240        fnLoc,241        data: stringifiedError,242      });243    }244  }245}246function handleError(247  err: unknown,248  context: {249    opts: PluginOptions;250    filename: string | null;251  },252  fnLoc: t.SourceLocation | null,253): void {254  logError(err, context, fnLoc);255  if (256    context.opts.panicThreshold === 'all_errors' ||257    (context.opts.panicThreshold === 'critical_errors' && isError(err)) ||258    isConfigError(err) // Always throws regardless of panic threshold259  ) {260    throw err;261  }262}263264export function createNewFunctionNode(265  originalFn: BabelFn,266  compiledFn: CodegenFunction,267): t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression {268  let transformedFn:269    | t.FunctionDeclaration270    | t.ArrowFunctionExpression271    | t.FunctionExpression;272  switch (originalFn.node.type) {273    case 'FunctionDeclaration': {274      const fn: t.FunctionDeclaration = {275        type: 'FunctionDeclaration',276        id: compiledFn.id,277        loc: originalFn.node.loc ?? null,278        async: compiledFn.async,279        generator: compiledFn.generator,280        params: compiledFn.params,281        body: compiledFn.body,282      };283      transformedFn = fn;284      break;285    }286    case 'ArrowFunctionExpression': {287      const fn: t.ArrowFunctionExpression = {288        type: 'ArrowFunctionExpression',289        loc: originalFn.node.loc ?? null,290        async: compiledFn.async,291        generator: compiledFn.generator,292        params: compiledFn.params,293        expression: originalFn.node.expression,294        body: compiledFn.body,295      };296      transformedFn = fn;297      break;298    }299    case 'FunctionExpression': {300      const fn: t.FunctionExpression = {301        type: 'FunctionExpression',302        id: compiledFn.id,303        loc: originalFn.node.loc ?? null,304        async: compiledFn.async,305        generator: compiledFn.generator,306        params: compiledFn.params,307        body: compiledFn.body,308      };309      transformedFn = fn;310      break;311    }312    default: {313      assertExhaustive(314        originalFn.node,315        `Creating unhandled function: ${originalFn.node}`,316      );317    }318  }319  // Avoid visiting the new transformed version320  return transformedFn;321}322323function insertNewOutlinedFunctionNode(324  program: NodePath<t.Program>,325  originalFn: BabelFn,326  compiledFn: CodegenFunction,327): BabelFn {328  switch (originalFn.type) {329    case 'FunctionDeclaration': {330      return originalFn.insertAfter(331        createNewFunctionNode(originalFn, compiledFn),332      )[0]!;333    }334    /**335     * We can't just append the outlined function as a sibling of the original function if it is an336     * (Arrow)FunctionExpression parented by a VariableDeclaration, as this would cause its parent337     * to become a SequenceExpression instead which breaks a bunch of assumptions elsewhere in the338     * plugin.339     *340     * To get around this, we always synthesize a new FunctionDeclaration for the outlined function341     * and insert it as a true sibling to the original function.342     */343    case 'ArrowFunctionExpression':344    case 'FunctionExpression': {345      const fn: t.FunctionDeclaration = {346        type: 'FunctionDeclaration',347        id: compiledFn.id,348        loc: originalFn.node.loc ?? null,349        async: compiledFn.async,350        generator: compiledFn.generator,351        params: compiledFn.params,352        body: compiledFn.body,353      };354      const insertedFuncDecl = program.pushContainer('body', [fn])[0]!;355      CompilerError.invariant(insertedFuncDecl.isFunctionDeclaration(), {356        reason: 'Expected inserted function declaration',357        description: `Got: ${insertedFuncDecl}`,358        loc: insertedFuncDecl.node?.loc ?? GeneratedSource,359      });360      return insertedFuncDecl;361    }362    default: {363      assertExhaustive(364        originalFn,365        `Inserting unhandled function: ${originalFn}`,366      );367    }368  }369}370371const DEFAULT_ESLINT_SUPPRESSIONS = [372  'react-hooks/exhaustive-deps',373  'react-hooks/rules-of-hooks',374];375376function isFilePartOfSources(377  sources: Array<string> | ((filename: string) => boolean),378  filename: string,379): boolean {380  if (typeof sources === 'function') {381    return sources(filename);382  }383384  for (const prefix of sources) {385    if (filename.indexOf(prefix) !== -1) {386      return true;387    }388  }389390  return false;391}392393/**394 * Main entrypoint for React Compiler.395 *396 * @param program The Babel program node to compile397 * @param pass Compiler configuration and context398 * @returns Compilation results or null if compilation was skipped399 */400export function compileProgram(401  program: NodePath<t.Program>,402  pass: CompilerPass,403): void {404  /**405   * This is directly invoked by the react-compiler babel plugin, so exceptions406   * thrown by this function will fail the babel build.407   * - call `handleError` if your error is recoverable.408   *   Unless the error is a warning / info diagnostic, compilation of a function409   *   / entire file should also be skipped.410   * - throw an exception if the error is fatal / not recoverable.411   *   Examples of this are invalid compiler configs or failure to codegen outlined412   *   functions *after* already emitting optimized components / hooks that invoke413   *   the outlined functions.414   */415  if (shouldSkipCompilation(program, pass)) {416    return;417  }418  const restrictedImportsErr = validateRestrictedImports(419    program,420    pass.opts.environment,421  );422  if (restrictedImportsErr) {423    handleError(restrictedImportsErr, pass, null);424    return;425  }426  /*427   * Record lint errors and critical errors as depending on Forget's config,428   * we may still need to run Forget's analysis on every function (even if we429   * have already encountered errors) for reporting.430   */431  const suppressions = findProgramSuppressions(432    pass.comments,433    /*434     * If the compiler is validating hooks rules and exhaustive memo dependencies, we don't need to check435     * for React ESLint suppressions436     */437    pass.opts.environment.validateExhaustiveMemoizationDependencies &&438      pass.opts.environment.validateHooksUsage439      ? null440      : (pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS),441    // Always bail on Flow suppressions442    pass.opts.flowSuppressions,443  );444445  const programContext = new ProgramContext({446    program: program,447    opts: pass.opts,448    filename: pass.filename,449    code: pass.code,450    suppressions,451    hasModuleScopeOptOut:452      findDirectiveDisablingMemoization(program.node.directives, pass.opts) !=453      null,454  });455456  const queue: Array<CompileSource> = findFunctionsToCompile(457    program,458    pass,459    programContext,460  );461  const compiledFns: Array<CompileResult> = [];462463  // outputMode takes precedence if specified464  const outputMode: CompilerOutputMode =465    pass.opts.outputMode ?? (pass.opts.noEmit ? 'lint' : 'client');466  while (queue.length !== 0) {467    const current = queue.shift()!;468    const compiled = processFn(469      current.fn,470      current.fnType,471      programContext,472      outputMode,473    );474475    if (compiled != null) {476      for (const outlined of compiled.outlined) {477        CompilerError.invariant(outlined.fn.outlined.length === 0, {478          reason: 'Unexpected nested outlined functions',479          loc: outlined.fn.loc,480        });481        const fn = insertNewOutlinedFunctionNode(482          program,483          current.fn,484          outlined.fn,485        );486        fn.skip();487        programContext.alreadyCompiled.add(fn.node);488        if (outlined.type !== null) {489          queue.push({490            kind: 'outlined',491            fn,492            fnType: outlined.type,493          });494        }495      }496      compiledFns.push({497        kind: current.kind,498        originalFn: current.fn,499        compiledFn: compiled,500      });501    }502  }503504  // Avoid modifying the program if we find a program level opt-out505  if (programContext.hasModuleScopeOptOut) {506    if (compiledFns.length > 0) {507      const error = new CompilerError();508      error.pushErrorDetail(509        new CompilerErrorDetail({510          reason:511            'Unexpected compiled functions when module scope opt-out is present',512          category: ErrorCategory.Invariant,513          loc: null,514        }),515      );516      handleError(error, programContext, null);517    }518    return;519  }520521  // Insert React Compiler generated functions into the Babel AST522  applyCompiledFunctions(program, compiledFns, pass, programContext);523}524525type CompileSource = {526  kind: 'original' | 'outlined';527  fn: BabelFn;528  fnType: ReactFunctionType;529};530/**531 * Find all React components and hooks that need to be compiled532 *533 * @returns An array of React functions from @param program to transform534 */535function findFunctionsToCompile(536  program: NodePath<t.Program>,537  pass: CompilerPass,538  programContext: ProgramContext,539): Array<CompileSource> {540  const queue: Array<CompileSource> = [];541  const traverseFunction = (fn: BabelFn, pass: CompilerPass): void => {542    // In 'all' mode, compile only top level functions543    if (544      pass.opts.compilationMode === 'all' &&545      fn.scope.getProgramParent() !== fn.scope.parent546    ) {547      return;548    }549550    const fnType = getReactFunctionType(fn, pass);551552    if (fnType === null || programContext.alreadyCompiled.has(fn.node)) {553      return;554    }555556    /*557     * We may be generating a new FunctionDeclaration node, so we must skip over it or this558     * traversal will loop infinitely.559     * Ensure we avoid visiting the original function again.560     */561    programContext.alreadyCompiled.add(fn.node);562    fn.skip();563564    queue.push({kind: 'original', fn, fnType});565  };566567  // Main traversal to compile with Forget568  program.traverse(569    {570      ClassDeclaration(node: NodePath<t.ClassDeclaration>) {571        /*572         * Don't visit functions defined inside classes, because they573         * can reference `this` which is unsafe for compilation574         */575        node.skip();576      },577578      ClassExpression(node: NodePath<t.ClassExpression>) {579        /*580         * Don't visit functions defined inside classes, because they581         * can reference `this` which is unsafe for compilation582         */583        node.skip();584      },585586      FunctionDeclaration: traverseFunction,587588      FunctionExpression: traverseFunction,589590      ArrowFunctionExpression: traverseFunction,591    },592    {593      ...pass,594      opts: {...pass.opts, ...pass.opts},595      filename: pass.filename ?? null,596    },597  );598  return queue;599}600601/**602 * Try to compile a source function, taking into account all local suppressions,603 * opt-ins, and opt-outs.604 *605 * Errors encountered during compilation are either logged (if recoverable) or606 * thrown (if non-recoverable).607 *608 * @returns the compiled function or null if the function was skipped (due to609 * config settings and/or outputs)610 */611function processFn(612  fn: BabelFn,613  fnType: ReactFunctionType,614  programContext: ProgramContext,615  outputMode: CompilerOutputMode,616): null | CodegenFunction {617  let directives: {618    optIn: t.Directive | null;619    optOut: t.Directive | null;620  };621  if (fn.node.body.type !== 'BlockStatement') {622    directives = {623      optIn: null,624      optOut: null,625    };626  } else {627    const optIn = tryFindDirectiveEnablingMemoization(628      fn.node.body.directives,629      programContext.opts,630    );631    if (optIn.isErr()) {632      /**633       * If parsing opt-in directive fails, it's most likely that React Compiler634       * was not tested or rolled out on this function. In that case, we handle635       * the error and fall back to the safest option which is to not optimize636       * the function.637       */638      handleError(optIn.unwrapErr(), programContext, fn.node.loc ?? null);639      return null;640    }641    directives = {642      optIn: optIn.unwrapOr(null),643      optOut: findDirectiveDisablingMemoization(644        fn.node.body.directives,645        programContext.opts,646      ),647    };648  }649650  let compiledFn: CodegenFunction;651  const compileResult = tryCompileFunction(652    fn,653    fnType,654    programContext,655    outputMode,656  );657  if (compileResult.kind === 'error') {658    if (directives.optOut != null) {659      logError(compileResult.error, programContext, fn.node.loc ?? null);660    } else {661      handleError(compileResult.error, programContext, fn.node.loc ?? null);662    }663    return null;664  } else {665    compiledFn = compileResult.compiledFn;666  }667668  /**669   * If 'use no forget/memo' is present and we still ran the code through the670   * compiler for validation, log a skip event and don't mutate the babel AST.671   * This allows us to flag if there is an unused 'use no forget/memo'672   * directive.673   */674  if (675    programContext.opts.ignoreUseNoForget === false &&676    directives.optOut != null677  ) {678    programContext.logEvent({679      kind: 'CompileSkip',680      fnLoc: fn.node.body.loc ?? null,681      reason: `Skipped due to '${directives.optOut.value.value}' directive.`,682      loc: directives.optOut.loc ?? null,683    });684    return null;685  }686  programContext.logEvent({687    kind: 'CompileSuccess',688    fnLoc: fn.node.loc ?? null,689    fnName: compiledFn.id?.name ?? null,690    memoSlots: compiledFn.memoSlotsUsed,691    memoBlocks: compiledFn.memoBlocks,692    memoValues: compiledFn.memoValues,693    prunedMemoBlocks: compiledFn.prunedMemoBlocks,694    prunedMemoValues: compiledFn.prunedMemoValues,695  });696697  if (programContext.hasModuleScopeOptOut) {698    return null;699  } else if (programContext.opts.outputMode === 'lint') {700    return null;701  } else if (702    programContext.opts.compilationMode === 'annotation' &&703    directives.optIn == null704  ) {705    /**706     * If no opt-in directive is found and the compiler is configured in707     * annotation mode, don't insert the compiled function.708     */709    return null;710  } else {711    return compiledFn;712  }713}714715function tryCompileFunction(716  fn: BabelFn,717  fnType: ReactFunctionType,718  programContext: ProgramContext,719  outputMode: CompilerOutputMode,720):721  | {kind: 'compile'; compiledFn: CodegenFunction}722  | {kind: 'error'; error: unknown} {723  /**724   * Note that Babel does not attach comment nodes to nodes; they are dangling off of the725   * Program node itself. We need to figure out whether an eslint suppression range726   * applies to this function first.727   */728  const suppressionsInFunction = filterSuppressionsThatAffectFunction(729    programContext.suppressions,730    fn,731  );732  if (suppressionsInFunction.length > 0) {733    return {734      kind: 'error',735      error: suppressionsToCompilerError(suppressionsInFunction),736    };737  }738739  try {740    const result = compileFn(741      fn,742      programContext.opts.environment,743      fnType,744      outputMode,745      programContext,746      programContext.opts.logger,747      programContext.filename,748      programContext.code,749    );750    if (result.isOk()) {751      return {kind: 'compile', compiledFn: result.unwrap()};752    } else {753      return {kind: 'error', error: result.unwrapErr()};754    }755  } catch (err) {756    /**757     * A pass incorrectly threw instead of recording the error.758     * Log for detection in development.759     */760    if (761      err instanceof CompilerError &&762      err.details.every(detail => detail.category !== ErrorCategory.Invariant)763    ) {764      programContext.logEvent({765        kind: 'CompileUnexpectedThrow',766        fnLoc: fn.node.loc ?? null,767        data: err.toString(),768      });769    }770    return {kind: 'error', error: err};771  }772}773774/**775 * Applies React Compiler generated functions to the babel AST by replacing776 * existing functions in place or inserting new declarations.777 */778function applyCompiledFunctions(779  program: NodePath<t.Program>,780  compiledFns: Array<CompileResult>,781  pass: CompilerPass,782  programContext: ProgramContext,783): void {784  let referencedBeforeDeclared = null;785  for (const result of compiledFns) {786    const {kind, originalFn, compiledFn} = result;787    const transformedFn = createNewFunctionNode(originalFn, compiledFn);788    programContext.alreadyCompiled.add(transformedFn);789790    let dynamicGating: ExternalFunction | null = null;791    if (originalFn.node.body.type === 'BlockStatement') {792      const result = findDirectivesDynamicGating(793        originalFn.node.body.directives,794        pass.opts,795      );796      if (result.isOk()) {797        dynamicGating = result.unwrap()?.gating ?? null;798      }799    }800    const functionGating = dynamicGating ?? pass.opts.gating;801    if (kind === 'original' && functionGating != null) {802      referencedBeforeDeclared ??=803        getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns);804      insertGatedFunctionDeclaration(805        originalFn,806        transformedFn,807        programContext,808        functionGating,809        referencedBeforeDeclared.has(result),810      );811    } else {812      originalFn.replaceWith(transformedFn);813    }814  }815816  // Forget compiled the component, we need to update existing imports of useMemoCache817  if (compiledFns.length > 0) {818    // Codegen may have registered `_c` for a function that was later discarded.819    const anyAppliedUsesMemo = compiledFns.some(820      result => result.compiledFn.memoSlotsUsed > 0,821    );822    if (!anyAppliedUsesMemo) {823      programContext.removeMemoCacheImport();824    }825    addImportsToProgram(program, programContext);826  }827}828829function shouldSkipCompilation(830  program: NodePath<t.Program>,831  pass: CompilerPass,832): boolean {833  if (pass.opts.sources) {834    if (pass.filename === null) {835      const error = new CompilerError();836      error.pushErrorDetail(837        new CompilerErrorDetail({838          reason: `Expected a filename but found none.`,839          description:840            "When the 'sources' config options is specified, the React compiler will only compile files with a name",841          category: ErrorCategory.Config,842          loc: null,843        }),844      );845      handleError(error, pass, null);846      return true;847    }848849    if (!isFilePartOfSources(pass.opts.sources, pass.filename)) {850      return true;851    }852  }853854  if (855    hasMemoCacheFunctionImport(856      program,857      getReactCompilerRuntimeModule(pass.opts.target),858    )859  ) {860    return true;861  }862  return false;863}864865function getReactFunctionType(866  fn: BabelFn,867  pass: CompilerPass,868): ReactFunctionType | null {869  if (fn.node.body.type === 'BlockStatement') {870    const optInDirectives = tryFindDirectiveEnablingMemoization(871      fn.node.body.directives,872      pass.opts,873    );874    if (optInDirectives.unwrapOr(null) != null) {875      return getComponentOrHookLike(fn) ?? 'Other';876    }877  }878879  // Component and hook declarations are known components/hooks880  let componentSyntaxType: ReactFunctionType | null = null;881  if (fn.isFunctionDeclaration()) {882    if (isComponentDeclaration(fn.node)) {883      componentSyntaxType = 'Component';884    } else if (isHookDeclaration(fn.node)) {885      componentSyntaxType = 'Hook';886    }887  }888889  switch (pass.opts.compilationMode) {890    case 'annotation': {891      // opt-ins are checked above892      return null;893    }894    case 'infer': {895      // Check if this is a component or hook-like function896      return componentSyntaxType ?? getComponentOrHookLike(fn);897    }898    case 'syntax': {899      return componentSyntaxType;900    }901    case 'all': {902      return getComponentOrHookLike(fn) ?? 'Other';903    }904    default: {905      assertExhaustive(906        pass.opts.compilationMode,907        `Unexpected compilationMode \`${pass.opts.compilationMode}\``,908      );909    }910  }911}912913/**914 * Returns true if the program contains an `import {c} from "<moduleName>"` declaration,915 * regardless of the local name of the 'c' specifier and the presence of other specifiers916 * in the same declaration.917 */918function hasMemoCacheFunctionImport(919  program: NodePath<t.Program>,920  moduleName: string,921): boolean {922  let hasUseMemoCache = false;923  program.traverse({924    ImportSpecifier(path) {925      const imported = path.get('imported');926      let importedName: string | null = null;927      if (imported.isIdentifier()) {928        importedName = imported.node.name;929      } else if (imported.isStringLiteral()) {930        importedName = imported.node.value;931      }932      if (933        importedName === 'c' &&934        path.parentPath.isImportDeclaration() &&935        path.parentPath.get('source').node.value === moduleName936      ) {937        hasUseMemoCache = true;938      }939    },940  });941  return hasUseMemoCache;942}943944function isHookName(s: string): boolean {945  return /^use[A-Z0-9]/.test(s);946}947948/*949 * We consider hooks to be a hook name identifier or a member expression950 * containing a hook name.951 */952953function isHook(path: NodePath<t.Expression | t.PrivateName>): boolean {954  if (path.isIdentifier()) {955    return isHookName(path.node.name);956  } else if (957    path.isMemberExpression() &&958    !path.node.computed &&959    isHook(path.get('property'))960  ) {961    const obj = path.get('object').node;962    const isPascalCaseNameSpace = /^[A-Z].*/;963    return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name);964  } else {965    return false;966  }967}968969/*970 * Checks if the node is a React component name. React component names must971 * always start with an uppercase letter.972 */973974function isComponentName(path: NodePath<t.Expression>): boolean {975  return path.isIdentifier() && /^[A-Z]/.test(path.node.name);976}977978function isReactAPI(979  path: NodePath<t.Expression | t.PrivateName | t.V8IntrinsicIdentifier>,980  functionName: string,981): boolean {982  const node = path.node;983  return (984    (node.type === 'Identifier' && node.name === functionName) ||985    (node.type === 'MemberExpression' &&986      node.object.type === 'Identifier' &&987      node.object.name === 'React' &&988      node.property.type === 'Identifier' &&989      node.property.name === functionName)990  );991}992993/*994 * Checks if the node is a callback argument of forwardRef. This render function995 * should follow the rules of hooks.996 */997998function isForwardRefCallback(path: NodePath<t.Expression>): boolean {999  return !!(1000    path.parentPath.isCallExpression() &&1001    path.parentPath.get('callee').isExpression() &&1002    isReactAPI(path.parentPath.get('callee'), 'forwardRef')1003  );1004}10051006/*1007 * Checks if the node is a callback argument of React.memo. This anonymous1008 * functional component should follow the rules of hooks.1009 */10101011function isMemoCallback(path: NodePath<t.Expression>): boolean {1012  return (1013    path.parentPath.isCallExpression() &&1014    path.parentPath.get('callee').isExpression() &&1015    isReactAPI(path.parentPath.get('callee'), 'memo')1016  );1017}10181019function isValidPropsAnnotation(1020  annot: t.TypeAnnotation | t.TSTypeAnnotation | t.Noop | null | undefined,1021): boolean {1022  if (annot == null) {1023    return true;1024  } else if (annot.type === 'TSTypeAnnotation') {1025    switch (annot.typeAnnotation.type) {1026      case 'TSArrayType':1027      case 'TSBigIntKeyword':1028      case 'TSBooleanKeyword':1029      case 'TSConstructorType':1030      case 'TSFunctionType':1031      case 'TSLiteralType':1032      case 'TSNeverKeyword':1033      case 'TSNumberKeyword':1034      case 'TSStringKeyword':1035      case 'TSSymbolKeyword':1036      case 'TSTupleType':1037        return false;1038    }1039    return true;1040  } else if (annot.type === 'TypeAnnotation') {1041    switch (annot.typeAnnotation.type) {1042      case 'ArrayTypeAnnotation':1043      case 'BooleanLiteralTypeAnnotation':1044      case 'BooleanTypeAnnotation':1045      case 'EmptyTypeAnnotation':1046      case 'FunctionTypeAnnotation':1047      case 'NumberLiteralTypeAnnotation':1048      case 'NumberTypeAnnotation':1049      case 'StringLiteralTypeAnnotation':1050      case 'StringTypeAnnotation':1051      case 'SymbolTypeAnnotation':1052      case 'ThisTypeAnnotation':1053      case 'TupleTypeAnnotation':1054        return false;1055    }1056    return true;1057  } else if (annot.type === 'Noop') {1058    return true;1059  } else {1060    assertExhaustive(annot, `Unexpected annotation node \`${annot}\``);1061  }1062}10631064function isValidComponentParams(1065  params: Array<NodePath<t.Identifier | t.Pattern | t.RestElement>>,1066): boolean {1067  if (params.length === 0) {1068    return true;1069  } else if (params.length > 0 && params.length <= 2) {1070    if (!isValidPropsAnnotation(params[0].node.typeAnnotation)) {1071      return false;1072    }10731074    if (params.length === 1) {1075      return !params[0].isRestElement();1076    } else if (params[1].isIdentifier()) {1077      // check if second param might be a ref1078      const {name} = params[1].node;1079      return name.includes('ref') || name.includes('Ref');1080    } else {1081      /**1082       * Otherwise, avoid helper functions that take more than one argument.1083       * Helpers are _usually_ named with lowercase, but some code may1084       * violate this rule1085       */1086      return false;1087    }1088  }1089  return false;1090}10911092/*1093 * Adapted from the ESLint rule at1094 * https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#L90-L1031095 */1096function getComponentOrHookLike(1097  node: NodePath<1098    t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression1099  >,1100): ReactFunctionType | null {1101  const functionName = getFunctionName(node);1102  // Check if the name is component or hook like:1103  if (functionName !== null && isComponentName(functionName)) {1104    let isComponent =1105      callsHooksOrCreatesJsx(node) &&1106      isValidComponentParams(node.get('params')) &&1107      !returnsNonNode(node);1108    return isComponent ? 'Component' : null;1109  } else if (functionName !== null && isHook(functionName)) {1110    // Hooks have hook invocations or JSX, but can take any # of arguments1111    return callsHooksOrCreatesJsx(node) ? 'Hook' : null;1112  }11131114  /*1115   * Otherwise for function or arrow function expressions, check if they1116   * appear as the argument to React.forwardRef() or React.memo():1117   */1118  if (node.isFunctionExpression() || node.isArrowFunctionExpression()) {1119    if (isForwardRefCallback(node) || isMemoCallback(node)) {1120      // As an added check we also look for hook invocations or JSX1121      return callsHooksOrCreatesJsx(node) ? 'Component' : null;1122    }1123  }1124  return null;1125}11261127function skipNestedFunctions(1128  node: NodePath<1129    t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression1130  >,1131) {1132  return (1133    fn: NodePath<1134      t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression1135    >,1136  ): void => {1137    if (fn.node !== node.node) {1138      fn.skip();1139    }1140  };1141}11421143function callsHooksOrCreatesJsx(1144  node: NodePath<1145    t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression1146  >,1147): boolean {1148  let invokesHooks = false;1149  let createsJsx = false;11501151  node.traverse({1152    JSX() {1153      createsJsx = true;1154    },1155    CallExpression(call) {1156      const callee = call.get('callee');1157      if (callee.isExpression() && isHook(callee)) {1158        invokesHooks = true;1159      }1160    },1161    ArrowFunctionExpression: skipNestedFunctions(node),1162    FunctionExpression: skipNestedFunctions(node),1163    FunctionDeclaration: skipNestedFunctions(node),1164  });11651166  return invokesHooks || createsJsx;1167}11681169function isNonNode(node?: t.Expression | null): boolean {1170  if (!node) {1171    return true;1172  }1173  switch (node.type) {1174    case 'ObjectExpression':1175    case 'ArrowFunctionExpression':1176    case 'FunctionExpression':1177    case 'BigIntLiteral':1178    case 'ClassExpression':1179    case 'NewExpression': // technically `new Array()` is legit, but unlikely1180      return true;1181  }1182  return false;1183}11841185function returnsNonNode(1186  node: NodePath<1187    t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression1188  >,1189): boolean {1190  let returnsNonNode = false;1191  if (1192    // node.traverse#ArrowFunctionExpression isn't called for the root node1193    node.type === 'ArrowFunctionExpression' &&1194    node.node.body.type !== 'BlockStatement'1195  ) {1196    returnsNonNode = isNonNode(node.node.body);1197  }11981199  node.traverse({1200    ReturnStatement(ret) {1201      returnsNonNode = isNonNode(ret.node.argument);1202    },1203    // Skip traversing all nested functions and their return statements1204    ArrowFunctionExpression: skipNestedFunctions(node),1205    FunctionExpression: skipNestedFunctions(node),1206    FunctionDeclaration: skipNestedFunctions(node),1207    ObjectMethod: node => node.skip(),1208  });12091210  return returnsNonNode;1211}12121213/*1214 * Gets the static name of a function AST node. For function declarations it is1215 * easy. For anonymous function expressions it is much harder. If you search for1216 * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places1217 * where JS gives anonymous function expressions names. We roughly detect the1218 * same AST nodes with some exceptions to better fit our use case.1219 */12201221function getFunctionName(1222  path: NodePath<1223    t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression1224  >,1225): NodePath<t.Expression> | null {1226  if (path.isFunctionDeclaration()) {1227    const id = path.get('id');1228    if (id.isIdentifier()) {1229      return id;1230    }1231    return null;1232  }1233  let id: NodePath<t.LVal | t.Expression | t.PrivateName> | null = null;1234  const parent = path.parentPath;1235  if (parent.isVariableDeclarator() && parent.get('init').node === path.node) {1236    // const useHook = () => {};1237    id = parent.get('id');1238  } else if (1239    parent.isAssignmentExpression() &&1240    parent.get('right').node === path.node &&1241    parent.get('operator') === '='1242  ) {1243    // useHook = () => {};1244    id = parent.get('left');1245  } else if (1246    parent.isProperty() &&1247    parent.get('value').node === path.node &&1248    !parent.get('computed') &&1249    parent.get('key').isLVal()1250  ) {1251    /*1252     * {useHook: () => {}}1253     * {useHook() {}}1254     */1255    id = parent.get('key');1256  } else if (1257    parent.isAssignmentPattern() &&1258    parent.get('right').node === path.node &&1259    !parent.get('computed')1260  ) {1261    /*1262     * const {useHook = () => {}} = {};1263     * ({useHook = () => {}} = {});1264     *1265     * Kinda clowny, but we'd said we'd follow spec convention for1266     * `IsAnonymousFunctionDefinition()` usage.1267     */1268    id = parent.get('left');1269  }1270  if (id !== null && (id.isIdentifier() || id.isMemberExpression())) {1271    return id;1272  } else {1273    return null;1274  }1275}12761277function getFunctionReferencedBeforeDeclarationAtTopLevel(1278  program: NodePath<t.Program>,1279  fns: Array<CompileResult>,1280): Set<CompileResult> {1281  const fnNames = new Map<string, {id: t.Identifier; fn: CompileResult}>(1282    fns1283      .map<[NodePath<t.Expression> | null, CompileResult]>(fn => [1284        getFunctionName(fn.originalFn),1285        fn,1286      ])1287      .filter(1288        (entry): entry is [NodePath<t.Identifier>, CompileResult] =>1289          !!entry[0] && entry[0].isIdentifier(),1290      )1291      .map(entry => [entry[0].node.name, {id: entry[0].node, fn: entry[1]}]),1292  );1293  const referencedBeforeDeclaration = new Set<CompileResult>();12941295  program.traverse({1296    TypeAnnotation(path) {1297      path.skip();1298    },1299    TSTypeAnnotation(path) {1300      path.skip();1301    },1302    TypeAlias(path) {1303      path.skip();1304    },1305    TSTypeAliasDeclaration(path) {1306      path.skip();1307    },1308    Identifier(id) {1309      const fn = fnNames.get(id.node.name);1310      // We're not tracking this identifier.1311      if (!fn) {1312        return;1313      }13141315      /*1316       * We've reached the declaration, hoisting is no longer possible, stop1317       * checking for this component name.1318       */1319      if (id.node === fn.id) {1320        fnNames.delete(id.node.name);1321        return;1322      }13231324      const scope = id.scope.getFunctionParent();1325      /*1326       * A null scope means there's no function scope, which means we're at the1327       * top level scope.1328       */1329      if (scope === null && id.isReferencedIdentifier()) {1330        referencedBeforeDeclaration.add(fn.fn);1331      }1332    },1333  });13341335  return referencedBeforeDeclaration;1336}13371338export function getReactCompilerRuntimeModule(1339  target: CompilerReactTarget,1340): string {1341  if (target === '19') {1342    return 'react/compiler-runtime'; // from react namespace1343  } else if (target === '17' || target === '18') {1344    return 'react-compiler-runtime'; // npm package1345  } else {1346    CompilerError.invariant(1347      target != null &&1348        target.kind === 'donotuse_meta_internal' &&1349        typeof target.runtimeModule === 'string',1350      {1351        reason: 'Expected target to already be validated',1352        loc: GeneratedSource,1353      },1354    );1355    return target.runtimeModule;1356  }1357}

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.