Skip to main content

react_compiler_hir/
environment.rs

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