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.