main
ts 1,093 lines 36.1 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import * as t from '@babel/types';
9 import {ZodError, z} from 'zod/v4';
10 import {fromZodError} from 'zod-validation-error/v4';
11 import {
12 CompilerDiagnostic,
13 CompilerError,
14 CompilerErrorDetail,
15 ErrorCategory,
16 } from '../CompilerError';
17 import {
18 CompilerOutputMode,
19 Logger,
20 ProgramContext,
21 formatDetailForLogging,
22 } from '../Entrypoint';
23 import {Err, Ok, Result} from '../Utils/Result';
24 import {
25 DEFAULT_GLOBALS,
26 DEFAULT_SHAPES,
27 Global,
28 GlobalRegistry,
29 getReanimatedModuleType,
30 installTypeConfig,
31 } from './Globals';
32 import {
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';
53 import {
54 BuiltInMixedReadonlyId,
55 DefaultMutatingHook,
56 DefaultNonmutatingHook,
57 FunctionSignature,
58 ShapeRegistry,
59 addHook,
60 } from './ObjectShape';
61 import {Scope as BabelScope, NodePath} from '@babel/traverse';
62 import {TypeSchema} from './TypeSchema';
63 import {FlowTypeEnv} from '../Flood/Types';
64 import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
65 import {assertExhaustive} from '../Utils/utils';
66
67 export const ExternalFunctionSchema = z.object({
68 // Source for the imported module that exports the `importSpecifierName` functions
69 source: z.string(),
70
71 // Unique name for the feature flag test condition, eg `isForgetEnabled_ProjectName`
72 importSpecifierName: z.string(),
73 });
74
75 export const InstrumentationSchema = z
76 .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 );
85
86 export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
87
88 export const MacroSchema = z.string();
89
90 export type CompilerMode = 'all_features' | 'no_inferred_memo';
91
92 export type Macro = z.infer<typeof MacroSchema>;
93
94 const HookSchema = z.object({
95 /*
96 * The effect of arguments to this hook. Describes whether the hook may or may
97 * not mutate arguments, etc.
98 */
99 effectKind: z.nativeEnum(Effect),
100
101 /*
102 * The kind of value returned by the hook. Allows indicating that a hook returns
103 * a primitive or already-frozen value, which can allow more precise memoization
104 * of callers.
105 */
106 valueKind: z.nativeEnum(ValueKind),
107
108 /*
109 * Specifies whether hook arguments may be aliased by other arguments or by the
110 * return value of the function. Defaults to false. When enabled, this allows the
111 * compiler to avoid memoizing arguments.
112 */
113 noAlias: z.boolean().default(false),
114
115 /*
116 * Specifies whether the hook returns data that is composed of:
117 * - undefined
118 * - null
119 * - boolean
120 * - number
121 * - string
122 * - arrays whose items are also transitiveMixed
123 * - objects whose values are also transitiveMixed
124 *
125 * Many state management and data-fetching APIs return data that meets
126 * this criteria since this is JSON + undefined. Forget can compile
127 * hooks that return transitively mixed data more optimally because it
128 * can make inferences about some method calls (especially array methods
129 * like `data.items.map(...)` since these builtin types have few built-in
130 * methods.
131 */
132 transitiveMixedData: z.boolean().default(false),
133 });
134
135 export type Hook = z.infer<typeof HookSchema>;
136
137 /*
138 * TODO(mofeiZ): User defined global types (with corresponding shapes).
139 * User defined global types should have inline ObjectShapes instead of directly
140 * using ObjectShapes.ShapeRegistry, as a user-provided ShapeRegistry may be
141 * accidentally be not well formed.
142 * i.e.
143 * missing required shapes (BuiltInArray for [] and BuiltInObject for {})
144 * missing some recursive Object / Function shapeIds
145 */
146
147 export const EnvironmentConfigSchema = z.object({
148 customHooks: z.map(z.string(), HookSchema).default(new Map()),
149
150 /**
151 * A function that, given the name of a module, can optionally return a description
152 * of that module's type signature.
153 */
154 moduleTypeProvider: z.nullable(z.any()).default(null),
155
156 /**
157 * A list of functions which the application compiles as macros, where
158 * the compiler must ensure they are not compiled to rename the macro or separate the
159 * "function" from its argument.
160 *
161 * For example, Meta has some APIs such as `featureflag("name-of-feature-flag")` which
162 * are rewritten by a plugin. Assigning `featureflag` to a temporary would break the
163 * plugin since it looks specifically for the name of the function being invoked, not
164 * following aliases.
165 */
166 customMacros: z.nullable(z.array(MacroSchema)).default(null),
167
168 /**
169 * Enable a check that resets the memoization cache when the source code of
170 * the file changes. This is intended to support hot module reloading (HMR),
171 * where the same runtime component instance will be reused across different
172 * versions of the component source.
173 *
174 * When set to
175 * - true: code for HMR support is always generated, regardless of NODE_ENV
176 * or `globalThis.__DEV__`
177 * - false: code for HMR support is not generated
178 * - null: (default) code for HMR support is conditionally generated dependent
179 * on `NODE_ENV` and `globalThis.__DEV__` at the time of compilation.
180 */
181 enableResetCacheOnSourceFileChanges: z.nullable(z.boolean()).default(null),
182
183 /**
184 * Enable using information from existing useMemo/useCallback to understand when a value is done
185 * being mutated. With this mode enabled, Forget will still discard the actual useMemo/useCallback
186 * calls and may memoize slightly differently. However, it will assume that the values produced
187 * 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 existing
190 * behavior that depends on referential equality in the original program. Notably, this preserves
191 * 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 ignore
194 * them, not using any information from them to guide compilation. Therefore, disabling this flag
195 * 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 attempt
198 * to disable this flag and see what changes or breaks. This will mostly likely be effects that
199 * 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 means
202 * that if a useEffect or useCallback references a function value, that function value will be
203 * considered frozen, and in turn all of its referenced variables will be considered frozen as well.
204 */
205 enablePreserveExistingMemoizationGuarantees: z.boolean().default(true),
206
207 /**
208 * Validates that all useMemo/useCallback values are also memoized by Forget. This mode can be
209 * used with or without @enablePreserveExistingMemoizationGuarantees.
210 *
211 * With enablePreserveExistingMemoizationGuarantees, this validation enables automatically and
212 * verifies that Forget was able to preserve manual memoization semantics under that mode's
213 * additional assumptions about the input.
214 *
215 * With enablePreserveExistingMemoizationGuarantees off, this validation ignores manual memoization
216 * when determining program behavior, and only uses information from useMemo/useCallback to check
217 * that the memoization was preserved. This can be useful for determining where referential equalities
218 * may change under Forget.
219 */
220 validatePreserveExistingMemoizationGuarantees: z.boolean().default(true),
221
222 /**
223 * Validate that dependencies supplied to manual memoization calls are exhaustive.
224 */
225 validateExhaustiveMemoizationDependencies: z.boolean().default(true),
226
227 /**
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 dependencies
232 * - 'missing-only': Only report missing dependencies
233 * - 'extra-only': Only report extra/unnecessary dependencies
234 */
235 validateExhaustiveEffectDependencies: z
236 .enum(['off', 'all', 'missing-only', 'extra-only'])
237 .default('off'),
238
239 // 🌲
240 enableForest: z.boolean().default(false),
241
242 /**
243 * Allows specifying a function that can populate HIR with type information from
244 * Flow
245 */
246 flowTypeProvider: z.nullable(z.any()).default(null),
247
248 /**
249 * Enables inference of optional dependency chains. Without this flag
250 * a property chain such as `props?.items?.foo` will infer as a dep on
251 * just `props`. With this flag enabled, we'll infer that full path as
252 * the dependency.
253 */
254 enableOptionalDependencies: z.boolean().default(true),
255
256 enableNameAnonymousFunctions: z.boolean().default(false),
257
258 /*
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 looks
261 * for suppressions of the lint rule).
262 */
263 validateHooksUsage: z.boolean().default(true),
264
265 // Validate that ref values (`ref.current`) are not accessed during render.
266 validateRefAccessDuringRender: z.boolean().default(true),
267
268 /*
269 * Validates that setState is not unconditionally called during render, as it can lead to
270 * infinite loops.
271 */
272 validateNoSetStateInRender: z.boolean().default(true),
273
274 /**
275 * When enabled, changes the behavior of validateNoSetStateInRender to recommend
276 * using useKeyedState instead of the manual pattern for resetting state.
277 */
278 enableUseKeyedState: z.boolean().default(false),
279
280 /**
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),
285
286 /**
287 * Validates that effects are not used to calculate derived data which could instead be computed
288 * during render.
289 */
290 validateNoDerivedComputationsInEffects: z.boolean().default(false),
291
292 /**
293 * Experimental: Validates that effects are not used to calculate derived data which could instead be computed
294 * during render. Generates a custom error message for each type of violation.
295 */
296 validateNoDerivedComputationsInEffects_exp: z.boolean().default(false),
297
298 /**
299 * Validates against creating JSX within a try block and recommends using an error boundary
300 * instead.
301 */
302 validateNoJSXInTryStatements: z.boolean().default(false),
303
304 /**
305 * Validates against dynamically creating components during render.
306 */
307 validateStaticComponents: z.boolean().default(false),
308
309 /**
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 may
312 * 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 Compiler
315 * 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 setting
317 * 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),
321
322 /**
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),
328
329 /**
330 * Validate against impure functions called during render
331 */
332 validateNoImpureFunctionsInRender: z.boolean().default(false),
333
334 /**
335 * Validate against passing mutable functions to hooks
336 */
337 validateNoFreezingKnownMutableFunctions: z.boolean().default(false),
338
339 /*
340 * When enabled, the compiler assumes that hooks follow the Rules of React:
341 * - Hooks may memoize computation based on any of their parameters, thus
342 * any arguments to a hook are assumed frozen after calling the hook.
343 * - Hooks may memoize the result they return, thus the return value is
344 * assumed frozen.
345 */
346 enableAssumeHooksFollowRulesOfReact: z.boolean().default(true),
347
348 /**
349 * When enabled, the compiler assumes that any values are not subsequently
350 * 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),
356
357 /**
358 * Enables function outlinining, where anonymous functions that do not close over
359 * local variables can be extracted into top-level helper functions.
360 */
361 enableFunctionOutlining: z.boolean().default(true),
362
363 /**
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 us
367 * 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),
410
411 /*
412 * Enables instrumentation codegen. This emits a dev-mode only call to an
413 * 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),
434
435 // Enable validation of mutable ranges
436 assertValidMutableRanges: z.boolean().default(false),
437
438 /**
439 * [TESTING ONLY] Throw an unknown exception during compilation to
440 * simulate unexpected exceptions e.g. errors from babel functions.
441 */
442 throwUnknownException__testonly: z.boolean().default(false),
443
444 /**
445 * The react native re-animated library uses custom Babel transforms that
446 * requires the calls to library API remain unmodified.
447 *
448 * If this flag is turned on, the React compiler will use custom type
449 * definitions for reanimated library to make it's Babel plugin work
450 * with the compiler.
451 */
452 enableCustomTypeDefinitionForReanimated: z.boolean().default(false),
453
454 /**
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),
470
471 /**
472 * Treat identifiers as SetState type if both
473 * - they are named with a "set-" prefix
474 * - they are called somewhere
475 */
476 enableTreatSetIdentifiersAsStateSetters: z.boolean().default(false),
477
478 /**
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),
488
489 /**
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 where
492 * state needs to take into account layer information, and a layout effect reads layout
493 * data from a ref and sets state.
494 * - Allow conditionally calling setState after manually comparing previous/new values
495 * 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/next
497 * for meaningful changes before setting state.
498 */
499 enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
500
501 /**
502 * When enabled, provides verbose error messages for setState calls within effects,
503 * presenting multiple possible fixes to the user/agent since we cannot statically
504 * determine which specific use-case applies:
505 * 1. Non-local derived data - requires restructuring state ownership
506 * 2. Derived event pattern - detecting when a prop changes
507 * 3. Force update / external sync - should use useSyncExternalStore
508 */
509 enableVerboseNoSetStateInEffect: z.boolean().default(false),
510 });
511
512 export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
513
514 export type PartialEnvironmentConfig = Partial<EnvironmentConfig>;
515
516 export type ReactFunctionType = 'Component' | 'Hook' | 'Other';
517
518 export 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 }
531
532 export 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;
551
552 #contextIdentifiers: Set<t.Identifier>;
553 #hoistedIdentifiers: Set<t.Identifier>;
554 parentFunction: NodePath<t.Function>;
555
556 #flowTypeEnvironment: FlowTypeEnv | null;
557
558 /**
559 * Accumulated compilation errors. Passes record errors here instead of
560 * throwing, so the pipeline can continue and report all errors at once.
561 */
562 #errors: CompilerError = new CompilerError();
563
564 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 compiled
571 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);
586
587 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.transitiveMixedData
598 ? {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 }
607
608 if (config.enableCustomTypeDefinitionForReanimated) {
609 const reanimatedModuleType = getReanimatedModuleType(this.#shapes);
610 this.#moduleTypes.set(REANIMATED_MODULE_NAME, reanimatedModuleType);
611 }
612
613 this.parentFunction = parentFunction;
614 this.#contextIdentifiers = contextIdentifiers;
615 this.#hoistedIdentifiers = new Set();
616
617 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 }
629
630 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 }
637
638 get enableDropManualMemoization(): boolean {
639 switch (this.outputMode) {
640 case 'lint': {
641 // linting drops to be more compatible with compiler analysis
642 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 }
656
657 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 preserved
662 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 }
675
676 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 }
691
692 get nextIdentifierId(): IdentifierId {
693 return makeIdentifierId(this.#nextIdentifer++);
694 }
695
696 get nextBlockId(): BlockId {
697 return makeBlockId(this.#nextBlock++);
698 }
699
700 get nextScopeId(): ScopeId {
701 return makeScopeId(this.#nextScope++);
702 }
703
704 get scope(): BabelScope {
705 return this.#scope;
706 }
707
708 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 }
720
721 /**
722 * Record a single diagnostic or error detail on this environment.
723 * If the error is an Invariant, it is immediately thrown since invariants
724 * 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 }
743
744 /**
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 }
752
753 /**
754 * Returns true if any errors have been recorded during compilation.
755 */
756 hasErrors(): boolean {
757 return this.#errors.hasAnyErrors();
758 }
759
760 /**
761 * Returns the accumulated CompilerError containing all recorded diagnostics.
762 */
763 aggregateErrors(): CompilerError {
764 return this.#errors;
765 }
766
767 isContextIdentifier(node: t.Identifier): boolean {
768 return this.#contextIdentifiers.has(node);
769 }
770
771 isHoistedIdentifier(node: t.Identifier): boolean {
772 return this.#hoistedIdentifiers.has(node);
773 }
774
775 generateGloballyUniqueIdentifierName(
776 name: string | null,
777 ): ValidatedIdentifier {
778 const identifierNode = this.#scope.generateUidIdentifier(name ?? undefined);
779 return makeIdentifierName(identifierNode.name);
780 }
781
782 outlineFunction(fn: HIRFunction, type: ReactFunctionType | null): void {
783 this.#outlinedFunctions.push({fn, type});
784 }
785
786 getOutlinedFunctions(): Array<{
787 fn: HIRFunction;
788 type: ReactFunctionType | null;
789 }> {
790 return this.#outlinedFunctions;
791 }
792
793 #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 to
798 * fallback to the default value here
799 */
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 }
836
837 getGlobalDeclaration(
838 binding: NonLocalBinding,
839 loc: SourceLocation,
840 ): Global | null {
841 switch (binding.kind) {
842 case 'ModuleLocal': {
843 // don't resolve module locals
844 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 for
857 * every React builtin hook yet, we also check to see if the imported name
858 * is hook-like (whereas the fall-through below is checking if the aliased
859 * 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 say
879 * 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 }
893
894 /**
895 * For modules we don't own, we look at whether the original name or import alias
896 * are hook-like. Both of the following are likely hooks so we would return a hook
897 * 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 about
911 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 name
931 */
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 }
949
950 #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'];
957
958 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 }
966
967 if (shapeId !== null) {
968 const shape = this.#shapes.get(shapeId);
969
970 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 }
978
979 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 assigned
990 * 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 }
1011
1012 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 }
1024
1025 addHoistedIdentifier(node: t.Identifier): void {
1026 this.#contextIdentifiers.add(node);
1027 this.#hoistedIdentifiers.add(node);
1028 }
1029
1030 #getCustomHookType(): Global {
1031 if (this.config.enableAssumeHooksFollowRulesOfReact) {
1032 return DefaultNonmutatingHook;
1033 } else {
1034 return DefaultMutatingHook;
1035 }
1036 }
1037 }
1038
1039 const REANIMATED_MODULE_NAME = 'react-native-reanimated';
1040
1041 // From https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#LL18C1-L23C2
1042 export function isHookName(name: string): boolean {
1043 return /^use[A-Z0-9]/.test(name);
1044 }
1045
1046 export 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 }
1056
1057 export function validateEnvironmentConfig(
1058 partialConfig: PartialEnvironmentConfig,
1059 ): EnvironmentConfig {
1060 const config = EnvironmentConfigSchema.safeParse(partialConfig);
1061 if (config.success) {
1062 return config.data;
1063 }
1064
1065 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 }
1073
1074 export function tryParseExternalFunction(
1075 maybeExternalFunction: any,
1076 ): ExternalFunction {
1077 const externalFunction = ExternalFunctionSchema.safeParse(
1078 maybeExternalFunction,
1079 );
1080 if (externalFunction.success) {
1081 return externalFunction.data;
1082 }
1083
1084 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 }
1092
1093 export const DEFAULT_EXPORT = 'default';