compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts TYPESCRIPT 335 lines View on github.com → Search inside
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */78import {NodePath} from '@babel/core';9import * as t from '@babel/types';10import {Scope as BabelScope} from '@babel/traverse';1112import {CompilerError, ErrorCategory} from '../CompilerError';13import {14  EnvironmentConfig,15  GeneratedSource,16  NonLocalImportSpecifier,17} from '../HIR';18import {getOrInsertWith} from '../Utils/utils';19import {ExternalFunction, isHookName} from '../HIR/Environment';20import {Err, Ok, Result} from '../Utils/Result';21import {LoggerEvent, ParsedPluginOptions} from './Options';22import {getReactCompilerRuntimeModule} from './Program';23import {SuppressionRange} from './Suppression';2425export function validateRestrictedImports(26  path: NodePath<t.Program>,27  {validateBlocklistedImports}: EnvironmentConfig,28): CompilerError | null {29  if (30    validateBlocklistedImports == null ||31    validateBlocklistedImports.length === 032  ) {33    return null;34  }35  const error = new CompilerError();36  const restrictedImports = new Set(validateBlocklistedImports);37  path.traverse({38    ImportDeclaration(importDeclPath) {39      if (restrictedImports.has(importDeclPath.node.source.value)) {40        error.push({41          category: ErrorCategory.Todo,42          reason: 'Bailing out due to blocklisted import',43          description: `Import from module ${importDeclPath.node.source.value}`,44          loc: importDeclPath.node.loc ?? null,45        });46      }47    },48  });49  if (error.hasAnyErrors()) {50    return error;51  } else {52    return null;53  }54}5556type ProgramContextOptions = {57  program: NodePath<t.Program>;58  suppressions: Array<SuppressionRange>;59  opts: ParsedPluginOptions;60  filename: string | null;61  code: string | null;62  hasModuleScopeOptOut: boolean;63};64export class ProgramContext {65  /**66   * Program and environment context67   */68  scope: BabelScope;69  opts: ParsedPluginOptions;70  filename: string | null;71  code: string | null;72  reactRuntimeModule: string;73  suppressions: Array<SuppressionRange>;74  hasModuleScopeOptOut: boolean;7576  /*77   * This is a hack to work around what seems to be a Babel bug. Babel doesn't78   * consistently respect the `skip()` function to avoid revisiting a node within79   * a pass, so we use this set to track nodes that we have compiled.80   */81  alreadyCompiled: WeakSet<object> | Set<object> = new (WeakSet ?? Set)();82  // known generated or referenced identifiers in the program83  knownReferencedNames: Set<string> = new Set();84  // generated imports85  imports: Map<string, Map<string, NonLocalImportSpecifier>> = new Map();8687  constructor({88    program,89    suppressions,90    opts,91    filename,92    code,93    hasModuleScopeOptOut,94  }: ProgramContextOptions) {95    this.scope = program.scope;96    this.opts = opts;97    this.filename = filename;98    this.code = code;99    this.reactRuntimeModule = getReactCompilerRuntimeModule(opts.target);100    this.suppressions = suppressions;101    this.hasModuleScopeOptOut = hasModuleScopeOptOut;102  }103104  isHookName(name: string): boolean {105    return isHookName(name);106  }107108  hasReference(name: string): boolean {109    return (110      this.knownReferencedNames.has(name) ||111      this.scope.hasBinding(name) ||112      this.scope.hasGlobal(name) ||113      this.scope.hasReference(name)114    );115  }116117  newUid(name: string): string {118    /**119     * Don't call babel's generateUid for known hook imports, as120     * InferTypes might eventually type `HookKind` based on callee naming121     * convention and `_useFoo` is not named as a hook.122     *123     * Local uid generation is susceptible to check-before-use bugs since we're124     * checking for naming conflicts / references long before we actually insert125     * the import. (see similar logic in HIRBuilder:resolveBinding)126     */127    let uid;128    if (this.isHookName(name)) {129      uid = name;130      let i = 0;131      while (this.hasReference(uid)) {132        this.knownReferencedNames.add(uid);133        uid = `${name}_${i++}`;134      }135    } else if (!this.hasReference(name)) {136      uid = name;137    } else {138      uid = this.scope.generateUid(name);139    }140    this.knownReferencedNames.add(uid);141    return uid;142  }143144  addMemoCacheImport(): NonLocalImportSpecifier {145    return this.addImportSpecifier(146      {147        source: this.reactRuntimeModule,148        importSpecifierName: 'c',149      },150      '_c',151    );152  }153154  removeMemoCacheImport(): void {155    const moduleImports = this.imports.get(this.reactRuntimeModule);156    if (moduleImports == null) {157      return;158    }159    moduleImports.delete('c');160    if (moduleImports.size === 0) {161      this.imports.delete(this.reactRuntimeModule);162    }163  }164165  /**166   *167   * @param externalFunction168   * @param nameHint if defined, will be used as the name of the import specifier169   * @returns170   */171  addImportSpecifier(172    {source: module, importSpecifierName: specifier}: ExternalFunction,173    nameHint?: string,174  ): NonLocalImportSpecifier {175    const maybeBinding = this.imports.get(module)?.get(specifier);176    if (maybeBinding != null) {177      return {...maybeBinding};178    }179180    const binding: NonLocalImportSpecifier = {181      kind: 'ImportSpecifier',182      name: this.newUid(nameHint ?? specifier),183      module,184      imported: specifier,185    };186    getOrInsertWith(this.imports, module, () => new Map()).set(specifier, {187      ...binding,188    });189    return binding;190  }191192  addNewReference(name: string): void {193    this.knownReferencedNames.add(name);194  }195196  assertGlobalBinding(197    name: string,198    localScope?: BabelScope,199  ): Result<void, CompilerError> {200    const scope = localScope ?? this.scope;201    if (!scope.hasReference(name) && !scope.hasBinding(name)) {202      return Ok(undefined);203    }204    const error = new CompilerError();205    error.push({206      category: ErrorCategory.Todo,207      reason: 'Encountered conflicting global in generated program',208      description: `Conflict from local binding ${name}`,209      loc: scope.getBinding(name)?.path.node.loc ?? null,210      suggestions: null,211    });212    return Err(error);213  }214215  logEvent(event: LoggerEvent): void {216    if (this.opts.logger != null) {217      this.opts.logger.logEvent(this.filename, event);218    }219  }220}221222function getExistingImports(223  program: NodePath<t.Program>,224): Map<string, NodePath<t.ImportDeclaration>> {225  const existingImports = new Map<string, NodePath<t.ImportDeclaration>>();226  program.traverse({227    ImportDeclaration(path) {228      if (isNonNamespacedImport(path)) {229        existingImports.set(path.node.source.value, path);230      }231    },232  });233  return existingImports;234}235236export function addImportsToProgram(237  path: NodePath<t.Program>,238  programContext: ProgramContext,239): void {240  const existingImports = getExistingImports(path);241  const stmts: Array<t.ImportDeclaration | t.VariableDeclaration> = [];242  const sortedModules = [...programContext.imports.entries()].sort(([a], [b]) =>243    a.localeCompare(b),244  );245  for (const [moduleName, importsMap] of sortedModules) {246    for (const [specifierName, loweredImport] of importsMap) {247      /**248       * Assert that the import identifier hasn't already be declared in the program.249       * Note: we use getBinding here since `Scope.hasBinding` pessimistically returns true250       * for all allocated uids (from `Scope.getUid`)251       */252      CompilerError.invariant(253        path.scope.getBinding(loweredImport.name) == null,254        {255          reason:256            'Encountered conflicting import specifiers in generated program',257          description: `Conflict from import ${loweredImport.module}:(${loweredImport.imported} as ${loweredImport.name})`,258          loc: GeneratedSource,259        },260      );261      CompilerError.invariant(262        loweredImport.module === moduleName &&263          loweredImport.imported === specifierName,264        {265          reason:266            'Found inconsistent import specifier. This is an internal bug.',267          description: `Expected import ${moduleName}:${specifierName} but found ${loweredImport.module}:${loweredImport.imported}`,268          loc: GeneratedSource,269        },270      );271    }272    const sortedImport: Array<NonLocalImportSpecifier> = [273      ...importsMap.values(),274    ].sort(({imported: a}, {imported: b}) => a.localeCompare(b));275    const importSpecifiers = sortedImport.map(specifier => {276      return t.importSpecifier(277        t.identifier(specifier.name),278        t.identifier(specifier.imported),279      );280    });281282    /**283     * If an existing import of this module exists (ie `import { ... } from284     * '<moduleName>'`), inject new imported specifiers into the list of285     * destructured variables.286     */287    const maybeExistingImports = existingImports.get(moduleName);288    if (maybeExistingImports != null) {289      maybeExistingImports.pushContainer('specifiers', importSpecifiers);290    } else {291      if (path.node.sourceType === 'module') {292        stmts.push(293          t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),294        );295      } else {296        stmts.push(297          t.variableDeclaration('const', [298            t.variableDeclarator(299              t.objectPattern(300                sortedImport.map(specifier => {301                  return t.objectProperty(302                    t.identifier(specifier.imported),303                    t.identifier(specifier.name),304                  );305                }),306              ),307              t.callExpression(t.identifier('require'), [308                t.stringLiteral(moduleName),309              ]),310            ),311          ]),312        );313      }314    }315  }316  path.unshiftContainer('body', stmts);317}318319/*320 * Matches `import { ... } from <moduleName>;`321 * but not `import * as React from <moduleName>;`322 *         `import type { Foo } from <moduleName>;`323 */324function isNonNamespacedImport(325  importDeclPath: NodePath<t.ImportDeclaration>,326): boolean {327  return (328    importDeclPath329      .get('specifiers')330      .every(specifier => specifier.isImportSpecifier()) &&331    importDeclPath.node.importKind !== 'type' &&332    importDeclPath.node.importKind !== 'typeof'333  );334}

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.