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.