main
rs 1,154 lines 44.2 KB
Raw
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 }