compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts TYPESCRIPT 1,094 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 {ZodError, z} from 'zod/v4';10import {fromZodError} from 'zod-validation-error/v4';11import {12  CompilerDiagnostic,13  CompilerError,14  CompilerErrorDetail,15  ErrorCategory,16} from '../CompilerError';17import {18  CompilerOutputMode,19  Logger,20  ProgramContext,21  formatDetailForLogging,22} from '../Entrypoint';23import {Err, Ok, Result} from '../Utils/Result';24import {25  DEFAULT_GLOBALS,26  DEFAULT_SHAPES,27  Global,28  GlobalRegistry,29  getReanimatedModuleType,30  installTypeConfig,31} from './Globals';32import {33  BlockId,34  BuiltInType,35  Effect,36  FunctionType,37  GeneratedSource,38  HIRFunction,39  IdentifierId,40  NonLocalBinding,41  PolyType,42  ScopeId,43  SourceLocation,44  Type,45  ValidatedIdentifier,46  ValueKind,47  getHookKindForType,48  makeBlockId,49  makeIdentifierId,50  makeIdentifierName,51  makeScopeId,52} from './HIR';53import {54  BuiltInMixedReadonlyId,55  DefaultMutatingHook,56  DefaultNonmutatingHook,57  FunctionSignature,58  ShapeRegistry,59  addHook,60} from './ObjectShape';61import {Scope as BabelScope, NodePath} from '@babel/traverse';62import {TypeSchema} from './TypeSchema';63import {FlowTypeEnv} from '../Flood/Types';64import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';65import {assertExhaustive} from '../Utils/utils';6667export const ExternalFunctionSchema = z.object({68  // Source for the imported module that exports the `importSpecifierName` functions69  source: z.string(),7071  // Unique name for the feature flag test condition, eg `isForgetEnabled_ProjectName`72  importSpecifierName: z.string(),73});7475export const InstrumentationSchema = z76  .object({77    fn: ExternalFunctionSchema,78    gating: ExternalFunctionSchema.nullable(),79    globalGating: z.string().nullable(),80  })81  .refine(82    opts => opts.gating != null || opts.globalGating != null,83    'Expected at least one of gating or globalGating',84  );8586export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;8788export const MacroSchema = z.string();8990export type CompilerMode = 'all_features' | 'no_inferred_memo';9192export type Macro = z.infer<typeof MacroSchema>;9394const HookSchema = z.object({95  /*96   * The effect of arguments to this hook. Describes whether the hook may or may97   * not mutate arguments, etc.98   */99  effectKind: z.nativeEnum(Effect),100101  /*102   * The kind of value returned by the hook. Allows indicating that a hook returns103   * a primitive or already-frozen value, which can allow more precise memoization104   * of callers.105   */106  valueKind: z.nativeEnum(ValueKind),107108  /*109   * Specifies whether hook arguments may be aliased by other arguments or by the110   * return value of the function. Defaults to false. When enabled, this allows the111   * compiler to avoid memoizing arguments.112   */113  noAlias: z.boolean().default(false),114115  /*116   * Specifies whether the hook returns data that is composed of:117   * - undefined118   * - null119   * - boolean120   * - number121   * - string122   * - arrays whose items are also transitiveMixed123   * - objects whose values are also transitiveMixed124   *125   * Many state management and data-fetching APIs return data that meets126   * this criteria since this is JSON + undefined. Forget can compile127   * hooks that return transitively mixed data more optimally because it128   * can make inferences about some method calls (especially array methods129   * like `data.items.map(...)` since these builtin types have few built-in130   * methods.131   */132  transitiveMixedData: z.boolean().default(false),133});134135export type Hook = z.infer<typeof HookSchema>;136137/*138 * TODO(mofeiZ): User defined global types (with corresponding shapes).139 * User defined global types should have inline ObjectShapes instead of directly140 * using ObjectShapes.ShapeRegistry, as a user-provided ShapeRegistry may be141 * accidentally be not well formed.142 * i.e.143 *   missing required shapes (BuiltInArray for [] and BuiltInObject for {})144 *   missing some recursive Object / Function shapeIds145 */146147export const EnvironmentConfigSchema = z.object({148  customHooks: z.map(z.string(), HookSchema).default(new Map()),149150  /**151   * A function that, given the name of a module, can optionally return a description152   * of that module's type signature.153   */154  moduleTypeProvider: z.nullable(z.any()).default(null),155156  /**157   * A list of functions which the application compiles as macros, where158   * the compiler must ensure they are not compiled to rename the macro or separate the159   * "function" from its argument.160   *161   * For example, Meta has some APIs such as `featureflag("name-of-feature-flag")` which162   * are rewritten by a plugin. Assigning `featureflag` to a temporary would break the163   * plugin since it looks specifically for the name of the function being invoked, not164   * following aliases.165   */166  customMacros: z.nullable(z.array(MacroSchema)).default(null),167168  /**169   * Enable a check that resets the memoization cache when the source code of170   * the file changes. This is intended to support hot module reloading (HMR),171   * where the same runtime component instance will be reused across different172   * versions of the component source.173   *174   * When set to175   * - true:  code for HMR support is always generated, regardless of NODE_ENV176   *          or `globalThis.__DEV__`177   * - false: code for HMR support is not generated178   * - null:  (default) code for HMR support is conditionally generated dependent179   *          on `NODE_ENV` and `globalThis.__DEV__` at the time of compilation.180   */181  enableResetCacheOnSourceFileChanges: z.nullable(z.boolean()).default(null),182183  /**184   * Enable using information from existing useMemo/useCallback to understand when a value is done185   * being mutated. With this mode enabled, Forget will still discard the actual useMemo/useCallback186   * calls and may memoize slightly differently. However, it will assume that the values produced187   * are not subsequently modified, guaranteeing that the value will be memoized.188   *189   * By preserving guarantees about when values are memoized, this option preserves any existing190   * behavior that depends on referential equality in the original program. Notably, this preserves191   * existing effect behavior (how often effects fire) for effects that rely on referential equality.192   *193   * When disabled, Forget will not only prune useMemo and useCallback calls but also completely ignore194   * them, not using any information from them to guide compilation. Therefore, disabling this flag195   * will produce output that mimics the result from removing all memoization.196   *197   * Our recommendation is to first try running your application with this flag enabled, then attempt198   * to disable this flag and see what changes or breaks. This will mostly likely be effects that199   * depend on referential equality, which can be refactored (TODO guide for this).200   *201   * NOTE: this mode treats freeze as a transitive operation for function expressions. This means202   * that if a useEffect or useCallback references a function value, that function value will be203   * considered frozen, and in turn all of its referenced variables will be considered frozen as well.204   */205  enablePreserveExistingMemoizationGuarantees: z.boolean().default(true),206207  /**208   * Validates that all useMemo/useCallback values are also memoized by Forget. This mode can be209   * used with or without @enablePreserveExistingMemoizationGuarantees.210   *211   * With enablePreserveExistingMemoizationGuarantees, this validation enables automatically and212   * verifies that Forget was able to preserve manual memoization semantics under that mode's213   * additional assumptions about the input.214   *215   * With enablePreserveExistingMemoizationGuarantees off, this validation ignores manual memoization216   * when determining program behavior, and only uses information from useMemo/useCallback to check217   * that the memoization was preserved. This can be useful for determining where referential equalities218   * may change under Forget.219   */220  validatePreserveExistingMemoizationGuarantees: z.boolean().default(true),221222  /**223   * Validate that dependencies supplied to manual memoization calls are exhaustive.224   */225  validateExhaustiveMemoizationDependencies: z.boolean().default(true),226227  /**228   * Validate that dependencies supplied to effect hooks are exhaustive.229   * Can be:230   * - 'off': No validation (default)231   * - 'all': Validate and report both missing and extra dependencies232   * - 'missing-only': Only report missing dependencies233   * - 'extra-only': Only report extra/unnecessary dependencies234   */235  validateExhaustiveEffectDependencies: z236    .enum(['off', 'all', 'missing-only', 'extra-only'])237    .default('off'),238239  // 🌲240  enableForest: z.boolean().default(false),241242  /**243   * Allows specifying a function that can populate HIR with type information from244   * Flow245   */246  flowTypeProvider: z.nullable(z.any()).default(null),247248  /**249   * Enables inference of optional dependency chains. Without this flag250   * a property chain such as `props?.items?.foo` will infer as a dep on251   * just `props`. With this flag enabled, we'll infer that full path as252   * the dependency.253   */254  enableOptionalDependencies: z.boolean().default(true),255256  enableNameAnonymousFunctions: z.boolean().default(false),257258  /*259   * Enable validation of hooks to partially check that the component honors the rules of hooks.260   * When disabled, the component is assumed to follow the rules (though the Babel plugin looks261   * for suppressions of the lint rule).262   */263  validateHooksUsage: z.boolean().default(true),264265  // Validate that ref values (`ref.current`) are not accessed during render.266  validateRefAccessDuringRender: z.boolean().default(true),267268  /*269   * Validates that setState is not unconditionally called during render, as it can lead to270   * infinite loops.271   */272  validateNoSetStateInRender: z.boolean().default(true),273274  /**275   * When enabled, changes the behavior of validateNoSetStateInRender to recommend276   * using useKeyedState instead of the manual pattern for resetting state.277   */278  enableUseKeyedState: z.boolean().default(false),279280  /**281   * Validates that setState is not called synchronously within an effect (useEffect and friends).282   * Scheduling a setState (with an event listener, subscription, etc) is valid.283   */284  validateNoSetStateInEffects: z.boolean().default(false),285286  /**287   * Validates that effects are not used to calculate derived data which could instead be computed288   * during render.289   */290  validateNoDerivedComputationsInEffects: z.boolean().default(false),291292  /**293   * Experimental: Validates that effects are not used to calculate derived data which could instead be computed294   * during render. Generates a custom error message for each type of violation.295   */296  validateNoDerivedComputationsInEffects_exp: z.boolean().default(false),297298  /**299   * Validates against creating JSX within a try block and recommends using an error boundary300   * instead.301   */302  validateNoJSXInTryStatements: z.boolean().default(false),303304  /**305   * Validates against dynamically creating components during render.306   */307  validateStaticComponents: z.boolean().default(false),308309  /**310   * Validates that there are no capitalized calls other than those allowed by the allowlist.311   * Calls to capitalized functions are often functions that used to be components and may312   * have lingering hook calls, which makes those calls risky to memoize.313   *314   * You can specify a list of capitalized calls to allowlist using this option. React Compiler315   * always includes its known global functions, including common functions like Boolean and String,316   * in this allowlist. You can enable this validation with no additional allowlisted calls by setting317   * this option to the empty array.318   */319  validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),320  validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),321322  /**323   * Validates that AST nodes generated during codegen have proper source locations.324   * This is useful for debugging issues with source maps and Istanbul coverage.325   * When enabled, the compiler will error if important source locations are missing in the generated AST.326   */327  validateSourceLocations: z.boolean().default(false),328329  /**330   * Validate against impure functions called during render331   */332  validateNoImpureFunctionsInRender: z.boolean().default(false),333334  /**335   * Validate against passing mutable functions to hooks336   */337  validateNoFreezingKnownMutableFunctions: z.boolean().default(false),338339  /*340   * When enabled, the compiler assumes that hooks follow the Rules of React:341   * - Hooks may memoize computation based on any of their parameters, thus342   *   any arguments to a hook are assumed frozen after calling the hook.343   * - Hooks may memoize the result they return, thus the return value is344   *   assumed frozen.345   */346  enableAssumeHooksFollowRulesOfReact: z.boolean().default(true),347348  /**349   * When enabled, the compiler assumes that any values are not subsequently350   * modified after they are captured by a function passed to React. For example,351   * if a value `x` is referenced inside a function expression passed to `useEffect`,352   * then this flag will assume that `x` is not subusequently modified.353   */354  enableTransitivelyFreezeFunctionExpressions: z.boolean().default(true),355  enableEmitHookGuards: ExternalFunctionSchema.nullable().default(null),356357  /**358   * Enables function outlinining, where anonymous functions that do not close over359   * local variables can be extracted into top-level helper functions.360   */361  enableFunctionOutlining: z.boolean().default(true),362363  /**364   * If enabled, this will outline nested JSX into a separate component.365   *366   * This will enable the compiler to memoize the separate component, giving us367   * the same behavior as compiling _within_ the callback.368   *369   * ```370   * function Component(countries, onDelete) {371   *   const name = useFoo();372   *   return countries.map(() => {373   *     return (374   *       <Foo>375   *         <Bar>{name}</Bar>376   *         <Button onclick={onDelete}>delete</Button>377   *       </Foo>378   *     );379   *   });380   * }381   * ```382   *383   * will be transpiled to:384   *385   * ```386   * function Component(countries, onDelete) {387   *   const name = useFoo();388   *   return countries.map(() => {389   *     return (390   *       <Temp name={name} onDelete={onDelete} />391   *     );392   *   });393   * }394   *395   * function Temp({name, onDelete}) {396   *   return (397   *     <Foo>398   *       <Bar>{name}</Bar>399   *       <Button onclick={onDelete}>delete</Button>400   *     </Foo>401   *   );402   * }403   *404   * Both, `Component` and `Temp` will then be memoized by the compiler.405   *406   * With this change, when `countries` is updated by adding one single value,407   * only the newly added value is re-rendered and not the entire list.408   */409  enableJsxOutlining: z.boolean().default(false),410411  /*412   * Enables instrumentation codegen. This emits a dev-mode only call to an413   * instrumentation function, for components and hooks that Forget compiles.414   * For example:415   *   instrumentForget: {416   *     import: {417   *       source: 'react-compiler-runtime',418   *       importSpecifierName: 'useRenderCounter',419   *      }420   *   }421   *422   * produces:423   *   import {useRenderCounter} from 'react-compiler-runtime';424   *425   *   function Component(props) {426   *     if (__DEV__) {427   *        useRenderCounter("Component", "/filepath/filename.js");428   *     }429   *     // ...430   *   }431   *432   */433  enableEmitInstrumentForget: InstrumentationSchema.nullable().default(null),434435  // Enable validation of mutable ranges436  assertValidMutableRanges: z.boolean().default(false),437438  /**439   * [TESTING ONLY] Throw an unknown exception during compilation to440   * simulate unexpected exceptions e.g. errors from babel functions.441   */442  throwUnknownException__testonly: z.boolean().default(false),443444  /**445   * The react native re-animated library uses custom Babel transforms that446   * requires the calls to library API remain unmodified.447   *448   * If this flag is turned on, the React compiler will use custom type449   * definitions for reanimated library to make it's Babel plugin work450   * with the compiler.451   */452  enableCustomTypeDefinitionForReanimated: z.boolean().default(false),453454  /**455   * If enabled, this will treat objects named as `ref` or if their names end with the substring `Ref`,456   * and contain a property named `current`, as React refs.457   *458   * ```459   * const ref = useMyRef();460   * const myRef = useMyRef2();461   * useEffect(() => {462   *   ref.current = ...;463   *   myRef.current = ...;464   * })465   * ```466   *467   * Here the variables `ref` and `myRef` will be typed as Refs.468   */469  enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true),470471  /**472   * Treat identifiers as SetState type if both473   * - they are named with a "set-" prefix474   * - they are called somewhere475   */476  enableTreatSetIdentifiersAsStateSetters: z.boolean().default(false),477478  /**479   * If enabled, will validate useMemos that don't return any values:480   *481   * Valid:482   *   useMemo(() => foo, [foo]);483   *   useMemo(() => { return foo }, [foo]);484   * Invalid:485   *   useMemo(() => { ... }, [...]);486   */487  validateNoVoidUseMemo: z.boolean().default(true),488489  /**490   * When enabled, allows setState calls in effects based on valid patterns involving refs:491   * - Allow setState where the value being set is derived from a ref. This is useful where492   *   state needs to take into account layer information, and a layout effect reads layout493   *   data from a ref and sets state.494   * - Allow conditionally calling setState after manually comparing previous/new values495   *   for changes via a ref. Relying on effect deps is insufficient for non-primitive values,496   *   so a ref is generally required to manually track previous values and compare prev/next497   *   for meaningful changes before setting state.498   */499  enableAllowSetStateFromRefsInEffects: z.boolean().default(true),500501  /**502   * When enabled, provides verbose error messages for setState calls within effects,503   * presenting multiple possible fixes to the user/agent since we cannot statically504   * determine which specific use-case applies:505   * 1. Non-local derived data - requires restructuring state ownership506   * 2. Derived event pattern - detecting when a prop changes507   * 3. Force update / external sync - should use useSyncExternalStore508   */509  enableVerboseNoSetStateInEffect: z.boolean().default(false),510});511512export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;513514export type PartialEnvironmentConfig = Partial<EnvironmentConfig>;515516export type ReactFunctionType = 'Component' | 'Hook' | 'Other';517518export function printFunctionType(type: ReactFunctionType): string {519  switch (type) {520    case 'Component': {521      return 'component';522    }523    case 'Hook': {524      return 'hook';525    }526    default: {527      return 'function';528    }529  }530}531532export class Environment {533  #globals: GlobalRegistry;534  #shapes: ShapeRegistry;535  #moduleTypes: Map<string, Global | null> = new Map();536  #nextIdentifer: number = 0;537  #nextBlock: number = 0;538  #nextScope: number = 0;539  #scope: BabelScope;540  #outlinedFunctions: Array<{541    fn: HIRFunction;542    type: ReactFunctionType | null;543  }> = [];544  logger: Logger | null;545  filename: string | null;546  code: string | null;547  config: EnvironmentConfig;548  fnType: ReactFunctionType;549  outputMode: CompilerOutputMode;550  programContext: ProgramContext;551552  #contextIdentifiers: Set<t.Identifier>;553  #hoistedIdentifiers: Set<t.Identifier>;554  parentFunction: NodePath<t.Function>;555556  #flowTypeEnvironment: FlowTypeEnv | null;557558  /**559   * Accumulated compilation errors. Passes record errors here instead of560   * throwing, so the pipeline can continue and report all errors at once.561   */562  #errors: CompilerError = new CompilerError();563564  constructor(565    scope: BabelScope,566    fnType: ReactFunctionType,567    outputMode: CompilerOutputMode,568    config: EnvironmentConfig,569    contextIdentifiers: Set<t.Identifier>,570    parentFunction: NodePath<t.Function>, // the outermost function being compiled571    logger: Logger | null,572    filename: string | null,573    code: string | null,574    programContext: ProgramContext,575  ) {576    this.#scope = scope;577    this.fnType = fnType;578    this.outputMode = outputMode;579    this.config = config;580    this.filename = filename;581    this.code = code;582    this.logger = logger;583    this.programContext = programContext;584    this.#shapes = new Map(DEFAULT_SHAPES);585    this.#globals = new Map(DEFAULT_GLOBALS);586587    for (const [hookName, hook] of this.config.customHooks) {588      CompilerError.invariant(!this.#globals.has(hookName), {589        reason: `[Globals] Found existing definition in global registry for custom hook ${hookName}`,590        loc: GeneratedSource,591      });592      this.#globals.set(593        hookName,594        addHook(this.#shapes, {595          positionalParams: [],596          restParam: hook.effectKind,597          returnType: hook.transitiveMixedData598            ? {kind: 'Object', shapeId: BuiltInMixedReadonlyId}599            : {kind: 'Poly'},600          returnValueKind: hook.valueKind,601          calleeEffect: Effect.Read,602          hookKind: 'Custom',603          noAlias: hook.noAlias,604        }),605      );606    }607608    if (config.enableCustomTypeDefinitionForReanimated) {609      const reanimatedModuleType = getReanimatedModuleType(this.#shapes);610      this.#moduleTypes.set(REANIMATED_MODULE_NAME, reanimatedModuleType);611    }612613    this.parentFunction = parentFunction;614    this.#contextIdentifiers = contextIdentifiers;615    this.#hoistedIdentifiers = new Set();616617    if (config.flowTypeProvider != null) {618      this.#flowTypeEnvironment = new FlowTypeEnv();619      CompilerError.invariant(code != null, {620        reason:621          'Expected Environment to be initialized with source code when a Flow type provider is specified',622        loc: GeneratedSource,623      });624      this.#flowTypeEnvironment.init(this, code);625    } else {626      this.#flowTypeEnvironment = null;627    }628  }629630  get typeContext(): FlowTypeEnv {631    CompilerError.invariant(this.#flowTypeEnvironment != null, {632      reason: 'Flow type environment not initialized',633      loc: GeneratedSource,634    });635    return this.#flowTypeEnvironment;636  }637638  get enableDropManualMemoization(): boolean {639    switch (this.outputMode) {640      case 'lint': {641        // linting drops to be more compatible with compiler analysis642        return true;643      }644      case 'client':645      case 'ssr': {646        return true;647      }648      default: {649        assertExhaustive(650          this.outputMode,651          `Unexpected output mode '${this.outputMode}'`,652        );653      }654    }655  }656657  get enableMemoization(): boolean {658    switch (this.outputMode) {659      case 'client':660      case 'lint': {661        // linting also enables memoization so that we can check if manual memoization is preserved662        return true;663      }664      case 'ssr': {665        return false;666      }667      default: {668        assertExhaustive(669          this.outputMode,670          `Unexpected output mode '${this.outputMode}'`,671        );672      }673    }674  }675676  get enableValidations(): boolean {677    switch (this.outputMode) {678      case 'client':679      case 'lint':680      case 'ssr': {681        return true;682      }683      default: {684        assertExhaustive(685          this.outputMode,686          `Unexpected output mode '${this.outputMode}'`,687        );688      }689    }690  }691692  get nextIdentifierId(): IdentifierId {693    return makeIdentifierId(this.#nextIdentifer++);694  }695696  get nextBlockId(): BlockId {697    return makeBlockId(this.#nextBlock++);698  }699700  get nextScopeId(): ScopeId {701    return makeScopeId(this.#nextScope++);702  }703704  get scope(): BabelScope {705    return this.#scope;706  }707708  logErrors(errors: Result<void, CompilerError>): void {709    if (errors.isOk() || this.logger == null) {710      return;711    }712    for (const error of errors.unwrapErr().details) {713      this.logger.logEvent(this.filename, {714        kind: 'CompileError',715        detail: formatDetailForLogging(error),716        fnLoc: null,717      });718    }719  }720721  /**722   * Record a single diagnostic or error detail on this environment.723   * If the error is an Invariant, it is immediately thrown since invariants724   * represent internal bugs that cannot be recovered from.725   * Otherwise, the error is accumulated and optionally logged.726   */727  recordError(error: CompilerDiagnostic | CompilerErrorDetail): void {728    if (error.category === ErrorCategory.Invariant) {729      const compilerError = new CompilerError();730      if (error instanceof CompilerDiagnostic) {731        compilerError.pushDiagnostic(error);732      } else {733        compilerError.pushErrorDetail(error);734      }735      throw compilerError;736    }737    if (error instanceof CompilerDiagnostic) {738      this.#errors.pushDiagnostic(error);739    } else {740      this.#errors.pushErrorDetail(error);741    }742  }743744  /**745   * Record all diagnostics from a CompilerError onto this environment.746   */747  recordErrors(error: CompilerError): void {748    for (const detail of error.details) {749      this.recordError(detail);750    }751  }752753  /**754   * Returns true if any errors have been recorded during compilation.755   */756  hasErrors(): boolean {757    return this.#errors.hasAnyErrors();758  }759760  /**761   * Returns the accumulated CompilerError containing all recorded diagnostics.762   */763  aggregateErrors(): CompilerError {764    return this.#errors;765  }766767  isContextIdentifier(node: t.Identifier): boolean {768    return this.#contextIdentifiers.has(node);769  }770771  isHoistedIdentifier(node: t.Identifier): boolean {772    return this.#hoistedIdentifiers.has(node);773  }774775  generateGloballyUniqueIdentifierName(776    name: string | null,777  ): ValidatedIdentifier {778    const identifierNode = this.#scope.generateUidIdentifier(name ?? undefined);779    return makeIdentifierName(identifierNode.name);780  }781782  outlineFunction(fn: HIRFunction, type: ReactFunctionType | null): void {783    this.#outlinedFunctions.push({fn, type});784  }785786  getOutlinedFunctions(): Array<{787    fn: HIRFunction;788    type: ReactFunctionType | null;789  }> {790    return this.#outlinedFunctions;791  }792793  #resolveModuleType(moduleName: string, loc: SourceLocation): Global | null {794    let moduleType = this.#moduleTypes.get(moduleName);795    if (moduleType === undefined) {796      /*797       * NOTE: Zod doesn't work when specifying a function as a default, so we have to798       * fallback to the default value here799       */800      const moduleTypeProvider =801        this.config.moduleTypeProvider ?? defaultModuleTypeProvider;802      if (moduleTypeProvider == null) {803        return null;804      }805      if (typeof moduleTypeProvider !== 'function') {806        CompilerError.throwInvalidConfig({807          reason: `Expected a function for \`moduleTypeProvider\``,808          loc,809        });810      }811      const unparsedModuleConfig = moduleTypeProvider(moduleName);812      if (unparsedModuleConfig != null) {813        const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);814        if (!parsedModuleConfig.success) {815          CompilerError.throwInvalidConfig({816            reason: `Could not parse module type, the configured \`moduleTypeProvider\` function returned an invalid module description`,817            description: parsedModuleConfig.error.toString(),818            loc,819          });820        }821        const moduleConfig = parsedModuleConfig.data;822        moduleType = installTypeConfig(823          this.#globals,824          this.#shapes,825          moduleConfig,826          moduleName,827          loc,828        );829      } else {830        moduleType = null;831      }832      this.#moduleTypes.set(moduleName, moduleType);833    }834    return moduleType;835  }836837  getGlobalDeclaration(838    binding: NonLocalBinding,839    loc: SourceLocation,840  ): Global | null {841    switch (binding.kind) {842      case 'ModuleLocal': {843        // don't resolve module locals844        return isHookName(binding.name) ? this.#getCustomHookType() : null;845      }846      case 'Global': {847        return (848          this.#globals.get(binding.name) ??849          (isHookName(binding.name) ? this.#getCustomHookType() : null)850        );851      }852      case 'ImportSpecifier': {853        if (this.#isKnownReactModule(binding.module)) {854          /**855           * For `import {imported as name} from "..."` form, we use the `imported`856           * name rather than the local alias. Because we don't have definitions for857           * every React builtin hook yet, we also check to see if the imported name858           * is hook-like (whereas the fall-through below is checking if the aliased859           * name is hook-like)860           */861          return (862            this.#globals.get(binding.imported) ??863            (isHookName(binding.imported) || isHookName(binding.name)864              ? this.#getCustomHookType()865              : null)866          );867        } else {868          const moduleType = this.#resolveModuleType(binding.module, loc);869          if (moduleType !== null) {870            const importedType = this.getPropertyType(871              moduleType,872              binding.imported,873            );874            if (importedType != null) {875              /*876               * Check that hook-like export names are hook types, and non-hook names are non-hook types.877               * The user-assigned alias isn't decidable by the type provider, so we ignore that for the check.878               * Thus we allow `import {fooNonHook as useFoo} from ...` because the name and type both say879               * that it's not a hook.880               */881              const expectHook = isHookName(binding.imported);882              const isHook = getHookKindForType(this, importedType) != null;883              if (expectHook !== isHook) {884                CompilerError.throwInvalidConfig({885                  reason: `Invalid type configuration for module`,886                  description: `Expected type for \`import {${binding.imported}} from '${binding.module}'\` ${expectHook ? 'to be a hook' : 'not to be a hook'} based on the exported name`,887                  loc,888                });889              }890              return importedType;891            }892          }893894          /**895           * For modules we don't own, we look at whether the original name or import alias896           * are hook-like. Both of the following are likely hooks so we would return a hook897           * type for both:898           *899           * `import {useHook as foo} ...`900           * `import {foo as useHook} ...`901           */902          return isHookName(binding.imported) || isHookName(binding.name)903            ? this.#getCustomHookType()904            : null;905        }906      }907      case 'ImportDefault':908      case 'ImportNamespace': {909        if (this.#isKnownReactModule(binding.module)) {910          // only resolve imports to modules we know about911          return (912            this.#globals.get(binding.name) ??913            (isHookName(binding.name) ? this.#getCustomHookType() : null)914          );915        } else {916          const moduleType = this.#resolveModuleType(binding.module, loc);917          if (moduleType !== null) {918            let importedType: Type | null = null;919            if (binding.kind === 'ImportDefault') {920              const defaultType = this.getPropertyType(moduleType, 'default');921              if (defaultType !== null) {922                importedType = defaultType;923              }924            } else {925              importedType = moduleType;926            }927            if (importedType !== null) {928              /*929               * Check that the hook-like modules are defined as types, and non hook-like modules are not typed as hooks.930               * So `import Foo from 'useFoo'` is expected to be a hook based on the module name931               */932              const expectHook = isHookName(binding.module);933              const isHook = getHookKindForType(this, importedType) != null;934              if (expectHook !== isHook) {935                CompilerError.throwInvalidConfig({936                  reason: `Invalid type configuration for module`,937                  description: `Expected type for \`import ... from '${binding.module}'\` ${expectHook ? 'to be a hook' : 'not to be a hook'} based on the module name`,938                  loc,939                });940              }941              return importedType;942            }943          }944          return isHookName(binding.name) ? this.#getCustomHookType() : null;945        }946      }947    }948  }949950  #isKnownReactModule(moduleName: string): boolean {951    return (952      moduleName.toLowerCase() === 'react' ||953      moduleName.toLowerCase() === 'react-dom'954    );955  }956  static knownReactModules: ReadonlyArray<string> = ['react', 'react-dom'];957958  getFallthroughPropertyType(959    receiver: Type,960    _property: Type,961  ): BuiltInType | PolyType | null {962    let shapeId = null;963    if (receiver.kind === 'Object' || receiver.kind === 'Function') {964      shapeId = receiver.shapeId;965    }966967    if (shapeId !== null) {968      const shape = this.#shapes.get(shapeId);969970      CompilerError.invariant(shape !== undefined, {971        reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,972        loc: GeneratedSource,973      });974      return shape.properties.get('*') ?? null;975    }976    return null;977  }978979  getPropertyType(980    receiver: Type,981    property: string | number,982  ): BuiltInType | PolyType | null {983    let shapeId = null;984    if (receiver.kind === 'Object' || receiver.kind === 'Function') {985      shapeId = receiver.shapeId;986    }987    if (shapeId !== null) {988      /*989       * If an object or function has a shapeId, it must have been assigned990       * by Forget (and be present in a builtin or user-defined registry)991       */992      const shape = this.#shapes.get(shapeId);993      CompilerError.invariant(shape !== undefined, {994        reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,995        loc: GeneratedSource,996      });997      if (typeof property === 'string') {998        return (999          shape.properties.get(property) ??1000          shape.properties.get('*') ??1001          (isHookName(property) ? this.#getCustomHookType() : null)1002        );1003      } else {1004        return shape.properties.get('*') ?? null;1005      }1006    } else if (typeof property === 'string' && isHookName(property)) {1007      return this.#getCustomHookType();1008    }1009    return null;1010  }10111012  getFunctionSignature(type: FunctionType): FunctionSignature | null {1013    const {shapeId} = type;1014    if (shapeId !== null) {1015      const shape = this.#shapes.get(shapeId);1016      CompilerError.invariant(shape !== undefined, {1017        reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,1018        loc: GeneratedSource,1019      });1020      return shape.functionType;1021    }1022    return null;1023  }10241025  addHoistedIdentifier(node: t.Identifier): void {1026    this.#contextIdentifiers.add(node);1027    this.#hoistedIdentifiers.add(node);1028  }10291030  #getCustomHookType(): Global {1031    if (this.config.enableAssumeHooksFollowRulesOfReact) {1032      return DefaultNonmutatingHook;1033    } else {1034      return DefaultMutatingHook;1035    }1036  }1037}10381039const REANIMATED_MODULE_NAME = 'react-native-reanimated';10401041// From https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#LL18C1-L23C21042export function isHookName(name: string): boolean {1043  return /^use[A-Z0-9]/.test(name);1044}10451046export function parseEnvironmentConfig(1047  partialConfig: PartialEnvironmentConfig,1048): Result<EnvironmentConfig, ZodError<PartialEnvironmentConfig>> {1049  const config = EnvironmentConfigSchema.safeParse(partialConfig);1050  if (config.success) {1051    return Ok(config.data);1052  } else {1053    return Err(config.error);1054  }1055}10561057export function validateEnvironmentConfig(1058  partialConfig: PartialEnvironmentConfig,1059): EnvironmentConfig {1060  const config = EnvironmentConfigSchema.safeParse(partialConfig);1061  if (config.success) {1062    return config.data;1063  }10641065  CompilerError.throwInvalidConfig({1066    reason:1067      'Could not validate environment config. Update React Compiler config to fix the error',1068    description: `${fromZodError(config.error)}`,1069    loc: null,1070    suggestions: null,1071  });1072}10731074export function tryParseExternalFunction(1075  maybeExternalFunction: any,1076): ExternalFunction {1077  const externalFunction = ExternalFunctionSchema.safeParse(1078    maybeExternalFunction,1079  );1080  if (externalFunction.success) {1081    return externalFunction.data;1082  }10831084  CompilerError.throwInvalidConfig({1085    reason:1086      'Could not parse external function. Update React Compiler config to fix the error',1087    description: `${fromZodError(externalFunction.error)}`,1088    loc: null,1089    suggestions: null,1090  });1091}10921093export const DEFAULT_EXPORT = 'default';

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.