| 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 | use rustc_hash::{FxHashMap, FxHashSet}; |
| 8 | |
| 9 | use react_compiler_ast::common::BaseNode; |
| 10 | use react_compiler_ast::declarations::{ |
| 11 | ImportDeclaration, ImportKind, ImportSpecifier, ImportSpecifierData, ModuleExportName, |
| 12 | }; |
| 13 | use react_compiler_ast::expressions::{CallExpression, Expression, Identifier}; |
| 14 | use react_compiler_ast::literals::StringLiteral; |
| 15 | use react_compiler_ast::patterns::{ |
| 16 | ObjectPattern, ObjectPatternProp, ObjectPatternProperty, PatternLike, |
| 17 | }; |
| 18 | use react_compiler_ast::scope::ScopeInfo; |
| 19 | use react_compiler_ast::statements::{ |
| 20 | Statement, VariableDeclaration, VariableDeclarationKind, VariableDeclarator, |
| 21 | }; |
| 22 | use react_compiler_ast::{Program, SourceType}; |
| 23 | use react_compiler_diagnostics::{ |
| 24 | CompilerError, CompilerErrorDetail, ErrorCategory, Position, SourceLocation, |
| 25 | }; |
| 26 | |
| 27 | use super::compile_result::{DebugLogEntry, LoggerEvent, OrderedLogItem}; |
| 28 | use super::plugin_options::{CompilerTarget, PluginOptions}; |
| 29 | use super::suppression::SuppressionRange; |
| 30 | use crate::timing::TimingData; |
| 31 | |
| 32 | /// An import specifier tracked by ProgramContext. |
| 33 | /// Corresponds to NonLocalImportSpecifier in the TS compiler. |
| 34 | #[derive(Debug, Clone)] |
| 35 | pub struct NonLocalImportSpecifier { |
| 36 | pub name: String, |
| 37 | pub module: String, |
| 38 | pub imported: String, |
| 39 | } |
| 40 | |
| 41 | /// Context for the program being compiled. |
| 42 | /// Tracks compiled functions, generated names, and import requirements. |
| 43 | /// Equivalent to ProgramContext class in Imports.ts. |
| 44 | pub struct ProgramContext { |
| 45 | pub opts: PluginOptions, |
| 46 | pub filename: Option<String>, |
| 47 | /// The source filename from the parser's sourceFilename option. |
| 48 | /// This is the filename stored on AST node `loc.filename` fields, |
| 49 | /// which may differ from `filename` (e.g., no path prefix). |
| 50 | source_filename: Option<String>, |
| 51 | pub code: Option<String>, |
| 52 | pub react_runtime_module: String, |
| 53 | pub suppressions: Vec<SuppressionRange>, |
| 54 | pub has_module_scope_opt_out: bool, |
| 55 | pub events: Vec<LoggerEvent>, |
| 56 | /// Unified ordered log that interleaves events and debug entries |
| 57 | /// in the order they were emitted during compilation. |
| 58 | pub ordered_log: Vec<OrderedLogItem>, |
| 59 | |
| 60 | // Pre-resolved import local names for codegen |
| 61 | pub instrument_fn_name: Option<String>, |
| 62 | pub instrument_gating_name: Option<String>, |
| 63 | pub hook_guard_name: Option<String>, |
| 64 | |
| 65 | // Variable renames from lowering, to be applied back to the Babel AST |
| 66 | pub renames: Vec<react_compiler_hir::environment::BindingRename>, |
| 67 | |
| 68 | /// Timing data for profiling. Accumulates across all function compilations. |
| 69 | pub timing: TimingData, |
| 70 | |
| 71 | /// Whether debug logging is enabled (HIR formatting after each pass). |
| 72 | pub debug_enabled: bool, |
| 73 | |
| 74 | // Internal state |
| 75 | already_compiled: FxHashSet<u32>, |
| 76 | known_referenced_names: FxHashSet<String>, |
| 77 | imports: FxHashMap<String, FxHashMap<String, NonLocalImportSpecifier>>, |
| 78 | } |
| 79 | |
| 80 | impl ProgramContext { |
| 81 | pub fn new( |
| 82 | opts: PluginOptions, |
| 83 | filename: Option<String>, |
| 84 | code: Option<String>, |
| 85 | suppressions: Vec<SuppressionRange>, |
| 86 | has_module_scope_opt_out: bool, |
| 87 | ) -> Self { |
| 88 | let react_runtime_module = get_react_compiler_runtime_module(&opts.target); |
| 89 | let profiling = opts.profiling; |
| 90 | let debug_enabled = opts.debug; |
| 91 | Self { |
| 92 | opts, |
| 93 | filename, |
| 94 | source_filename: None, |
| 95 | code, |
| 96 | react_runtime_module, |
| 97 | suppressions, |
| 98 | has_module_scope_opt_out, |
| 99 | events: Vec::new(), |
| 100 | ordered_log: Vec::new(), |
| 101 | instrument_fn_name: None, |
| 102 | instrument_gating_name: None, |
| 103 | hook_guard_name: None, |
| 104 | renames: Vec::new(), |
| 105 | timing: TimingData::new(profiling), |
| 106 | debug_enabled, |
| 107 | already_compiled: FxHashSet::default(), |
| 108 | known_referenced_names: FxHashSet::default(), |
| 109 | imports: FxHashMap::default(), |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | /// Set the source filename (from AST node loc.filename). |
| 114 | pub fn set_source_filename(&mut self, filename: Option<String>) { |
| 115 | if self.source_filename.is_none() { |
| 116 | self.source_filename = filename; |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | /// Get the source filename for logger events. |
| 121 | pub fn source_filename(&self) -> Option<String> { |
| 122 | self.source_filename.clone() |
| 123 | } |
| 124 | |
| 125 | /// Check if a function at the given start position has already been compiled. |
| 126 | /// This is a workaround for Babel not consistently respecting skip(). |
| 127 | pub fn is_already_compiled(&self, start: u32) -> bool { |
| 128 | self.already_compiled.contains(&start) |
| 129 | } |
| 130 | |
| 131 | /// Mark a function at the given start position as compiled. |
| 132 | pub fn mark_compiled(&mut self, start: u32) { |
| 133 | self.already_compiled.insert(start); |
| 134 | } |
| 135 | |
| 136 | /// Initialize known referenced names from scope bindings. |
| 137 | /// Call this after construction to seed conflict detection with program scope bindings. |
| 138 | pub fn init_from_scope(&mut self, scope: &ScopeInfo) { |
| 139 | // Register ALL bindings (not just program-scope) so that UID generation |
| 140 | // avoids name conflicts with any binding in the file. This matches |
| 141 | // Babel's generateUid() which checks all scopes. |
| 142 | for binding in &scope.bindings { |
| 143 | self.known_referenced_names.insert(binding.name.clone()); |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | /// Check if a name conflicts with known references. |
| 148 | pub fn has_reference(&self, name: &str) -> bool { |
| 149 | self.known_referenced_names.contains(name) |
| 150 | } |
| 151 | |
| 152 | /// Generate a unique identifier name that doesn't conflict with existing bindings. |
| 153 | /// |
| 154 | /// For hook names (use*), preserves the original name to avoid breaking |
| 155 | /// hook-name-based type inference. For other names, prefixes with underscore |
| 156 | /// similar to Babel's generateUid. |
| 157 | pub fn new_uid(&mut self, name: &str) -> String { |
| 158 | if is_hook_name(name) { |
| 159 | // Don't prefix hooks with underscore, since InferTypes might |
| 160 | // type HookKind based on callee naming convention. |
| 161 | let mut uid = name.to_string(); |
| 162 | let mut i = 0; |
| 163 | while self.has_reference(&uid) { |
| 164 | uid = format!("{}_{}", name, i); |
| 165 | i += 1; |
| 166 | } |
| 167 | self.known_referenced_names.insert(uid.clone()); |
| 168 | uid |
| 169 | } else if !self.has_reference(name) { |
| 170 | self.known_referenced_names.insert(name.to_string()); |
| 171 | name.to_string() |
| 172 | } else { |
| 173 | // Generate unique name with underscore prefix (similar to Babel's generateUid). |
| 174 | // Babel strips leading underscores before prefixing, so: |
| 175 | // generateUid("_c") → strips to "c" → generates "_c", "_c2", "_c3", ... |
| 176 | // generateUid("foo") → generates "_foo", "_foo2", "_foo3", ... |
| 177 | let base = name.trim_start_matches('_'); |
| 178 | let mut uid = format!("_{}", base); |
| 179 | let mut i = 2; |
| 180 | while self.has_reference(&uid) { |
| 181 | uid = format!("_{}{}", base, i); |
| 182 | i += 1; |
| 183 | } |
| 184 | self.known_referenced_names.insert(uid.clone()); |
| 185 | uid |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | /// Add the memo cache import (the `c` function from the compiler runtime). |
| 190 | pub fn add_memo_cache_import(&mut self) -> NonLocalImportSpecifier { |
| 191 | let module = self.react_runtime_module.clone(); |
| 192 | self.add_import_specifier(&module, "c", Some("_c")) |
| 193 | } |
| 194 | |
| 195 | /// Add an import specifier, reusing an existing one if it was already added. |
| 196 | /// |
| 197 | /// If `name_hint` is provided, it will be used as the basis for the local |
| 198 | /// name; otherwise `specifier` is used. |
| 199 | pub fn add_import_specifier( |
| 200 | &mut self, |
| 201 | module: &str, |
| 202 | specifier: &str, |
| 203 | name_hint: Option<&str>, |
| 204 | ) -> NonLocalImportSpecifier { |
| 205 | // Check if already imported |
| 206 | if let Some(module_imports) = self.imports.get(module) { |
| 207 | if let Some(existing) = module_imports.get(specifier) { |
| 208 | return existing.clone(); |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | let name = self.new_uid(name_hint.unwrap_or(specifier)); |
| 213 | let binding = NonLocalImportSpecifier { |
| 214 | name, |
| 215 | module: module.to_string(), |
| 216 | imported: specifier.to_string(), |
| 217 | }; |
| 218 | |
| 219 | self.imports |
| 220 | .entry(module.to_string()) |
| 221 | .or_default() |
| 222 | .insert(specifier.to_string(), binding.clone()); |
| 223 | |
| 224 | binding |
| 225 | } |
| 226 | |
| 227 | /// Register a name as referenced so future uid generation avoids it. |
| 228 | pub fn add_new_reference(&mut self, name: String) { |
| 229 | self.known_referenced_names.insert(name); |
| 230 | } |
| 231 | |
| 232 | /// Get the set of known referenced names for seeding per-function Environment UID generation. |
| 233 | pub fn known_referenced_names(&self) -> &FxHashSet<String> { |
| 234 | &self.known_referenced_names |
| 235 | } |
| 236 | |
| 237 | /// Merge UID names generated during a function compilation back into the program context, |
| 238 | /// so subsequent function compilations avoid collisions. |
| 239 | pub fn merge_uid_known_names(&mut self, names: &FxHashSet<String>) { |
| 240 | self.known_referenced_names.extend(names.iter().cloned()); |
| 241 | } |
| 242 | |
| 243 | /// Log a compilation event. |
| 244 | pub fn log_event(&mut self, event: LoggerEvent) { |
| 245 | self.ordered_log.push(OrderedLogItem::Event { |
| 246 | event: event.clone(), |
| 247 | }); |
| 248 | self.events.push(event); |
| 249 | } |
| 250 | |
| 251 | /// Log a debug entry (for debugLogIRs support). |
| 252 | pub fn log_debug(&mut self, entry: DebugLogEntry) { |
| 253 | self.ordered_log.push(OrderedLogItem::Debug { entry }); |
| 254 | } |
| 255 | |
| 256 | /// Check if there are any pending imports to add to the program. |
| 257 | pub fn has_pending_imports(&self) -> bool { |
| 258 | !self.imports.is_empty() |
| 259 | } |
| 260 | |
| 261 | /// Get an immutable view of the generated imports. |
| 262 | pub fn imports(&self) -> &FxHashMap<String, FxHashMap<String, NonLocalImportSpecifier>> { |
| 263 | &self.imports |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | /// Check for blocklisted import modules. |
| 268 | /// Returns a CompilerError if any blocklisted imports are found. |
| 269 | pub fn validate_restricted_imports( |
| 270 | program: &Program, |
| 271 | blocklisted: &Option<Vec<String>>, |
| 272 | ) -> Option<CompilerError> { |
| 273 | let blocklisted = match blocklisted { |
| 274 | Some(b) if !b.is_empty() => b, |
| 275 | _ => return None, |
| 276 | }; |
| 277 | let restricted: FxHashSet<&str> = blocklisted.iter().map(|s| s.as_str()).collect(); |
| 278 | let mut error = CompilerError::new(); |
| 279 | |
| 280 | for stmt in &program.body { |
| 281 | if let Statement::ImportDeclaration(import) = stmt { |
| 282 | if import |
| 283 | .source |
| 284 | .value |
| 285 | .as_str() |
| 286 | .is_some_and(|v| restricted.contains(v)) |
| 287 | { |
| 288 | let mut detail = CompilerErrorDetail::new( |
| 289 | ErrorCategory::Todo, |
| 290 | "Bailing out due to blocklisted import", |
| 291 | ) |
| 292 | .with_description(format!("Import from module {}", import.source.value)); |
| 293 | detail.loc = import.base.loc.as_ref().map(|loc| SourceLocation { |
| 294 | start: Position { |
| 295 | line: loc.start.line, |
| 296 | column: loc.start.column, |
| 297 | index: loc.start.index, |
| 298 | }, |
| 299 | end: Position { |
| 300 | line: loc.end.line, |
| 301 | column: loc.end.column, |
| 302 | index: loc.end.index, |
| 303 | }, |
| 304 | }); |
| 305 | error.push_error_detail(detail); |
| 306 | } |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | if error.has_any_errors() { |
| 311 | Some(error) |
| 312 | } else { |
| 313 | None |
| 314 | } |
| 315 | } |
| 316 | |
| 317 | /// Insert import declarations into the program body. |
| 318 | /// Handles both ESM imports and CommonJS require. |
| 319 | /// |
| 320 | /// For existing imports of the same module (non-namespaced, value imports), |
| 321 | /// new specifiers are merged into the existing declaration. Otherwise, |
| 322 | /// new import/require statements are prepended to the program body. |
| 323 | pub fn add_imports_to_program(program: &mut Program, context: &ProgramContext) { |
| 324 | if context.imports.is_empty() { |
| 325 | return; |
| 326 | } |
| 327 | |
| 328 | // Collect existing non-namespaced imports by module name |
| 329 | let existing_import_indices: FxHashMap<String, usize> = program |
| 330 | .body |
| 331 | .iter() |
| 332 | .enumerate() |
| 333 | .filter_map(|(idx, stmt)| { |
| 334 | if let Statement::ImportDeclaration(import) = stmt { |
| 335 | if is_non_namespaced_import(import) { |
| 336 | return Some((import.source.value.to_marker_string(), idx)); |
| 337 | } |
| 338 | } |
| 339 | None |
| 340 | }) |
| 341 | .collect(); |
| 342 | |
| 343 | let mut stmts: Vec<Statement> = Vec::new(); |
| 344 | let mut sorted_modules: Vec<_> = context.imports.iter().collect(); |
| 345 | sorted_modules.sort_by(|(a, _), (b, _)| a.to_lowercase().cmp(&b.to_lowercase())); |
| 346 | |
| 347 | for (module_name, imports_map) in sorted_modules { |
| 348 | let sorted_imports = { |
| 349 | let mut sorted: Vec<_> = imports_map.values().collect(); |
| 350 | sorted.sort_by_key(|s| &s.imported); |
| 351 | sorted |
| 352 | }; |
| 353 | |
| 354 | let import_specifiers: Vec<ImportSpecifier> = sorted_imports |
| 355 | .iter() |
| 356 | .map(|spec| make_import_specifier(spec)) |
| 357 | .collect(); |
| 358 | |
| 359 | // If an existing import of this module exists, merge into it |
| 360 | if let Some(&idx) = existing_import_indices.get(module_name.as_str()) { |
| 361 | if let Statement::ImportDeclaration(ref mut import) = program.body[idx] { |
| 362 | import.specifiers.extend(import_specifiers); |
| 363 | } |
| 364 | } else if matches!(program.source_type, SourceType::Module) { |
| 365 | // ESM: import { ... } from 'module' |
| 366 | stmts.push(Statement::ImportDeclaration(ImportDeclaration { |
| 367 | base: BaseNode::typed("ImportDeclaration"), |
| 368 | specifiers: import_specifiers, |
| 369 | source: StringLiteral { |
| 370 | base: BaseNode::typed("StringLiteral"), |
| 371 | value: module_name.clone().into(), |
| 372 | }, |
| 373 | import_kind: None, |
| 374 | assertions: None, |
| 375 | attributes: None, |
| 376 | })); |
| 377 | } else { |
| 378 | // CommonJS: const { imported: local, ... } = require('module') |
| 379 | let properties: Vec<ObjectPatternProperty> = sorted_imports |
| 380 | .iter() |
| 381 | .map(|spec| { |
| 382 | ObjectPatternProperty::ObjectProperty(ObjectPatternProp { |
| 383 | base: BaseNode::typed("ObjectProperty"), |
| 384 | key: Box::new(Expression::Identifier(Identifier { |
| 385 | base: BaseNode::typed("Identifier"), |
| 386 | name: spec.imported.clone(), |
| 387 | type_annotation: None, |
| 388 | optional: None, |
| 389 | decorators: None, |
| 390 | })), |
| 391 | value: Box::new(PatternLike::Identifier(Identifier { |
| 392 | base: BaseNode::typed("Identifier"), |
| 393 | name: spec.name.clone(), |
| 394 | type_annotation: None, |
| 395 | optional: None, |
| 396 | decorators: None, |
| 397 | })), |
| 398 | computed: false, |
| 399 | shorthand: false, |
| 400 | decorators: None, |
| 401 | method: None, |
| 402 | }) |
| 403 | }) |
| 404 | .collect(); |
| 405 | |
| 406 | stmts.push(Statement::VariableDeclaration(VariableDeclaration { |
| 407 | base: BaseNode::typed("VariableDeclaration"), |
| 408 | kind: VariableDeclarationKind::Const, |
| 409 | declarations: vec![VariableDeclarator { |
| 410 | base: BaseNode::typed("VariableDeclarator"), |
| 411 | id: PatternLike::ObjectPattern(ObjectPattern { |
| 412 | base: BaseNode::typed("ObjectPattern"), |
| 413 | properties, |
| 414 | type_annotation: None, |
| 415 | decorators: None, |
| 416 | }), |
| 417 | init: Some(Box::new(Expression::CallExpression(CallExpression { |
| 418 | base: BaseNode::typed("CallExpression"), |
| 419 | callee: Box::new(Expression::Identifier(Identifier { |
| 420 | base: BaseNode::typed("Identifier"), |
| 421 | name: "require".to_string(), |
| 422 | type_annotation: None, |
| 423 | optional: None, |
| 424 | decorators: None, |
| 425 | })), |
| 426 | arguments: vec![Expression::StringLiteral(StringLiteral { |
| 427 | base: BaseNode::typed("StringLiteral"), |
| 428 | value: module_name.clone().into(), |
| 429 | })], |
| 430 | type_parameters: None, |
| 431 | type_arguments: None, |
| 432 | optional: None, |
| 433 | }))), |
| 434 | definite: None, |
| 435 | }], |
| 436 | declare: None, |
| 437 | })); |
| 438 | } |
| 439 | } |
| 440 | |
| 441 | // Prepend new import statements to the program body |
| 442 | if !stmts.is_empty() { |
| 443 | let mut new_body = stmts; |
| 444 | new_body.append(&mut program.body); |
| 445 | program.body = new_body; |
| 446 | } |
| 447 | } |
| 448 | |
| 449 | /// Create an ImportSpecifier AST node from a NonLocalImportSpecifier. |
| 450 | fn make_import_specifier(spec: &NonLocalImportSpecifier) -> ImportSpecifier { |
| 451 | ImportSpecifier::ImportSpecifier(ImportSpecifierData { |
| 452 | base: BaseNode::typed("ImportSpecifier"), |
| 453 | local: Identifier { |
| 454 | base: BaseNode::typed("Identifier"), |
| 455 | name: spec.name.clone(), |
| 456 | type_annotation: None, |
| 457 | optional: None, |
| 458 | decorators: None, |
| 459 | }, |
| 460 | imported: ModuleExportName::Identifier(Identifier { |
| 461 | base: BaseNode::typed("Identifier"), |
| 462 | name: spec.imported.clone(), |
| 463 | type_annotation: None, |
| 464 | optional: None, |
| 465 | decorators: None, |
| 466 | }), |
| 467 | import_kind: None, |
| 468 | }) |
| 469 | } |
| 470 | |
| 471 | /// Check if an import declaration is a non-namespaced value import. |
| 472 | /// Matches `import { ... } from 'module'` but NOT: |
| 473 | /// - `import * as Foo from 'module'` (namespace) |
| 474 | /// - `import type { Foo } from 'module'` (type import) |
| 475 | /// - `import typeof { Foo } from 'module'` (typeof import) |
| 476 | fn is_non_namespaced_import(import: &ImportDeclaration) -> bool { |
| 477 | import |
| 478 | .specifiers |
| 479 | .iter() |
| 480 | .all(|s| matches!(s, ImportSpecifier::ImportSpecifier(_))) |
| 481 | && import |
| 482 | .import_kind |
| 483 | .as_ref() |
| 484 | .map_or(true, |k| matches!(k, ImportKind::Value)) |
| 485 | } |
| 486 | |
| 487 | /// Check if a name follows the React hook naming convention (use[A-Z0-9]...). |
| 488 | fn is_hook_name(name: &str) -> bool { |
| 489 | let bytes = name.as_bytes(); |
| 490 | bytes.len() >= 4 |
| 491 | && bytes[0] == b'u' |
| 492 | && bytes[1] == b's' |
| 493 | && bytes[2] == b'e' |
| 494 | && bytes |
| 495 | .get(3) |
| 496 | .map_or(false, |c| c.is_ascii_uppercase() || c.is_ascii_digit()) |
| 497 | } |
| 498 | |
| 499 | /// Get the runtime module name based on the compiler target. |
| 500 | pub fn get_react_compiler_runtime_module(target: &CompilerTarget) -> String { |
| 501 | match target { |
| 502 | CompilerTarget::Version(v) if v == "19" => "react/compiler-runtime".to_string(), |
| 503 | CompilerTarget::Version(v) if v == "17" || v == "18" => { |
| 504 | "react-compiler-runtime".to_string() |
| 505 | } |
| 506 | CompilerTarget::MetaInternal { runtime_module, .. } => runtime_module.clone(), |
| 507 | // Default to React 19 runtime for unrecognized versions |
| 508 | CompilerTarget::Version(_) => "react/compiler-runtime".to_string(), |
| 509 | } |
| 510 | } |