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