| 1 | use rustc_hash::{FxHashMap, FxHashSet}; |
| 2 | |
| 3 | use react_compiler_diagnostics::CompilerDiagnostic; |
| 4 | use react_compiler_diagnostics::CompilerError; |
| 5 | use react_compiler_diagnostics::CompilerErrorDetail; |
| 6 | use react_compiler_diagnostics::ErrorCategory; |
| 7 | |
| 8 | use crate::default_module_type_provider::default_module_type_provider; |
| 9 | use crate::environment_config::EnvironmentConfig; |
| 10 | use crate::globals::Global; |
| 11 | use crate::globals::GlobalRegistry; |
| 12 | use crate::globals::{self}; |
| 13 | use crate::object_shape::BUILT_IN_MIXED_READONLY_ID; |
| 14 | use crate::object_shape::FunctionSignature; |
| 15 | use crate::object_shape::HookKind; |
| 16 | use crate::object_shape::HookSignatureBuilder; |
| 17 | use crate::object_shape::ShapeRegistry; |
| 18 | use crate::object_shape::add_hook; |
| 19 | use crate::object_shape::default_mutating_hook; |
| 20 | use crate::object_shape::default_nonmutating_hook; |
| 21 | use crate::*; |
| 22 | |
| 23 | /// A variable rename from lowering: the binding at `declaration_start` position |
| 24 | /// was renamed from `original` to `renamed`. |
| 25 | #[derive(Debug, Clone)] |
| 26 | pub struct BindingRename { |
| 27 | pub original: String, |
| 28 | pub renamed: String, |
| 29 | pub declaration_start: u32, |
| 30 | } |
| 31 | |
| 32 | /// Output mode for the compiler, mirrored from the entrypoint's CompilerOutputMode. |
| 33 | /// Stored on Environment so pipeline passes can access it. |
| 34 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 35 | pub enum OutputMode { |
| 36 | Ssr, |
| 37 | Client, |
| 38 | Lint, |
| 39 | } |
| 40 | |
| 41 | pub struct Environment { |
| 42 | // Counters |
| 43 | pub next_block_id_counter: u32, |
| 44 | pub next_scope_id_counter: u32, |
| 45 | next_mutable_range_id_counter: u32, |
| 46 | |
| 47 | // Arenas (use direct field access for sliced borrows) |
| 48 | pub identifiers: Vec<Identifier>, |
| 49 | pub types: Vec<Type>, |
| 50 | pub scopes: Vec<ReactiveScope>, |
| 51 | pub functions: Vec<HirFunction>, |
| 52 | |
| 53 | // Error accumulation |
| 54 | pub errors: CompilerError, |
| 55 | |
| 56 | // Function type classification (Component, Hook, Other) |
| 57 | pub fn_type: ReactFunctionType, |
| 58 | |
| 59 | // Output mode (Client, Ssr, Lint) |
| 60 | pub output_mode: OutputMode, |
| 61 | |
| 62 | // Source file code (for fast refresh hash computation) |
| 63 | pub code: Option<String>, |
| 64 | |
| 65 | // Source file name (for instrumentation) |
| 66 | pub filename: Option<String>, |
| 67 | |
| 68 | // Pre-resolved import local names for instrumentation/hook guards. |
| 69 | // Set by the program-level code before compilation. |
| 70 | pub instrument_fn_name: Option<String>, |
| 71 | pub instrument_gating_name: Option<String>, |
| 72 | pub hook_guard_name: Option<String>, |
| 73 | |
| 74 | // Renames: tracks variable renames from lowering (original_name → new_name) |
| 75 | // keyed by binding declaration position, for applying back to the Babel AST. |
| 76 | pub renames: Vec<BindingRename>, |
| 77 | |
| 78 | // Node IDs of identifiers that are actual references to bindings. |
| 79 | // Used by codegen to filter type annotation renames — only rename identifiers |
| 80 | // whose node_id is in this set (type labels like ObjectTypeIndexer params |
| 81 | // are NOT in this set and should keep their original names). |
| 82 | pub reference_node_ids: FxHashSet<u32>, |
| 83 | |
| 84 | // Hoisted identifiers: tracks which bindings have already been hoisted |
| 85 | // via DeclareContext to avoid duplicate hoisting. |
| 86 | // Uses u32 to avoid depending on react_compiler_ast types. |
| 87 | hoisted_identifiers: FxHashSet<u32>, |
| 88 | |
| 89 | // Config flags for validation passes (kept for backwards compat with existing pipeline code) |
| 90 | pub validate_preserve_existing_memoization_guarantees: bool, |
| 91 | pub validate_no_set_state_in_render: bool, |
| 92 | pub enable_preserve_existing_memoization_guarantees: bool, |
| 93 | |
| 94 | // Type system registries |
| 95 | globals: GlobalRegistry, |
| 96 | pub shapes: ShapeRegistry, |
| 97 | module_types: FxHashMap<String, Option<Global>>, |
| 98 | module_type_errors: FxHashMap<String, Vec<String>>, |
| 99 | |
| 100 | // Environment configuration (feature flags, custom hooks, etc.) |
| 101 | pub config: EnvironmentConfig, |
| 102 | |
| 103 | // Cached default hook types (lazily initialized) |
| 104 | default_nonmutating_hook: Option<Global>, |
| 105 | default_mutating_hook: Option<Global>, |
| 106 | |
| 107 | // Outlined functions: functions extracted from the component during outlining passes |
| 108 | outlined_functions: Vec<OutlinedFunctionEntry>, |
| 109 | |
| 110 | // Known names for collision-aware UID generation. Lazily populated from |
| 111 | // identifiers on first use, then updated with each generated name. |
| 112 | // Matches Babel's generateUid behavior of checking hasBinding/hasReference. |
| 113 | uid_known_names: Option<FxHashSet<String>>, |
| 114 | } |
| 115 | |
| 116 | /// An outlined function entry, stored on Environment during compilation. |
| 117 | /// Corresponds to TS `{ fn: HIRFunction, type: ReactFunctionType | null }`. |
| 118 | #[derive(Debug, Clone)] |
| 119 | pub struct OutlinedFunctionEntry { |
| 120 | pub func: HirFunction, |
| 121 | pub fn_type: Option<ReactFunctionType>, |
| 122 | } |
| 123 | |
| 124 | impl Environment { |
| 125 | pub fn new() -> Self { |
| 126 | Self::with_config(EnvironmentConfig::default()) |
| 127 | } |
| 128 | |
| 129 | /// Create a new Environment with the given configuration. |
| 130 | /// |
| 131 | /// Initializes the shape and global registries, registers custom hooks, |
| 132 | /// and sets up the module type cache. |
| 133 | pub fn with_config(config: EnvironmentConfig) -> Self { |
| 134 | let mut shapes = ShapeRegistry::with_base(globals::base_shapes()); |
| 135 | let mut global_registry = GlobalRegistry::with_base(globals::base_globals()); |
| 136 | |
| 137 | // Register custom hooks from config |
| 138 | for (hook_name, hook) in &config.custom_hooks { |
| 139 | // Don't overwrite existing globals (matches TS invariant) |
| 140 | if global_registry.contains_key(hook_name) { |
| 141 | continue; |
| 142 | } |
| 143 | let return_type = if hook.transitive_mixed_data { |
| 144 | Type::Object { |
| 145 | shape_id: Some(BUILT_IN_MIXED_READONLY_ID.to_string()), |
| 146 | } |
| 147 | } else { |
| 148 | Type::Poly |
| 149 | }; |
| 150 | let hook_type = add_hook( |
| 151 | &mut shapes, |
| 152 | HookSignatureBuilder { |
| 153 | rest_param: Some(hook.effect_kind), |
| 154 | return_type, |
| 155 | return_value_kind: hook.value_kind, |
| 156 | hook_kind: HookKind::Custom, |
| 157 | no_alias: hook.no_alias, |
| 158 | ..Default::default() |
| 159 | }, |
| 160 | None, |
| 161 | ); |
| 162 | global_registry.insert(hook_name.clone(), hook_type); |
| 163 | } |
| 164 | |
| 165 | // Register reanimated module type when enabled |
| 166 | let mut module_types: FxHashMap<String, Option<Global>> = FxHashMap::default(); |
| 167 | if config.enable_custom_type_definition_for_reanimated { |
| 168 | let reanimated_module_type = globals::get_reanimated_module_type(&mut shapes); |
| 169 | module_types.insert( |
| 170 | "react-native-reanimated".to_string(), |
| 171 | Some(reanimated_module_type), |
| 172 | ); |
| 173 | } |
| 174 | |
| 175 | Self { |
| 176 | next_block_id_counter: 0, |
| 177 | next_scope_id_counter: 0, |
| 178 | next_mutable_range_id_counter: 0, |
| 179 | identifiers: Vec::new(), |
| 180 | types: Vec::new(), |
| 181 | scopes: Vec::new(), |
| 182 | functions: Vec::new(), |
| 183 | errors: CompilerError::new(), |
| 184 | fn_type: ReactFunctionType::Other, |
| 185 | output_mode: OutputMode::Client, |
| 186 | code: None, |
| 187 | filename: None, |
| 188 | instrument_fn_name: None, |
| 189 | instrument_gating_name: None, |
| 190 | hook_guard_name: None, |
| 191 | renames: Vec::new(), |
| 192 | reference_node_ids: FxHashSet::default(), |
| 193 | hoisted_identifiers: FxHashSet::default(), |
| 194 | validate_preserve_existing_memoization_guarantees: config |
| 195 | .validate_preserve_existing_memoization_guarantees, |
| 196 | validate_no_set_state_in_render: config.validate_no_set_state_in_render, |
| 197 | enable_preserve_existing_memoization_guarantees: config |
| 198 | .enable_preserve_existing_memoization_guarantees, |
| 199 | globals: global_registry, |
| 200 | shapes, |
| 201 | module_types, |
| 202 | module_type_errors: FxHashMap::default(), |
| 203 | default_nonmutating_hook: None, |
| 204 | default_mutating_hook: None, |
| 205 | outlined_functions: Vec::new(), |
| 206 | uid_known_names: None, |
| 207 | config, |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | /// Create a child Environment for compiling an outlined function. |
| 212 | /// |
| 213 | /// The child shares the same config, globals, and shapes, and receives copies of |
| 214 | /// all arenas (identifiers, types, scopes, functions) so that references from |
| 215 | /// the outlined HIR remain valid. Block/scope counters start past the cloned |
| 216 | /// data to avoid ID conflicts. |
| 217 | pub fn for_outlined_fn(&self, fn_type: ReactFunctionType) -> Self { |
| 218 | Self { |
| 219 | // Start block counter past any existing blocks in the outlined function. |
| 220 | // The outlined function has BlockId(0), parent may have more. Use parent's |
| 221 | // counter which is guaranteed to be > any block ID in the outlined function. |
| 222 | next_block_id_counter: self.next_block_id_counter, |
| 223 | // Scope counter must be consistent with scopes vec length |
| 224 | next_scope_id_counter: self.scopes.len() as u32, |
| 225 | next_mutable_range_id_counter: self.next_mutable_range_id_counter, |
| 226 | identifiers: self.identifiers.clone(), |
| 227 | types: self.types.clone(), |
| 228 | scopes: self.scopes.clone(), |
| 229 | functions: self.functions.clone(), |
| 230 | errors: CompilerError::new(), |
| 231 | fn_type, |
| 232 | output_mode: self.output_mode, |
| 233 | code: self.code.clone(), |
| 234 | filename: self.filename.clone(), |
| 235 | instrument_fn_name: self.instrument_fn_name.clone(), |
| 236 | instrument_gating_name: self.instrument_gating_name.clone(), |
| 237 | hook_guard_name: self.hook_guard_name.clone(), |
| 238 | renames: Vec::new(), |
| 239 | reference_node_ids: FxHashSet::default(), |
| 240 | hoisted_identifiers: FxHashSet::default(), |
| 241 | validate_preserve_existing_memoization_guarantees: self |
| 242 | .validate_preserve_existing_memoization_guarantees, |
| 243 | validate_no_set_state_in_render: self.validate_no_set_state_in_render, |
| 244 | enable_preserve_existing_memoization_guarantees: self |
| 245 | .enable_preserve_existing_memoization_guarantees, |
| 246 | globals: self.globals.clone(), |
| 247 | shapes: self.shapes.clone(), |
| 248 | module_types: self.module_types.clone(), |
| 249 | module_type_errors: self.module_type_errors.clone(), |
| 250 | config: self.config.clone(), |
| 251 | default_nonmutating_hook: self.default_nonmutating_hook.clone(), |
| 252 | default_mutating_hook: self.default_mutating_hook.clone(), |
| 253 | outlined_functions: Vec::new(), |
| 254 | uid_known_names: self.uid_known_names.clone(), |
| 255 | } |
| 256 | } |
| 257 | |
| 258 | pub fn next_block_id(&mut self) -> BlockId { |
| 259 | let id = BlockId(self.next_block_id_counter); |
| 260 | self.next_block_id_counter += 1; |
| 261 | id |
| 262 | } |
| 263 | |
| 264 | /// Create a new MutableRange with a unique ID. |
| 265 | /// Use this when creating a logically new range (not copying an existing one). |
| 266 | /// To copy a range preserving its identity, use `.clone()` instead. |
| 267 | pub fn new_mutable_range( |
| 268 | &mut self, |
| 269 | start: EvaluationOrder, |
| 270 | end: EvaluationOrder, |
| 271 | ) -> MutableRange { |
| 272 | let id = MutableRangeId(self.next_mutable_range_id_counter); |
| 273 | self.next_mutable_range_id_counter += 1; |
| 274 | MutableRange { id, start, end } |
| 275 | } |
| 276 | |
| 277 | /// Allocate a new Identifier in the arena with default values, |
| 278 | /// returns its IdentifierId. |
| 279 | pub fn next_identifier_id(&mut self) -> IdentifierId { |
| 280 | let id = IdentifierId(self.identifiers.len() as u32); |
| 281 | let type_id = self.make_type(); |
| 282 | let mutable_range = self.new_mutable_range(EvaluationOrder(0), EvaluationOrder(0)); |
| 283 | self.identifiers.push(Identifier { |
| 284 | id, |
| 285 | declaration_id: DeclarationId(id.0), |
| 286 | name: None, |
| 287 | mutable_range, |
| 288 | scope: None, |
| 289 | type_: type_id, |
| 290 | loc: None, |
| 291 | }); |
| 292 | id |
| 293 | } |
| 294 | |
| 295 | /// Allocate a new ReactiveScope in the arena, returns its ScopeId. |
| 296 | pub fn next_scope_id(&mut self) -> ScopeId { |
| 297 | let id = ScopeId(self.next_scope_id_counter); |
| 298 | self.next_scope_id_counter += 1; |
| 299 | let range = self.new_mutable_range(EvaluationOrder(0), EvaluationOrder(0)); |
| 300 | self.scopes.push(ReactiveScope { |
| 301 | id, |
| 302 | range, |
| 303 | dependencies: Vec::new(), |
| 304 | declarations: Vec::new(), |
| 305 | reassignments: Vec::new(), |
| 306 | early_return_value: None, |
| 307 | merged: Vec::new(), |
| 308 | loc: None, |
| 309 | }); |
| 310 | id |
| 311 | } |
| 312 | |
| 313 | /// Allocate a new Type in the arena, returns its TypeId. |
| 314 | pub fn next_type_id(&mut self) -> TypeId { |
| 315 | let id = TypeId(self.types.len() as u32); |
| 316 | self.types.push(Type::TypeVar { id }); |
| 317 | id |
| 318 | } |
| 319 | |
| 320 | /// Allocate a new Type (TypeVar) in the arena, returns its TypeId. |
| 321 | pub fn make_type(&mut self) -> TypeId { |
| 322 | self.next_type_id() |
| 323 | } |
| 324 | |
| 325 | pub fn add_function(&mut self, func: HirFunction) -> FunctionId { |
| 326 | let id = FunctionId(self.functions.len() as u32); |
| 327 | self.functions.push(func); |
| 328 | id |
| 329 | } |
| 330 | |
| 331 | pub fn record_error(&mut self, detail: CompilerErrorDetail) -> Result<(), CompilerError> { |
| 332 | if detail.category == ErrorCategory::Invariant { |
| 333 | let detail_clone = detail.clone(); |
| 334 | self.errors.push_error_detail(detail); |
| 335 | let mut err = CompilerError::new(); |
| 336 | err.push_error_detail(detail_clone); |
| 337 | return Err(err); |
| 338 | } |
| 339 | self.errors.push_error_detail(detail); |
| 340 | Ok(()) |
| 341 | } |
| 342 | |
| 343 | pub fn record_diagnostic(&mut self, diagnostic: CompilerDiagnostic) { |
| 344 | self.errors.push_diagnostic(diagnostic); |
| 345 | } |
| 346 | |
| 347 | pub fn has_errors(&self) -> bool { |
| 348 | self.errors.has_any_errors() |
| 349 | } |
| 350 | |
| 351 | pub fn error_count(&self) -> usize { |
| 352 | self.errors.details.len() |
| 353 | } |
| 354 | |
| 355 | /// Check if any recorded errors have Invariant category. |
| 356 | /// In TS, Invariant errors throw immediately from recordError(), |
| 357 | /// which aborts the current operation. |
| 358 | pub fn has_invariant_errors(&self) -> bool { |
| 359 | self.errors.has_invariant_errors() |
| 360 | } |
| 361 | |
| 362 | pub fn errors(&self) -> &CompilerError { |
| 363 | &self.errors |
| 364 | } |
| 365 | |
| 366 | pub fn take_errors(&mut self) -> CompilerError { |
| 367 | let mut errors = std::mem::take(&mut self.errors); |
| 368 | // Mark as not thrown — these are accumulated errors returned at the end |
| 369 | // of the pipeline, not errors thrown by a pass. |
| 370 | errors.is_thrown = false; |
| 371 | errors |
| 372 | } |
| 373 | |
| 374 | /// Take errors added after position `since_count`, leaving earlier errors in place. |
| 375 | /// Used to detect new errors added by a specific pass. |
| 376 | pub fn take_errors_since(&mut self, since_count: usize) -> CompilerError { |
| 377 | let mut taken = CompilerError::new(); |
| 378 | if self.errors.details.len() > since_count { |
| 379 | taken.details = self.errors.details.split_off(since_count); |
| 380 | } |
| 381 | taken |
| 382 | } |
| 383 | |
| 384 | /// Take only the Invariant errors, leaving non-Invariant errors in place. |
| 385 | /// In TS, Invariant errors throw as a separate CompilerError, so only |
| 386 | /// the Invariant error is surfaced. |
| 387 | pub fn take_invariant_errors(&mut self) -> CompilerError { |
| 388 | let mut invariant = CompilerError::new(); |
| 389 | let mut remaining = CompilerError::new(); |
| 390 | let old = std::mem::take(&mut self.errors); |
| 391 | for detail in old.details { |
| 392 | let is_invariant = match &detail { |
| 393 | react_compiler_diagnostics::CompilerErrorOrDiagnostic::Diagnostic(d) => { |
| 394 | d.category == react_compiler_diagnostics::ErrorCategory::Invariant |
| 395 | } |
| 396 | react_compiler_diagnostics::CompilerErrorOrDiagnostic::ErrorDetail(d) => { |
| 397 | d.category == react_compiler_diagnostics::ErrorCategory::Invariant |
| 398 | } |
| 399 | }; |
| 400 | if is_invariant { |
| 401 | invariant.details.push(detail); |
| 402 | } else { |
| 403 | remaining.details.push(detail); |
| 404 | } |
| 405 | } |
| 406 | self.errors = remaining; |
| 407 | invariant |
| 408 | } |
| 409 | |
| 410 | /// Check if any recorded errors have Todo category. |
| 411 | /// In TS, Todo errors throw immediately via CompilerError.throwTodo(). |
| 412 | pub fn has_todo_errors(&self) -> bool { |
| 413 | self.errors.details.iter().any(|d| match d { |
| 414 | react_compiler_diagnostics::CompilerErrorOrDiagnostic::Diagnostic(d) => { |
| 415 | d.category == react_compiler_diagnostics::ErrorCategory::Todo |
| 416 | } |
| 417 | react_compiler_diagnostics::CompilerErrorOrDiagnostic::ErrorDetail(d) => { |
| 418 | d.category == react_compiler_diagnostics::ErrorCategory::Todo |
| 419 | } |
| 420 | }) |
| 421 | } |
| 422 | |
| 423 | /// Take errors that would have been thrown in TS (Invariant and Todo), |
| 424 | /// leaving other accumulated errors in place. |
| 425 | pub fn take_thrown_errors(&mut self) -> CompilerError { |
| 426 | let mut thrown = CompilerError::new(); |
| 427 | let mut remaining = CompilerError::new(); |
| 428 | let old = std::mem::take(&mut self.errors); |
| 429 | for detail in old.details { |
| 430 | let is_thrown = match &detail { |
| 431 | react_compiler_diagnostics::CompilerErrorOrDiagnostic::Diagnostic(d) => { |
| 432 | d.category == react_compiler_diagnostics::ErrorCategory::Invariant |
| 433 | || d.category == react_compiler_diagnostics::ErrorCategory::Todo |
| 434 | } |
| 435 | react_compiler_diagnostics::CompilerErrorOrDiagnostic::ErrorDetail(d) => { |
| 436 | d.category == react_compiler_diagnostics::ErrorCategory::Invariant |
| 437 | || d.category == react_compiler_diagnostics::ErrorCategory::Todo |
| 438 | } |
| 439 | }; |
| 440 | if is_thrown { |
| 441 | thrown.details.push(detail); |
| 442 | } else { |
| 443 | remaining.details.push(detail); |
| 444 | } |
| 445 | } |
| 446 | self.errors = remaining; |
| 447 | thrown |
| 448 | } |
| 449 | |
| 450 | /// Check if a binding has been hoisted (via DeclareContext) already. |
| 451 | pub fn is_hoisted_identifier(&self, binding_id: u32) -> bool { |
| 452 | self.hoisted_identifiers.contains(&binding_id) |
| 453 | } |
| 454 | |
| 455 | /// Mark a binding as hoisted. |
| 456 | pub fn add_hoisted_identifier(&mut self, binding_id: u32) { |
| 457 | self.hoisted_identifiers.insert(binding_id); |
| 458 | } |
| 459 | |
| 460 | // ========================================================================= |
| 461 | // Type resolution methods (ported from Environment.ts) |
| 462 | // ========================================================================= |
| 463 | |
| 464 | /// Resolve a non-local binding to its type. Ported from TS `getGlobalDeclaration`. |
| 465 | /// |
| 466 | /// The `loc` parameter is used for error diagnostics when validating module type |
| 467 | /// configurations. Pass `None` if no source location is available. |
| 468 | pub fn get_global_declaration( |
| 469 | &mut self, |
| 470 | binding: &NonLocalBinding, |
| 471 | loc: Option<SourceLocation>, |
| 472 | ) -> Result<Option<Global>, CompilerError> { |
| 473 | match binding { |
| 474 | NonLocalBinding::ModuleLocal { name, .. } => { |
| 475 | if is_hook_name(name) { |
| 476 | Ok(Some(self.get_custom_hook_type())) |
| 477 | } else { |
| 478 | Ok(None) |
| 479 | } |
| 480 | } |
| 481 | NonLocalBinding::Global { name, .. } => { |
| 482 | if let Some(ty) = self.globals.get(name) { |
| 483 | return Ok(Some(ty.clone())); |
| 484 | } |
| 485 | if is_hook_name(name) { |
| 486 | Ok(Some(self.get_custom_hook_type())) |
| 487 | } else { |
| 488 | Ok(None) |
| 489 | } |
| 490 | } |
| 491 | NonLocalBinding::ImportSpecifier { |
| 492 | name, |
| 493 | module, |
| 494 | imported, |
| 495 | } => { |
| 496 | if self.is_known_react_module(module) { |
| 497 | if let Some(ty) = self.globals.get(imported) { |
| 498 | return Ok(Some(ty.clone())); |
| 499 | } |
| 500 | if is_hook_name(imported) || is_hook_name(name) { |
| 501 | return Ok(Some(self.get_custom_hook_type())); |
| 502 | } |
| 503 | return Ok(None); |
| 504 | } |
| 505 | |
| 506 | // Try module type provider. We resolve first, then do property |
| 507 | // lookup on the cloned result to avoid double-borrow of self. |
| 508 | let module_type = self.resolve_module_type(module); |
| 509 | |
| 510 | // Check for module type validation errors (hook-name vs hook-type mismatches) |
| 511 | if let Some(errors) = self.module_type_errors.remove(module.as_str()) { |
| 512 | if let Some(first_error) = errors.into_iter().next() { |
| 513 | self.record_error( |
| 514 | CompilerErrorDetail::new( |
| 515 | ErrorCategory::Config, |
| 516 | "Invalid type configuration for module", |
| 517 | ) |
| 518 | .with_description(format!("{}", first_error)) |
| 519 | .with_loc(loc), |
| 520 | )?; |
| 521 | } |
| 522 | } |
| 523 | |
| 524 | if let Some(module_type) = module_type { |
| 525 | if let Some(imported_type) = |
| 526 | Self::get_property_type_from_shapes(&self.shapes, &module_type, imported) |
| 527 | { |
| 528 | return Ok(Some(imported_type)); |
| 529 | } |
| 530 | } |
| 531 | |
| 532 | if is_hook_name(imported) || is_hook_name(name) { |
| 533 | Ok(Some(self.get_custom_hook_type())) |
| 534 | } else { |
| 535 | Ok(None) |
| 536 | } |
| 537 | } |
| 538 | NonLocalBinding::ImportDefault { name, module } |
| 539 | | NonLocalBinding::ImportNamespace { name, module } => { |
| 540 | let is_default = matches!(binding, NonLocalBinding::ImportDefault { .. }); |
| 541 | |
| 542 | if self.is_known_react_module(module) { |
| 543 | if let Some(ty) = self.globals.get(name) { |
| 544 | return Ok(Some(ty.clone())); |
| 545 | } |
| 546 | if is_hook_name(name) { |
| 547 | return Ok(Some(self.get_custom_hook_type())); |
| 548 | } |
| 549 | return Ok(None); |
| 550 | } |
| 551 | |
| 552 | let module_type = self.resolve_module_type(module); |
| 553 | |
| 554 | // Check for module type validation errors (hook-name vs hook-type mismatches) |
| 555 | if let Some(errors) = self.module_type_errors.remove(module.as_str()) { |
| 556 | if let Some(first_error) = errors.into_iter().next() { |
| 557 | self.record_error( |
| 558 | CompilerErrorDetail::new( |
| 559 | ErrorCategory::Config, |
| 560 | "Invalid type configuration for module", |
| 561 | ) |
| 562 | .with_description(format!("{}", first_error)) |
| 563 | .with_loc(loc), |
| 564 | )?; |
| 565 | } |
| 566 | } |
| 567 | |
| 568 | if let Some(module_type) = module_type { |
| 569 | let imported_type = if is_default { |
| 570 | Self::get_property_type_from_shapes(&self.shapes, &module_type, "default") |
| 571 | } else { |
| 572 | Some(module_type) |
| 573 | }; |
| 574 | if let Some(imported_type) = imported_type { |
| 575 | // Validate hook-name vs hook-type consistency for module name |
| 576 | let expect_hook = is_hook_name(module); |
| 577 | let is_hook = self |
| 578 | .get_hook_kind_for_type(&imported_type) |
| 579 | .ok() |
| 580 | .flatten() |
| 581 | .is_some(); |
| 582 | if expect_hook != is_hook { |
| 583 | self.record_error( |
| 584 | CompilerErrorDetail::new( |
| 585 | ErrorCategory::Config, |
| 586 | "Invalid type configuration for module", |
| 587 | ) |
| 588 | .with_description(format!( |
| 589 | "Expected type for `import ... from '{}'` {} based on the module name", |
| 590 | module, |
| 591 | if expect_hook { "to be a hook" } else { "not to be a hook" } |
| 592 | )) |
| 593 | .with_loc(loc), |
| 594 | )?; |
| 595 | } |
| 596 | return Ok(Some(imported_type)); |
| 597 | } |
| 598 | } |
| 599 | |
| 600 | if is_hook_name(name) { |
| 601 | Ok(Some(self.get_custom_hook_type())) |
| 602 | } else { |
| 603 | Ok(None) |
| 604 | } |
| 605 | } |
| 606 | } |
| 607 | } |
| 608 | |
| 609 | /// Static helper: resolve a property type using only the shapes registry. |
| 610 | /// Used internally to avoid double-borrow of `self`. Includes hook-name |
| 611 | /// fallback matching TS `getPropertyType`. |
| 612 | fn get_property_type_from_shapes( |
| 613 | shapes: &ShapeRegistry, |
| 614 | receiver: &Type, |
| 615 | property: &str, |
| 616 | ) -> Option<Type> { |
| 617 | let shape_id = match receiver { |
| 618 | Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(), |
| 619 | _ => None, |
| 620 | }; |
| 621 | if let Some(shape_id) = shape_id { |
| 622 | let shape = shapes.get(shape_id)?; |
| 623 | if let Some(ty) = shape.properties.get(property) { |
| 624 | return Some(ty.clone()); |
| 625 | } |
| 626 | if let Some(ty) = shape.properties.get("*") { |
| 627 | return Some(ty.clone()); |
| 628 | } |
| 629 | // Hook-name fallback: callers that need the custom hook type |
| 630 | // check is_hook_name after this returns None, which produces |
| 631 | // the same result as the TS getPropertyType hook-name fallback. |
| 632 | } |
| 633 | None |
| 634 | } |
| 635 | |
| 636 | /// Get the type of a named property on a receiver type. |
| 637 | /// Ported from TS `getPropertyType`. |
| 638 | pub fn get_property_type( |
| 639 | &mut self, |
| 640 | receiver: &Type, |
| 641 | property: &str, |
| 642 | ) -> Result<Option<Type>, CompilerDiagnostic> { |
| 643 | let shape_id = match receiver { |
| 644 | Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(), |
| 645 | _ => None, |
| 646 | }; |
| 647 | if let Some(shape_id) = shape_id { |
| 648 | let shape = self.shapes.get(shape_id).ok_or_else(|| { |
| 649 | CompilerDiagnostic::new( |
| 650 | ErrorCategory::Invariant, |
| 651 | format!( |
| 652 | "[HIR] Forget internal error: cannot resolve shape {}", |
| 653 | shape_id |
| 654 | ), |
| 655 | None, |
| 656 | ) |
| 657 | })?; |
| 658 | if let Some(ty) = shape.properties.get(property) { |
| 659 | return Ok(Some(ty.clone())); |
| 660 | } |
| 661 | // Fall through to wildcard |
| 662 | if let Some(ty) = shape.properties.get("*") { |
| 663 | return Ok(Some(ty.clone())); |
| 664 | } |
| 665 | // If property name looks like a hook, return custom hook type |
| 666 | if is_hook_name(property) { |
| 667 | return Ok(Some(self.get_custom_hook_type())); |
| 668 | } |
| 669 | return Ok(None); |
| 670 | } |
| 671 | // No shape ID — if property looks like a hook, return custom hook type |
| 672 | if is_hook_name(property) { |
| 673 | return Ok(Some(self.get_custom_hook_type())); |
| 674 | } |
| 675 | Ok(None) |
| 676 | } |
| 677 | |
| 678 | /// Get the type of a numeric property on a receiver type. |
| 679 | /// Ported from the numeric branch of TS `getPropertyType`. |
| 680 | pub fn get_property_type_numeric( |
| 681 | &self, |
| 682 | receiver: &Type, |
| 683 | ) -> Result<Option<Type>, CompilerDiagnostic> { |
| 684 | let shape_id = match receiver { |
| 685 | Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(), |
| 686 | _ => None, |
| 687 | }; |
| 688 | if let Some(shape_id) = shape_id { |
| 689 | let shape = self.shapes.get(shape_id).ok_or_else(|| { |
| 690 | CompilerDiagnostic::new( |
| 691 | ErrorCategory::Invariant, |
| 692 | format!( |
| 693 | "[HIR] Forget internal error: cannot resolve shape {}", |
| 694 | shape_id |
| 695 | ), |
| 696 | None, |
| 697 | ) |
| 698 | })?; |
| 699 | return Ok(shape.properties.get("*").cloned()); |
| 700 | } |
| 701 | Ok(None) |
| 702 | } |
| 703 | |
| 704 | /// Get the fallthrough (wildcard `*`) property type for computed property access. |
| 705 | /// Ported from TS `getFallthroughPropertyType`. |
| 706 | pub fn get_fallthrough_property_type( |
| 707 | &self, |
| 708 | receiver: &Type, |
| 709 | ) -> Result<Option<Type>, CompilerDiagnostic> { |
| 710 | let shape_id = match receiver { |
| 711 | Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(), |
| 712 | _ => None, |
| 713 | }; |
| 714 | if let Some(shape_id) = shape_id { |
| 715 | let shape = self.shapes.get(shape_id).ok_or_else(|| { |
| 716 | CompilerDiagnostic::new( |
| 717 | ErrorCategory::Invariant, |
| 718 | format!( |
| 719 | "[HIR] Forget internal error: cannot resolve shape {}", |
| 720 | shape_id |
| 721 | ), |
| 722 | None, |
| 723 | ) |
| 724 | })?; |
| 725 | return Ok(shape.properties.get("*").cloned()); |
| 726 | } |
| 727 | Ok(None) |
| 728 | } |
| 729 | |
| 730 | /// Get the function signature for a function type. |
| 731 | /// Ported from TS `getFunctionSignature`. |
| 732 | pub fn get_function_signature( |
| 733 | &self, |
| 734 | ty: &Type, |
| 735 | ) -> Result<Option<&FunctionSignature>, CompilerDiagnostic> { |
| 736 | let shape_id = match ty { |
| 737 | Type::Function { shape_id, .. } => shape_id.as_deref(), |
| 738 | _ => return Ok(None), |
| 739 | }; |
| 740 | if let Some(shape_id) = shape_id { |
| 741 | let shape = self.shapes.get(shape_id).ok_or_else(|| { |
| 742 | CompilerDiagnostic::new( |
| 743 | ErrorCategory::Invariant, |
| 744 | format!( |
| 745 | "[HIR] Forget internal error: cannot resolve shape {}", |
| 746 | shape_id |
| 747 | ), |
| 748 | None, |
| 749 | ) |
| 750 | })?; |
| 751 | return Ok(shape.function_type.as_ref()); |
| 752 | } |
| 753 | Ok(None) |
| 754 | } |
| 755 | |
| 756 | /// Get the hook kind for a type, if it represents a hook. |
| 757 | /// Ported from TS `getHookKindForType` in HIR.ts. |
| 758 | pub fn get_hook_kind_for_type( |
| 759 | &self, |
| 760 | ty: &Type, |
| 761 | ) -> Result<Option<&HookKind>, CompilerDiagnostic> { |
| 762 | Ok(self |
| 763 | .get_function_signature(ty)? |
| 764 | .and_then(|sig| sig.hook_kind.as_ref())) |
| 765 | } |
| 766 | |
| 767 | /// Resolve the module type provider for a given module name. |
| 768 | /// Caches results. Checks pre-resolved provider results first, then falls |
| 769 | /// back to `defaultModuleTypeProvider` (hardcoded). |
| 770 | fn resolve_module_type(&mut self, module_name: &str) -> Option<Global> { |
| 771 | if let Some(cached) = self.module_types.get(module_name) { |
| 772 | return cached.clone(); |
| 773 | } |
| 774 | |
| 775 | // Check pre-resolved provider results first, then fall back to default |
| 776 | let module_config = self |
| 777 | .config |
| 778 | .module_type_provider |
| 779 | .as_ref() |
| 780 | .and_then(|map| map.get(module_name).cloned()) |
| 781 | .or_else(|| default_module_type_provider(module_name)); |
| 782 | |
| 783 | let module_type = module_config.map(|config| { |
| 784 | let mut type_errors: Vec<String> = Vec::new(); |
| 785 | let ty = globals::install_type_config_with_errors( |
| 786 | &mut self.globals, |
| 787 | &mut self.shapes, |
| 788 | &config, |
| 789 | module_name, |
| 790 | (), |
| 791 | &mut type_errors, |
| 792 | ); |
| 793 | // Store errors for later reporting when the import is actually used |
| 794 | for err in type_errors { |
| 795 | self.module_type_errors |
| 796 | .entry(module_name.to_string()) |
| 797 | .or_default() |
| 798 | .push(err); |
| 799 | } |
| 800 | ty |
| 801 | }); |
| 802 | self.module_types |
| 803 | .insert(module_name.to_string(), module_type.clone()); |
| 804 | module_type |
| 805 | } |
| 806 | |
| 807 | fn is_known_react_module(&self, module_name: &str) -> bool { |
| 808 | let lower = module_name.to_lowercase(); |
| 809 | lower == "react" || lower == "react-dom" |
| 810 | } |
| 811 | |
| 812 | fn get_custom_hook_type(&mut self) -> Global { |
| 813 | if self.config.enable_assume_hooks_follow_rules_of_react { |
| 814 | if self.default_nonmutating_hook.is_none() { |
| 815 | self.default_nonmutating_hook = Some(default_nonmutating_hook(&mut self.shapes)); |
| 816 | } |
| 817 | self.default_nonmutating_hook.clone().unwrap() |
| 818 | } else { |
| 819 | if self.default_mutating_hook.is_none() { |
| 820 | self.default_mutating_hook = Some(default_mutating_hook(&mut self.shapes)); |
| 821 | } |
| 822 | self.default_mutating_hook.clone().unwrap() |
| 823 | } |
| 824 | } |
| 825 | |
| 826 | /// Public accessor for the custom hook type, used by InferTypes for |
| 827 | /// property resolution fallback when a property name looks like a hook. |
| 828 | pub fn get_custom_hook_type_opt(&mut self) -> Option<Global> { |
| 829 | Some(self.get_custom_hook_type()) |
| 830 | } |
| 831 | |
| 832 | /// Get a reference to the shapes registry. |
| 833 | pub fn shapes(&self) -> &ShapeRegistry { |
| 834 | &self.shapes |
| 835 | } |
| 836 | |
| 837 | /// Get a reference to the globals registry. |
| 838 | pub fn globals(&self) -> &GlobalRegistry { |
| 839 | &self.globals |
| 840 | } |
| 841 | |
| 842 | /// Generate a globally unique identifier name, analogous to TS |
| 843 | /// `generateGloballyUniqueIdentifierName` which delegates to Babel's |
| 844 | /// `scope.generateUidIdentifier`. Matches Babel's naming convention: |
| 845 | /// first name is `_<name>`, subsequent are `_<name>2`, `_<name>3`, etc. |
| 846 | /// Also applies Babel's `toIdentifier` sanitization on the input name. |
| 847 | /// |
| 848 | /// Like Babel's `generateUid`, checks for collisions against existing |
| 849 | /// bindings (source-level identifier names) and previously generated UIDs, |
| 850 | /// rather than using a blind counter. |
| 851 | pub fn generate_globally_unique_identifier_name(&mut self, name: Option<&str>) -> String { |
| 852 | let base = name.unwrap_or("temp"); |
| 853 | // Apply Babel's toIdentifier sanitization: |
| 854 | // 1. Replace non-identifier chars with '-' |
| 855 | // 2. Strip leading '-' and digits |
| 856 | // 3. CamelCase: replace '-' sequences + optional following char with uppercase of that char |
| 857 | let mut dashed = String::new(); |
| 858 | for c in base.chars() { |
| 859 | if c.is_ascii_alphanumeric() || c == '_' || c == '$' { |
| 860 | dashed.push(c); |
| 861 | } else { |
| 862 | dashed.push('-'); |
| 863 | } |
| 864 | } |
| 865 | // Strip leading dashes and digits |
| 866 | let trimmed = dashed.trim_start_matches(|c: char| c == '-' || c.is_ascii_digit()); |
| 867 | // CamelCase conversion: replace sequences of '-' followed by optional char with uppercase |
| 868 | let mut camel = String::new(); |
| 869 | let mut chars = trimmed.chars().peekable(); |
| 870 | while let Some(c) = chars.next() { |
| 871 | if c == '-' { |
| 872 | while chars.peek() == Some(&'-') { |
| 873 | chars.next(); |
| 874 | } |
| 875 | if let Some(next) = chars.next() { |
| 876 | for uc in next.to_uppercase() { |
| 877 | camel.push(uc); |
| 878 | } |
| 879 | } |
| 880 | } else { |
| 881 | camel.push(c); |
| 882 | } |
| 883 | } |
| 884 | if camel.is_empty() { |
| 885 | camel = "temp".to_string(); |
| 886 | } |
| 887 | // Strip leading '_' and trailing digits (Babel's generateUid behavior) |
| 888 | let stripped = camel.trim_start_matches('_'); |
| 889 | let stripped = stripped.trim_end_matches(|c: char| c.is_ascii_digit()); |
| 890 | let uid_base = if stripped.is_empty() { |
| 891 | "temp" |
| 892 | } else { |
| 893 | stripped |
| 894 | }; |
| 895 | |
| 896 | // Lazily build the set of known names from existing identifiers. |
| 897 | // This approximates Babel's hasBinding/hasGlobal/hasReference checks. |
| 898 | if self.uid_known_names.is_none() { |
| 899 | let mut known = FxHashSet::default(); |
| 900 | for id in &self.identifiers { |
| 901 | if let Some(name) = &id.name { |
| 902 | known.insert(name.value().to_string()); |
| 903 | } |
| 904 | } |
| 905 | self.uid_known_names = Some(known); |
| 906 | } |
| 907 | |
| 908 | // Find a name that doesn't collide, matching Babel's generateUid loop |
| 909 | let mut i = 1u32; |
| 910 | let uid = loop { |
| 911 | let candidate = if i == 1 { |
| 912 | format!("_{}", uid_base) |
| 913 | } else { |
| 914 | format!("_{}{}", uid_base, i) |
| 915 | }; |
| 916 | i += 1; |
| 917 | if !self.uid_known_names.as_ref().unwrap().contains(&candidate) { |
| 918 | break candidate; |
| 919 | } |
| 920 | }; |
| 921 | |
| 922 | // Register the generated name so subsequent calls see it |
| 923 | self.uid_known_names.as_mut().unwrap().insert(uid.clone()); |
| 924 | |
| 925 | uid |
| 926 | } |
| 927 | |
| 928 | /// Seed the UID known names set with external names (e.g. from ProgramContext). |
| 929 | /// This ensures UID generation avoids names generated by previous function compilations, |
| 930 | /// matching Babel's behavior where the program scope accumulates all generated UIDs. |
| 931 | pub fn seed_uid_known_names(&mut self, names: &FxHashSet<String>) { |
| 932 | match &mut self.uid_known_names { |
| 933 | Some(existing) => existing.extend(names.iter().cloned()), |
| 934 | None => self.uid_known_names = Some(names.clone()), |
| 935 | } |
| 936 | } |
| 937 | |
| 938 | /// Return the UID known names accumulated during this compilation. |
| 939 | pub fn take_uid_known_names(&mut self) -> Option<FxHashSet<String>> { |
| 940 | self.uid_known_names.take() |
| 941 | } |
| 942 | |
| 943 | /// Record an outlined function (extracted during outlineFunctions or outlineJSX). |
| 944 | /// Corresponds to TS `env.outlineFunction(fn, type)`. |
| 945 | pub fn outline_function(&mut self, func: HirFunction, fn_type: Option<ReactFunctionType>) { |
| 946 | self.outlined_functions |
| 947 | .push(OutlinedFunctionEntry { func, fn_type }); |
| 948 | } |
| 949 | |
| 950 | /// Get the outlined functions accumulated during compilation. |
| 951 | pub fn get_outlined_functions(&self) -> &[OutlinedFunctionEntry] { |
| 952 | &self.outlined_functions |
| 953 | } |
| 954 | |
| 955 | /// Take the outlined functions, leaving the vec empty. |
| 956 | pub fn take_outlined_functions(&mut self) -> Vec<OutlinedFunctionEntry> { |
| 957 | std::mem::take(&mut self.outlined_functions) |
| 958 | } |
| 959 | |
| 960 | /// Whether memoization is enabled for this compilation. |
| 961 | /// Ported from TS `get enableMemoization()` in Environment.ts. |
| 962 | /// Returns true for client/lint modes, false for SSR. |
| 963 | pub fn enable_memoization(&self) -> bool { |
| 964 | match self.output_mode { |
| 965 | OutputMode::Client | OutputMode::Lint => true, |
| 966 | OutputMode::Ssr => false, |
| 967 | } |
| 968 | } |
| 969 | |
| 970 | /// Whether validations are enabled for this compilation. |
| 971 | /// Ported from TS `get enableValidations()` in Environment.ts. |
| 972 | pub fn enable_validations(&self) -> bool { |
| 973 | match self.output_mode { |
| 974 | OutputMode::Client | OutputMode::Lint | OutputMode::Ssr => true, |
| 975 | } |
| 976 | } |
| 977 | |
| 978 | // ========================================================================= |
| 979 | // Name resolution helpers |
| 980 | // ========================================================================= |
| 981 | |
| 982 | /// Get the user-visible name for an identifier. |
| 983 | /// |
| 984 | /// First checks the identifier's own name. If None, looks for another |
| 985 | /// identifier with the same `declaration_id` that has a name. This handles |
| 986 | /// SSA identifiers that don't carry names but share a declaration_id with |
| 987 | /// the original named identifier from lowering. |
| 988 | /// |
| 989 | /// This is analogous to `identifierName` on Babel's SourceLocation, |
| 990 | /// which the parser sets on every identifier node. |
| 991 | pub fn identifier_name_for_id(&self, id: IdentifierId) -> Option<String> { |
| 992 | let ident = &self.identifiers[id.0 as usize]; |
| 993 | if let Some(name) = &ident.name { |
| 994 | return Some(name.value().to_string()); |
| 995 | } |
| 996 | // Fall back: find another identifier with the same declaration_id that has a Named name |
| 997 | let decl_id = ident.declaration_id; |
| 998 | for other in &self.identifiers { |
| 999 | if other.declaration_id == decl_id { |
| 1000 | if let Some(IdentifierName::Named(name)) = &other.name { |
| 1001 | return Some(name.clone()); |
| 1002 | } |
| 1003 | } |
| 1004 | } |
| 1005 | None |
| 1006 | } |
| 1007 | |
| 1008 | // ========================================================================= |
| 1009 | // ID-based type helper methods |
| 1010 | // ========================================================================= |
| 1011 | |
| 1012 | /// Check whether the function type for an identifier has a noAlias signature. |
| 1013 | /// Looks up the identifier's type and checks its function signature. |
| 1014 | pub fn has_no_alias_signature(&self, identifier_id: IdentifierId) -> bool { |
| 1015 | let ty = &self.types[self.identifiers[identifier_id.0 as usize].type_.0 as usize]; |
| 1016 | self.get_function_signature(ty) |
| 1017 | .ok() |
| 1018 | .flatten() |
| 1019 | .map_or(false, |sig| sig.no_alias) |
| 1020 | } |
| 1021 | |
| 1022 | /// Get the hook kind for an identifier, if its type represents a hook. |
| 1023 | /// Looks up the identifier's type and delegates to `get_hook_kind_for_type`. |
| 1024 | pub fn get_hook_kind_for_id( |
| 1025 | &self, |
| 1026 | identifier_id: IdentifierId, |
| 1027 | ) -> Result<Option<&HookKind>, CompilerDiagnostic> { |
| 1028 | let ty = &self.types[self.identifiers[identifier_id.0 as usize].type_.0 as usize]; |
| 1029 | self.get_hook_kind_for_type(ty) |
| 1030 | } |
| 1031 | } |
| 1032 | |
| 1033 | impl Default for Environment { |
| 1034 | fn default() -> Self { |
| 1035 | Self::new() |
| 1036 | } |
| 1037 | } |
| 1038 | |
| 1039 | /// Check if a name matches the React hook naming convention: `use[A-Z0-9]`. |
| 1040 | /// Ported from TS `isHookName` in Environment.ts. |
| 1041 | pub fn is_hook_name(name: &str) -> bool { |
| 1042 | if name.len() < 4 { |
| 1043 | return false; |
| 1044 | } |
| 1045 | if !name.starts_with("use") { |
| 1046 | return false; |
| 1047 | } |
| 1048 | let fourth_char = name.as_bytes()[3]; |
| 1049 | fourth_char.is_ascii_uppercase() || fourth_char.is_ascii_digit() |
| 1050 | } |
| 1051 | |
| 1052 | /// Returns true if the name follows React naming conventions (component or hook). |
| 1053 | /// Components start with an uppercase letter; hooks match `use[A-Z0-9]`. |
| 1054 | pub fn is_react_like_name(name: &str) -> bool { |
| 1055 | if name.is_empty() { |
| 1056 | return false; |
| 1057 | } |
| 1058 | let first_char = name.as_bytes()[0]; |
| 1059 | if first_char.is_ascii_uppercase() { |
| 1060 | return true; |
| 1061 | } |
| 1062 | is_hook_name(name) |
| 1063 | } |
| 1064 | |
| 1065 | #[cfg(test)] |
| 1066 | mod tests { |
| 1067 | use super::*; |
| 1068 | |
| 1069 | #[test] |
| 1070 | fn test_is_hook_name() { |
| 1071 | assert!(is_hook_name("useState")); |
| 1072 | assert!(is_hook_name("useEffect")); |
| 1073 | assert!(is_hook_name("useMyHook")); |
| 1074 | assert!(is_hook_name("use3rdParty")); |
| 1075 | assert!(!is_hook_name("use")); |
| 1076 | assert!(!is_hook_name("used")); |
| 1077 | assert!(!is_hook_name("useless")); |
| 1078 | assert!(!is_hook_name("User")); |
| 1079 | assert!(!is_hook_name("foo")); |
| 1080 | } |
| 1081 | |
| 1082 | #[test] |
| 1083 | fn test_environment_has_globals() { |
| 1084 | let env = Environment::new(); |
| 1085 | assert!(env.globals().contains_key("useState")); |
| 1086 | assert!(env.globals().contains_key("useEffect")); |
| 1087 | assert!(env.globals().contains_key("useRef")); |
| 1088 | assert!(env.globals().contains_key("Math")); |
| 1089 | assert!(env.globals().contains_key("console")); |
| 1090 | assert!(env.globals().contains_key("Array")); |
| 1091 | assert!(env.globals().contains_key("Object")); |
| 1092 | } |
| 1093 | |
| 1094 | #[test] |
| 1095 | fn test_get_property_type_array() { |
| 1096 | let mut env = Environment::new(); |
| 1097 | let array_type = Type::Object { |
| 1098 | shape_id: Some("BuiltInArray".to_string()), |
| 1099 | }; |
| 1100 | let map_type = env.get_property_type(&array_type, "map").unwrap(); |
| 1101 | assert!(map_type.is_some()); |
| 1102 | let push_type = env.get_property_type(&array_type, "push").unwrap(); |
| 1103 | assert!(push_type.is_some()); |
| 1104 | let nonexistent = env |
| 1105 | .get_property_type(&array_type, "nonExistentMethod") |
| 1106 | .unwrap(); |
| 1107 | assert!(nonexistent.is_none()); |
| 1108 | } |
| 1109 | |
| 1110 | #[test] |
| 1111 | fn test_get_function_signature() { |
| 1112 | let env = Environment::new(); |
| 1113 | let use_state_type = env.globals().get("useState").unwrap(); |
| 1114 | let sig = env.get_function_signature(use_state_type).unwrap(); |
| 1115 | assert!(sig.is_some()); |
| 1116 | let sig = sig.unwrap(); |
| 1117 | assert!(sig.hook_kind.is_some()); |
| 1118 | assert_eq!(sig.hook_kind.as_ref().unwrap(), &HookKind::UseState); |
| 1119 | } |
| 1120 | |
| 1121 | #[test] |
| 1122 | fn test_get_global_declaration() { |
| 1123 | let mut env = Environment::new(); |
| 1124 | // Global binding |
| 1125 | let binding = NonLocalBinding::Global { |
| 1126 | name: "Math".to_string(), |
| 1127 | }; |
| 1128 | let result = env.get_global_declaration(&binding, None).unwrap(); |
| 1129 | assert!(result.is_some()); |
| 1130 | |
| 1131 | // Import from react |
| 1132 | let binding = NonLocalBinding::ImportSpecifier { |
| 1133 | name: "useState".to_string(), |
| 1134 | module: "react".to_string(), |
| 1135 | imported: "useState".to_string(), |
| 1136 | }; |
| 1137 | let result = env.get_global_declaration(&binding, None).unwrap(); |
| 1138 | assert!(result.is_some()); |
| 1139 | |
| 1140 | // Unknown global |
| 1141 | let binding = NonLocalBinding::Global { |
| 1142 | name: "unknownThing".to_string(), |
| 1143 | }; |
| 1144 | let result = env.get_global_declaration(&binding, None).unwrap(); |
| 1145 | assert!(result.is_none()); |
| 1146 | |
| 1147 | // Hook-like name gets default hook type |
| 1148 | let binding = NonLocalBinding::Global { |
| 1149 | name: "useCustom".to_string(), |
| 1150 | }; |
| 1151 | let result = env.get_global_declaration(&binding, None).unwrap(); |
| 1152 | assert!(result.is_some()); |
| 1153 | } |
| 1154 | } |