Skip to main content

react_compiler_hir/
environment.rs

1use std::collections::HashMap;
2use std::collections::HashSet;
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: HashSet<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: HashSet<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: HashMap<String, Option<Global>>,
99    module_type_errors: HashMap<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<HashSet<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: HashMap<String, Option<Global>> = HashMap::new();
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: HashSet::new(),
194            hoisted_identifiers: HashSet::new(),
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: HashMap::new(),
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: HashSet::new(),
241            hoisted_identifiers: HashSet::new(),
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(&mut self, start: EvaluationOrder, end: EvaluationOrder) -> MutableRange {
269        let id = MutableRangeId(self.next_mutable_range_id_counter);
270        self.next_mutable_range_id_counter += 1;
271        MutableRange { id, start, end }
272    }
273
274    /// Allocate a new Identifier in the arena with default values,
275    /// returns its IdentifierId.
276    pub fn next_identifier_id(&mut self) -> IdentifierId {
277        let id = IdentifierId(self.identifiers.len() as u32);
278        let type_id = self.make_type();
279        let mutable_range = self.new_mutable_range(EvaluationOrder(0), EvaluationOrder(0));
280        self.identifiers.push(Identifier {
281            id,
282            declaration_id: DeclarationId(id.0),
283            name: None,
284            mutable_range,
285            scope: None,
286            type_: type_id,
287            loc: None,
288        });
289        id
290    }
291
292    /// Allocate a new ReactiveScope in the arena, returns its ScopeId.
293    pub fn next_scope_id(&mut self) -> ScopeId {
294        let id = ScopeId(self.next_scope_id_counter);
295        self.next_scope_id_counter += 1;
296        let range = self.new_mutable_range(EvaluationOrder(0), EvaluationOrder(0));
297        self.scopes.push(ReactiveScope {
298            id,
299            range,
300            dependencies: Vec::new(),
301            declarations: Vec::new(),
302            reassignments: Vec::new(),
303            early_return_value: None,
304            merged: Vec::new(),
305            loc: None,
306        });
307        id
308    }
309
310    /// Allocate a new Type in the arena, returns its TypeId.
311    pub fn next_type_id(&mut self) -> TypeId {
312        let id = TypeId(self.types.len() as u32);
313        self.types.push(Type::TypeVar { id });
314        id
315    }
316
317    /// Allocate a new Type (TypeVar) in the arena, returns its TypeId.
318    pub fn make_type(&mut self) -> TypeId {
319        self.next_type_id()
320    }
321
322    pub fn add_function(&mut self, func: HirFunction) -> FunctionId {
323        let id = FunctionId(self.functions.len() as u32);
324        self.functions.push(func);
325        id
326    }
327
328    pub fn record_error(&mut self, detail: CompilerErrorDetail) -> Result<(), CompilerError> {
329        if detail.category == ErrorCategory::Invariant {
330            let detail_clone = detail.clone();
331            self.errors.push_error_detail(detail);
332            let mut err = CompilerError::new();
333            err.push_error_detail(detail_clone);
334            return Err(err);
335        }
336        self.errors.push_error_detail(detail);
337        Ok(())
338    }
339
340    pub fn record_diagnostic(&mut self, diagnostic: CompilerDiagnostic) {
341        self.errors.push_diagnostic(diagnostic);
342    }
343
344    pub fn has_errors(&self) -> bool {
345        self.errors.has_any_errors()
346    }
347
348    pub fn error_count(&self) -> usize {
349        self.errors.details.len()
350    }
351
352    /// Check if any recorded errors have Invariant category.
353    /// In TS, Invariant errors throw immediately from recordError(),
354    /// which aborts the current operation.
355    pub fn has_invariant_errors(&self) -> bool {
356        self.errors.has_invariant_errors()
357    }
358
359    pub fn errors(&self) -> &CompilerError {
360        &self.errors
361    }
362
363    pub fn take_errors(&mut self) -> CompilerError {
364        let mut errors = std::mem::take(&mut self.errors);
365        // Mark as not thrown — these are accumulated errors returned at the end
366        // of the pipeline, not errors thrown by a pass.
367        errors.is_thrown = false;
368        errors
369    }
370
371    /// Take errors added after position `since_count`, leaving earlier errors in place.
372    /// Used to detect new errors added by a specific pass.
373    pub fn take_errors_since(&mut self, since_count: usize) -> CompilerError {
374        let mut taken = CompilerError::new();
375        if self.errors.details.len() > since_count {
376            taken.details = self.errors.details.split_off(since_count);
377        }
378        taken
379    }
380
381    /// Take only the Invariant errors, leaving non-Invariant errors in place.
382    /// In TS, Invariant errors throw as a separate CompilerError, so only
383    /// the Invariant error is surfaced.
384    pub fn take_invariant_errors(&mut self) -> CompilerError {
385        let mut invariant = CompilerError::new();
386        let mut remaining = CompilerError::new();
387        let old = std::mem::take(&mut self.errors);
388        for detail in old.details {
389            let is_invariant = match &detail {
390                react_compiler_diagnostics::CompilerErrorOrDiagnostic::Diagnostic(d) => {
391                    d.category == react_compiler_diagnostics::ErrorCategory::Invariant
392                }
393                react_compiler_diagnostics::CompilerErrorOrDiagnostic::ErrorDetail(d) => {
394                    d.category == react_compiler_diagnostics::ErrorCategory::Invariant
395                }
396            };
397            if is_invariant {
398                invariant.details.push(detail);
399            } else {
400                remaining.details.push(detail);
401            }
402        }
403        self.errors = remaining;
404        invariant
405    }
406
407    /// Check if any recorded errors have Todo category.
408    /// In TS, Todo errors throw immediately via CompilerError.throwTodo().
409    pub fn has_todo_errors(&self) -> bool {
410        self.errors.details.iter().any(|d| match d {
411            react_compiler_diagnostics::CompilerErrorOrDiagnostic::Diagnostic(d) => {
412                d.category == react_compiler_diagnostics::ErrorCategory::Todo
413            }
414            react_compiler_diagnostics::CompilerErrorOrDiagnostic::ErrorDetail(d) => {
415                d.category == react_compiler_diagnostics::ErrorCategory::Todo
416            }
417        })
418    }
419
420    /// Take errors that would have been thrown in TS (Invariant and Todo),
421    /// leaving other accumulated errors in place.
422    pub fn take_thrown_errors(&mut self) -> CompilerError {
423        let mut thrown = CompilerError::new();
424        let mut remaining = CompilerError::new();
425        let old = std::mem::take(&mut self.errors);
426        for detail in old.details {
427            let is_thrown = match &detail {
428                react_compiler_diagnostics::CompilerErrorOrDiagnostic::Diagnostic(d) => {
429                    d.category == react_compiler_diagnostics::ErrorCategory::Invariant
430                        || d.category == react_compiler_diagnostics::ErrorCategory::Todo
431                }
432                react_compiler_diagnostics::CompilerErrorOrDiagnostic::ErrorDetail(d) => {
433                    d.category == react_compiler_diagnostics::ErrorCategory::Invariant
434                        || d.category == react_compiler_diagnostics::ErrorCategory::Todo
435                }
436            };
437            if is_thrown {
438                thrown.details.push(detail);
439            } else {
440                remaining.details.push(detail);
441            }
442        }
443        self.errors = remaining;
444        thrown
445    }
446
447    /// Check if a binding has been hoisted (via DeclareContext) already.
448    pub fn is_hoisted_identifier(&self, binding_id: u32) -> bool {
449        self.hoisted_identifiers.contains(&binding_id)
450    }
451
452    /// Mark a binding as hoisted.
453    pub fn add_hoisted_identifier(&mut self, binding_id: u32) {
454        self.hoisted_identifiers.insert(binding_id);
455    }
456
457    // =========================================================================
458    // Type resolution methods (ported from Environment.ts)
459    // =========================================================================
460
461    /// Resolve a non-local binding to its type. Ported from TS `getGlobalDeclaration`.
462    ///
463    /// The `loc` parameter is used for error diagnostics when validating module type
464    /// configurations. Pass `None` if no source location is available.
465    pub fn get_global_declaration(
466        &mut self,
467        binding: &NonLocalBinding,
468        loc: Option<SourceLocation>,
469    ) -> Result<Option<Global>, CompilerError> {
470        match binding {
471            NonLocalBinding::ModuleLocal { name, .. } => {
472                if is_hook_name(name) {
473                    Ok(Some(self.get_custom_hook_type()))
474                } else {
475                    Ok(None)
476                }
477            }
478            NonLocalBinding::Global { name, .. } => {
479                if let Some(ty) = self.globals.get(name) {
480                    return Ok(Some(ty.clone()));
481                }
482                if is_hook_name(name) {
483                    Ok(Some(self.get_custom_hook_type()))
484                } else {
485                    Ok(None)
486                }
487            }
488            NonLocalBinding::ImportSpecifier {
489                name,
490                module,
491                imported,
492            } => {
493                if self.is_known_react_module(module) {
494                    if let Some(ty) = self.globals.get(imported) {
495                        return Ok(Some(ty.clone()));
496                    }
497                    if is_hook_name(imported) || is_hook_name(name) {
498                        return Ok(Some(self.get_custom_hook_type()));
499                    }
500                    return Ok(None);
501                }
502
503                // Try module type provider. We resolve first, then do property
504                // lookup on the cloned result to avoid double-borrow of self.
505                let module_type = self.resolve_module_type(module);
506
507                // Check for module type validation errors (hook-name vs hook-type mismatches)
508                if let Some(errors) = self.module_type_errors.remove(module.as_str()) {
509                    if let Some(first_error) = errors.into_iter().next() {
510                        self.record_error(
511                            CompilerErrorDetail::new(
512                                ErrorCategory::Config,
513                                "Invalid type configuration for module",
514                            )
515                            .with_description(format!("{}", first_error))
516                            .with_loc(loc),
517                        )?;
518                    }
519                }
520
521                if let Some(module_type) = module_type {
522                    if let Some(imported_type) =
523                        Self::get_property_type_from_shapes(&self.shapes, &module_type, imported)
524                    {
525                        return Ok(Some(imported_type));
526                    }
527                }
528
529                if is_hook_name(imported) || is_hook_name(name) {
530                    Ok(Some(self.get_custom_hook_type()))
531                } else {
532                    Ok(None)
533                }
534            }
535            NonLocalBinding::ImportDefault { name, module }
536            | NonLocalBinding::ImportNamespace { name, module } => {
537                let is_default = matches!(binding, NonLocalBinding::ImportDefault { .. });
538
539                if self.is_known_react_module(module) {
540                    if let Some(ty) = self.globals.get(name) {
541                        return Ok(Some(ty.clone()));
542                    }
543                    if is_hook_name(name) {
544                        return Ok(Some(self.get_custom_hook_type()));
545                    }
546                    return Ok(None);
547                }
548
549                let module_type = self.resolve_module_type(module);
550
551                // Check for module type validation errors (hook-name vs hook-type mismatches)
552                if let Some(errors) = self.module_type_errors.remove(module.as_str()) {
553                    if let Some(first_error) = errors.into_iter().next() {
554                        self.record_error(
555                            CompilerErrorDetail::new(
556                                ErrorCategory::Config,
557                                "Invalid type configuration for module",
558                            )
559                            .with_description(format!("{}", first_error))
560                            .with_loc(loc),
561                        )?;
562                    }
563                }
564
565                if let Some(module_type) = module_type {
566                    let imported_type = if is_default {
567                        Self::get_property_type_from_shapes(&self.shapes, &module_type, "default")
568                    } else {
569                        Some(module_type)
570                    };
571                    if let Some(imported_type) = imported_type {
572                        // Validate hook-name vs hook-type consistency for module name
573                        let expect_hook = is_hook_name(module);
574                        let is_hook = self
575                            .get_hook_kind_for_type(&imported_type)
576                            .ok()
577                            .flatten()
578                            .is_some();
579                        if expect_hook != is_hook {
580                            self.record_error(
581                                CompilerErrorDetail::new(
582                                    ErrorCategory::Config,
583                                    "Invalid type configuration for module",
584                                )
585                                .with_description(format!(
586                                    "Expected type for `import ... from '{}'` {} based on the module name",
587                                    module,
588                                    if expect_hook { "to be a hook" } else { "not to be a hook" }
589                                ))
590                                .with_loc(loc),
591                            )?;
592                        }
593                        return Ok(Some(imported_type));
594                    }
595                }
596
597                if is_hook_name(name) {
598                    Ok(Some(self.get_custom_hook_type()))
599                } else {
600                    Ok(None)
601                }
602            }
603        }
604    }
605
606    /// Static helper: resolve a property type using only the shapes registry.
607    /// Used internally to avoid double-borrow of `self`. Includes hook-name
608    /// fallback matching TS `getPropertyType`.
609    fn get_property_type_from_shapes(
610        shapes: &ShapeRegistry,
611        receiver: &Type,
612        property: &str,
613    ) -> Option<Type> {
614        let shape_id = match receiver {
615            Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(),
616            _ => None,
617        };
618        if let Some(shape_id) = shape_id {
619            let shape = shapes.get(shape_id)?;
620            if let Some(ty) = shape.properties.get(property) {
621                return Some(ty.clone());
622            }
623            if let Some(ty) = shape.properties.get("*") {
624                return Some(ty.clone());
625            }
626            // Hook-name fallback: callers that need the custom hook type
627            // check is_hook_name after this returns None, which produces
628            // the same result as the TS getPropertyType hook-name fallback.
629        }
630        None
631    }
632
633    /// Get the type of a named property on a receiver type.
634    /// Ported from TS `getPropertyType`.
635    pub fn get_property_type(
636        &mut self,
637        receiver: &Type,
638        property: &str,
639    ) -> Result<Option<Type>, CompilerDiagnostic> {
640        let shape_id = match receiver {
641            Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(),
642            _ => None,
643        };
644        if let Some(shape_id) = shape_id {
645            let shape = self.shapes.get(shape_id).ok_or_else(|| {
646                CompilerDiagnostic::new(
647                    ErrorCategory::Invariant,
648                    format!(
649                        "[HIR] Forget internal error: cannot resolve shape {}",
650                        shape_id
651                    ),
652                    None,
653                )
654            })?;
655            if let Some(ty) = shape.properties.get(property) {
656                return Ok(Some(ty.clone()));
657            }
658            // Fall through to wildcard
659            if let Some(ty) = shape.properties.get("*") {
660                return Ok(Some(ty.clone()));
661            }
662            // If property name looks like a hook, return custom hook type
663            if is_hook_name(property) {
664                return Ok(Some(self.get_custom_hook_type()));
665            }
666            return Ok(None);
667        }
668        // No shape ID — if property looks like a hook, return custom hook type
669        if is_hook_name(property) {
670            return Ok(Some(self.get_custom_hook_type()));
671        }
672        Ok(None)
673    }
674
675    /// Get the type of a numeric property on a receiver type.
676    /// Ported from the numeric branch of TS `getPropertyType`.
677    pub fn get_property_type_numeric(
678        &self,
679        receiver: &Type,
680    ) -> Result<Option<Type>, CompilerDiagnostic> {
681        let shape_id = match receiver {
682            Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(),
683            _ => None,
684        };
685        if let Some(shape_id) = shape_id {
686            let shape = self.shapes.get(shape_id).ok_or_else(|| {
687                CompilerDiagnostic::new(
688                    ErrorCategory::Invariant,
689                    format!(
690                        "[HIR] Forget internal error: cannot resolve shape {}",
691                        shape_id
692                    ),
693                    None,
694                )
695            })?;
696            return Ok(shape.properties.get("*").cloned());
697        }
698        Ok(None)
699    }
700
701    /// Get the fallthrough (wildcard `*`) property type for computed property access.
702    /// Ported from TS `getFallthroughPropertyType`.
703    pub fn get_fallthrough_property_type(
704        &self,
705        receiver: &Type,
706    ) -> Result<Option<Type>, CompilerDiagnostic> {
707        let shape_id = match receiver {
708            Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(),
709            _ => None,
710        };
711        if let Some(shape_id) = shape_id {
712            let shape = self.shapes.get(shape_id).ok_or_else(|| {
713                CompilerDiagnostic::new(
714                    ErrorCategory::Invariant,
715                    format!(
716                        "[HIR] Forget internal error: cannot resolve shape {}",
717                        shape_id
718                    ),
719                    None,
720                )
721            })?;
722            return Ok(shape.properties.get("*").cloned());
723        }
724        Ok(None)
725    }
726
727    /// Get the function signature for a function type.
728    /// Ported from TS `getFunctionSignature`.
729    pub fn get_function_signature(
730        &self,
731        ty: &Type,
732    ) -> Result<Option<&FunctionSignature>, CompilerDiagnostic> {
733        let shape_id = match ty {
734            Type::Function { shape_id, .. } => shape_id.as_deref(),
735            _ => return Ok(None),
736        };
737        if let Some(shape_id) = shape_id {
738            let shape = self.shapes.get(shape_id).ok_or_else(|| {
739                CompilerDiagnostic::new(
740                    ErrorCategory::Invariant,
741                    format!(
742                        "[HIR] Forget internal error: cannot resolve shape {}",
743                        shape_id
744                    ),
745                    None,
746                )
747            })?;
748            return Ok(shape.function_type.as_ref());
749        }
750        Ok(None)
751    }
752
753    /// Get the hook kind for a type, if it represents a hook.
754    /// Ported from TS `getHookKindForType` in HIR.ts.
755    pub fn get_hook_kind_for_type(
756        &self,
757        ty: &Type,
758    ) -> Result<Option<&HookKind>, CompilerDiagnostic> {
759        Ok(self
760            .get_function_signature(ty)?
761            .and_then(|sig| sig.hook_kind.as_ref()))
762    }
763
764    /// Resolve the module type provider for a given module name.
765    /// Caches results. Checks pre-resolved provider results first, then falls
766    /// back to `defaultModuleTypeProvider` (hardcoded).
767    fn resolve_module_type(&mut self, module_name: &str) -> Option<Global> {
768        if let Some(cached) = self.module_types.get(module_name) {
769            return cached.clone();
770        }
771
772        // Check pre-resolved provider results first, then fall back to default
773        let module_config = self
774            .config
775            .module_type_provider
776            .as_ref()
777            .and_then(|map| map.get(module_name).cloned())
778            .or_else(|| default_module_type_provider(module_name));
779
780        let module_type = module_config.map(|config| {
781            let mut type_errors: Vec<String> = Vec::new();
782            let ty = globals::install_type_config_with_errors(
783                &mut self.globals,
784                &mut self.shapes,
785                &config,
786                module_name,
787                (),
788                &mut type_errors,
789            );
790            // Store errors for later reporting when the import is actually used
791            for err in type_errors {
792                self.module_type_errors
793                    .entry(module_name.to_string())
794                    .or_default()
795                    .push(err);
796            }
797            ty
798        });
799        self.module_types
800            .insert(module_name.to_string(), module_type.clone());
801        module_type
802    }
803
804    fn is_known_react_module(&self, module_name: &str) -> bool {
805        let lower = module_name.to_lowercase();
806        lower == "react" || lower == "react-dom"
807    }
808
809    fn get_custom_hook_type(&mut self) -> Global {
810        if self.config.enable_assume_hooks_follow_rules_of_react {
811            if self.default_nonmutating_hook.is_none() {
812                self.default_nonmutating_hook = Some(default_nonmutating_hook(&mut self.shapes));
813            }
814            self.default_nonmutating_hook.clone().unwrap()
815        } else {
816            if self.default_mutating_hook.is_none() {
817                self.default_mutating_hook = Some(default_mutating_hook(&mut self.shapes));
818            }
819            self.default_mutating_hook.clone().unwrap()
820        }
821    }
822
823    /// Public accessor for the custom hook type, used by InferTypes for
824    /// property resolution fallback when a property name looks like a hook.
825    pub fn get_custom_hook_type_opt(&mut self) -> Option<Global> {
826        Some(self.get_custom_hook_type())
827    }
828
829    /// Get a reference to the shapes registry.
830    pub fn shapes(&self) -> &ShapeRegistry {
831        &self.shapes
832    }
833
834    /// Get a reference to the globals registry.
835    pub fn globals(&self) -> &GlobalRegistry {
836        &self.globals
837    }
838
839    /// Generate a globally unique identifier name, analogous to TS
840    /// `generateGloballyUniqueIdentifierName` which delegates to Babel's
841    /// `scope.generateUidIdentifier`. Matches Babel's naming convention:
842    /// first name is `_<name>`, subsequent are `_<name>2`, `_<name>3`, etc.
843    /// Also applies Babel's `toIdentifier` sanitization on the input name.
844    ///
845    /// Like Babel's `generateUid`, checks for collisions against existing
846    /// bindings (source-level identifier names) and previously generated UIDs,
847    /// rather than using a blind counter.
848    pub fn generate_globally_unique_identifier_name(&mut self, name: Option<&str>) -> String {
849        let base = name.unwrap_or("temp");
850        // Apply Babel's toIdentifier sanitization:
851        // 1. Replace non-identifier chars with '-'
852        // 2. Strip leading '-' and digits
853        // 3. CamelCase: replace '-' sequences + optional following char with uppercase of that char
854        let mut dashed = String::new();
855        for c in base.chars() {
856            if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
857                dashed.push(c);
858            } else {
859                dashed.push('-');
860            }
861        }
862        // Strip leading dashes and digits
863        let trimmed = dashed.trim_start_matches(|c: char| c == '-' || c.is_ascii_digit());
864        // CamelCase conversion: replace sequences of '-' followed by optional char with uppercase
865        let mut camel = String::new();
866        let mut chars = trimmed.chars().peekable();
867        while let Some(c) = chars.next() {
868            if c == '-' {
869                while chars.peek() == Some(&'-') {
870                    chars.next();
871                }
872                if let Some(next) = chars.next() {
873                    for uc in next.to_uppercase() {
874                        camel.push(uc);
875                    }
876                }
877            } else {
878                camel.push(c);
879            }
880        }
881        if camel.is_empty() {
882            camel = "temp".to_string();
883        }
884        // Strip leading '_' and trailing digits (Babel's generateUid behavior)
885        let stripped = camel.trim_start_matches('_');
886        let stripped = stripped.trim_end_matches(|c: char| c.is_ascii_digit());
887        let uid_base = if stripped.is_empty() {
888            "temp"
889        } else {
890            stripped
891        };
892
893        // Lazily build the set of known names from existing identifiers.
894        // This approximates Babel's hasBinding/hasGlobal/hasReference checks.
895        if self.uid_known_names.is_none() {
896            let mut known = HashSet::new();
897            for id in &self.identifiers {
898                if let Some(name) = &id.name {
899                    known.insert(name.value().to_string());
900                }
901            }
902            self.uid_known_names = Some(known);
903        }
904
905        // Find a name that doesn't collide, matching Babel's generateUid loop
906        let mut i = 1u32;
907        let uid = loop {
908            let candidate = if i == 1 {
909                format!("_{}", uid_base)
910            } else {
911                format!("_{}{}", uid_base, i)
912            };
913            i += 1;
914            if !self.uid_known_names.as_ref().unwrap().contains(&candidate) {
915                break candidate;
916            }
917        };
918
919        // Register the generated name so subsequent calls see it
920        self.uid_known_names.as_mut().unwrap().insert(uid.clone());
921
922        uid
923    }
924
925    /// Seed the UID known names set with external names (e.g. from ProgramContext).
926    /// This ensures UID generation avoids names generated by previous function compilations,
927    /// matching Babel's behavior where the program scope accumulates all generated UIDs.
928    pub fn seed_uid_known_names(&mut self, names: &HashSet<String>) {
929        match &mut self.uid_known_names {
930            Some(existing) => existing.extend(names.iter().cloned()),
931            None => self.uid_known_names = Some(names.clone()),
932        }
933    }
934
935    /// Return the UID known names accumulated during this compilation.
936    pub fn take_uid_known_names(&mut self) -> Option<HashSet<String>> {
937        self.uid_known_names.take()
938    }
939
940    /// Record an outlined function (extracted during outlineFunctions or outlineJSX).
941    /// Corresponds to TS `env.outlineFunction(fn, type)`.
942    pub fn outline_function(&mut self, func: HirFunction, fn_type: Option<ReactFunctionType>) {
943        self.outlined_functions
944            .push(OutlinedFunctionEntry { func, fn_type });
945    }
946
947    /// Get the outlined functions accumulated during compilation.
948    pub fn get_outlined_functions(&self) -> &[OutlinedFunctionEntry] {
949        &self.outlined_functions
950    }
951
952    /// Take the outlined functions, leaving the vec empty.
953    pub fn take_outlined_functions(&mut self) -> Vec<OutlinedFunctionEntry> {
954        std::mem::take(&mut self.outlined_functions)
955    }
956
957    /// Whether memoization is enabled for this compilation.
958    /// Ported from TS `get enableMemoization()` in Environment.ts.
959    /// Returns true for client/lint modes, false for SSR.
960    pub fn enable_memoization(&self) -> bool {
961        match self.output_mode {
962            OutputMode::Client | OutputMode::Lint => true,
963            OutputMode::Ssr => false,
964        }
965    }
966
967    /// Whether validations are enabled for this compilation.
968    /// Ported from TS `get enableValidations()` in Environment.ts.
969    pub fn enable_validations(&self) -> bool {
970        match self.output_mode {
971            OutputMode::Client | OutputMode::Lint | OutputMode::Ssr => true,
972        }
973    }
974
975    // =========================================================================
976    // Name resolution helpers
977    // =========================================================================
978
979    /// Get the user-visible name for an identifier.
980    ///
981    /// First checks the identifier's own name. If None, looks for another
982    /// identifier with the same `declaration_id` that has a name. This handles
983    /// SSA identifiers that don't carry names but share a declaration_id with
984    /// the original named identifier from lowering.
985    ///
986    /// This is analogous to `identifierName` on Babel's SourceLocation,
987    /// which the parser sets on every identifier node.
988    pub fn identifier_name_for_id(&self, id: IdentifierId) -> Option<String> {
989        let ident = &self.identifiers[id.0 as usize];
990        if let Some(name) = &ident.name {
991            return Some(name.value().to_string());
992        }
993        // Fall back: find another identifier with the same declaration_id that has a Named name
994        let decl_id = ident.declaration_id;
995        for other in &self.identifiers {
996            if other.declaration_id == decl_id {
997                if let Some(IdentifierName::Named(name)) = &other.name {
998                    return Some(name.clone());
999                }
1000            }
1001        }
1002        None
1003    }
1004
1005    // =========================================================================
1006    // ID-based type helper methods
1007    // =========================================================================
1008
1009    /// Check whether the function type for an identifier has a noAlias signature.
1010    /// Looks up the identifier's type and checks its function signature.
1011    pub fn has_no_alias_signature(&self, identifier_id: IdentifierId) -> bool {
1012        let ty = &self.types[self.identifiers[identifier_id.0 as usize].type_.0 as usize];
1013        self.get_function_signature(ty)
1014            .ok()
1015            .flatten()
1016            .map_or(false, |sig| sig.no_alias)
1017    }
1018
1019    /// Get the hook kind for an identifier, if its type represents a hook.
1020    /// Looks up the identifier's type and delegates to `get_hook_kind_for_type`.
1021    pub fn get_hook_kind_for_id(
1022        &self,
1023        identifier_id: IdentifierId,
1024    ) -> Result<Option<&HookKind>, CompilerDiagnostic> {
1025        let ty = &self.types[self.identifiers[identifier_id.0 as usize].type_.0 as usize];
1026        self.get_hook_kind_for_type(ty)
1027    }
1028}
1029
1030impl Default for Environment {
1031    fn default() -> Self {
1032        Self::new()
1033    }
1034}
1035
1036/// Check if a name matches the React hook naming convention: `use[A-Z0-9]`.
1037/// Ported from TS `isHookName` in Environment.ts.
1038pub fn is_hook_name(name: &str) -> bool {
1039    if name.len() < 4 {
1040        return false;
1041    }
1042    if !name.starts_with("use") {
1043        return false;
1044    }
1045    let fourth_char = name.as_bytes()[3];
1046    fourth_char.is_ascii_uppercase() || fourth_char.is_ascii_digit()
1047}
1048
1049/// Returns true if the name follows React naming conventions (component or hook).
1050/// Components start with an uppercase letter; hooks match `use[A-Z0-9]`.
1051pub fn is_react_like_name(name: &str) -> bool {
1052    if name.is_empty() {
1053        return false;
1054    }
1055    let first_char = name.as_bytes()[0];
1056    if first_char.is_ascii_uppercase() {
1057        return true;
1058    }
1059    is_hook_name(name)
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064    use super::*;
1065
1066    #[test]
1067    fn test_is_hook_name() {
1068        assert!(is_hook_name("useState"));
1069        assert!(is_hook_name("useEffect"));
1070        assert!(is_hook_name("useMyHook"));
1071        assert!(is_hook_name("use3rdParty"));
1072        assert!(!is_hook_name("use"));
1073        assert!(!is_hook_name("used"));
1074        assert!(!is_hook_name("useless"));
1075        assert!(!is_hook_name("User"));
1076        assert!(!is_hook_name("foo"));
1077    }
1078
1079    #[test]
1080    fn test_environment_has_globals() {
1081        let env = Environment::new();
1082        assert!(env.globals().contains_key("useState"));
1083        assert!(env.globals().contains_key("useEffect"));
1084        assert!(env.globals().contains_key("useRef"));
1085        assert!(env.globals().contains_key("Math"));
1086        assert!(env.globals().contains_key("console"));
1087        assert!(env.globals().contains_key("Array"));
1088        assert!(env.globals().contains_key("Object"));
1089    }
1090
1091    #[test]
1092    fn test_get_property_type_array() {
1093        let mut env = Environment::new();
1094        let array_type = Type::Object {
1095            shape_id: Some("BuiltInArray".to_string()),
1096        };
1097        let map_type = env.get_property_type(&array_type, "map").unwrap();
1098        assert!(map_type.is_some());
1099        let push_type = env.get_property_type(&array_type, "push").unwrap();
1100        assert!(push_type.is_some());
1101        let nonexistent = env
1102            .get_property_type(&array_type, "nonExistentMethod")
1103            .unwrap();
1104        assert!(nonexistent.is_none());
1105    }
1106
1107    #[test]
1108    fn test_get_function_signature() {
1109        let env = Environment::new();
1110        let use_state_type = env.globals().get("useState").unwrap();
1111        let sig = env.get_function_signature(use_state_type).unwrap();
1112        assert!(sig.is_some());
1113        let sig = sig.unwrap();
1114        assert!(sig.hook_kind.is_some());
1115        assert_eq!(sig.hook_kind.as_ref().unwrap(), &HookKind::UseState);
1116    }
1117
1118    #[test]
1119    fn test_get_global_declaration() {
1120        let mut env = Environment::new();
1121        // Global binding
1122        let binding = NonLocalBinding::Global {
1123            name: "Math".to_string(),
1124        };
1125        let result = env.get_global_declaration(&binding, None).unwrap();
1126        assert!(result.is_some());
1127
1128        // Import from react
1129        let binding = NonLocalBinding::ImportSpecifier {
1130            name: "useState".to_string(),
1131            module: "react".to_string(),
1132            imported: "useState".to_string(),
1133        };
1134        let result = env.get_global_declaration(&binding, None).unwrap();
1135        assert!(result.is_some());
1136
1137        // Unknown global
1138        let binding = NonLocalBinding::Global {
1139            name: "unknownThing".to_string(),
1140        };
1141        let result = env.get_global_declaration(&binding, None).unwrap();
1142        assert!(result.is_none());
1143
1144        // Hook-like name gets default hook type
1145        let binding = NonLocalBinding::Global {
1146            name: "useCustom".to_string(),
1147        };
1148        let result = env.get_global_declaration(&binding, None).unwrap();
1149        assert!(result.is_some());
1150    }
1151}