1use rustc_hash::{FxHashMap, FxHashSet};23use react_compiler_diagnostics::CompilerDiagnostic;4use react_compiler_diagnostics::CompilerError;5use react_compiler_diagnostics::CompilerErrorDetail;6use react_compiler_diagnostics::ErrorCategory;78use crate::default_module_type_provider::default_module_type_provider;9use crate::environment_config::EnvironmentConfig;10use crate::globals::Global;11use crate::globals::GlobalRegistry;12use crate::globals::{self};13use crate::object_shape::BUILT_IN_MIXED_READONLY_ID;14use crate::object_shape::FunctionSignature;15use crate::object_shape::HookKind;16use crate::object_shape::HookSignatureBuilder;17use crate::object_shape::ShapeRegistry;18use crate::object_shape::add_hook;19use crate::object_shape::default_mutating_hook;20use crate::object_shape::default_nonmutating_hook;21use crate::*;2223/// A variable rename from lowering: the binding at `declaration_start` position24/// was renamed from `original` to `renamed`.25#[derive(Debug, Clone)]26pub struct BindingRename {27 pub original: String,28 pub renamed: String,29 pub declaration_start: u32,30}3132/// Output mode for the compiler, mirrored from the entrypoint's CompilerOutputMode.33/// Stored on Environment so pipeline passes can access it.34#[derive(Debug, Clone, Copy, PartialEq, Eq)]35pub enum OutputMode {36 Ssr,37 Client,38 Lint,39}4041pub struct Environment {42 // Counters43 pub next_block_id_counter: u32,44 pub next_scope_id_counter: u32,45 next_mutable_range_id_counter: u32,4647 // Arenas (use direct field access for sliced borrows)48 pub identifiers: Vec<Identifier>,49 pub types: Vec<Type>,50 pub scopes: Vec<ReactiveScope>,51 pub functions: Vec<HirFunction>,5253 // Error accumulation54 pub errors: CompilerError,5556 // Function type classification (Component, Hook, Other)57 pub fn_type: ReactFunctionType,5859 // Output mode (Client, Ssr, Lint)60 pub output_mode: OutputMode,6162 // Source file code (for fast refresh hash computation)63 pub code: Option<String>,6465 // Source file name (for instrumentation)66 pub filename: Option<String>,6768 // Pre-resolved import local names for instrumentation/hook guards.69 // Set by the program-level code before compilation.70 pub instrument_fn_name: Option<String>,71 pub instrument_gating_name: Option<String>,72 pub hook_guard_name: Option<String>,7374 // Renames: tracks variable renames from lowering (original_name → new_name)75 // keyed by binding declaration position, for applying back to the Babel AST.76 pub renames: Vec<BindingRename>,7778 // Node IDs of identifiers that are actual references to bindings.79 // Used by codegen to filter type annotation renames — only rename identifiers80 // whose node_id is in this set (type labels like ObjectTypeIndexer params81 // are NOT in this set and should keep their original names).82 pub reference_node_ids: FxHashSet<u32>,8384 // Hoisted identifiers: tracks which bindings have already been hoisted85 // via DeclareContext to avoid duplicate hoisting.86 // Uses u32 to avoid depending on react_compiler_ast types.87 hoisted_identifiers: FxHashSet<u32>,8889 // Config flags for validation passes (kept for backwards compat with existing pipeline code)90 pub validate_preserve_existing_memoization_guarantees: bool,91 pub validate_no_set_state_in_render: bool,92 pub enable_preserve_existing_memoization_guarantees: bool,9394 // Type system registries95 globals: GlobalRegistry,96 pub shapes: ShapeRegistry,97 module_types: FxHashMap<String, Option<Global>>,98 module_type_errors: FxHashMap<String, Vec<String>>,99100 // Environment configuration (feature flags, custom hooks, etc.)101 pub config: EnvironmentConfig,102103 // Cached default hook types (lazily initialized)104 default_nonmutating_hook: Option<Global>,105 default_mutating_hook: Option<Global>,106107 // Outlined functions: functions extracted from the component during outlining passes108 outlined_functions: Vec<OutlinedFunctionEntry>,109110 // Known names for collision-aware UID generation. Lazily populated from111 // identifiers on first use, then updated with each generated name.112 // Matches Babel's generateUid behavior of checking hasBinding/hasReference.113 uid_known_names: Option<FxHashSet<String>>,114}115116/// An outlined function entry, stored on Environment during compilation.117/// Corresponds to TS `{ fn: HIRFunction, type: ReactFunctionType | null }`.118#[derive(Debug, Clone)]119pub struct OutlinedFunctionEntry {120 pub func: HirFunction,121 pub fn_type: Option<ReactFunctionType>,122}123124impl Environment {125 pub fn new() -> Self {126 Self::with_config(EnvironmentConfig::default())127 }128129 /// Create a new Environment with the given configuration.130 ///131 /// Initializes the shape and global registries, registers custom hooks,132 /// and sets up the module type cache.133 pub fn with_config(config: EnvironmentConfig) -> Self {134 let mut shapes = ShapeRegistry::with_base(globals::base_shapes());135 let mut global_registry = GlobalRegistry::with_base(globals::base_globals());136137 // Register custom hooks from config138 for (hook_name, hook) in &config.custom_hooks {139 // Don't overwrite existing globals (matches TS invariant)140 if global_registry.contains_key(hook_name) {141 continue;142 }143 let return_type = if hook.transitive_mixed_data {144 Type::Object {145 shape_id: Some(BUILT_IN_MIXED_READONLY_ID.to_string()),146 }147 } else {148 Type::Poly149 };150 let hook_type = add_hook(151 &mut shapes,152 HookSignatureBuilder {153 rest_param: Some(hook.effect_kind),154 return_type,155 return_value_kind: hook.value_kind,156 hook_kind: HookKind::Custom,157 no_alias: hook.no_alias,158 ..Default::default()159 },160 None,161 );162 global_registry.insert(hook_name.clone(), hook_type);163 }164165 // Register reanimated module type when enabled166 let mut module_types: FxHashMap<String, Option<Global>> = FxHashMap::default();167 if config.enable_custom_type_definition_for_reanimated {168 let reanimated_module_type = globals::get_reanimated_module_type(&mut shapes);169 module_types.insert(170 "react-native-reanimated".to_string(),171 Some(reanimated_module_type),172 );173 }174175 Self {176 next_block_id_counter: 0,177 next_scope_id_counter: 0,178 next_mutable_range_id_counter: 0,179 identifiers: Vec::new(),180 types: Vec::new(),181 scopes: Vec::new(),182 functions: Vec::new(),183 errors: CompilerError::new(),184 fn_type: ReactFunctionType::Other,185 output_mode: OutputMode::Client,186 code: None,187 filename: None,188 instrument_fn_name: None,189 instrument_gating_name: None,190 hook_guard_name: None,191 renames: Vec::new(),192 reference_node_ids: FxHashSet::default(),193 hoisted_identifiers: FxHashSet::default(),194 validate_preserve_existing_memoization_guarantees: config195 .validate_preserve_existing_memoization_guarantees,196 validate_no_set_state_in_render: config.validate_no_set_state_in_render,197 enable_preserve_existing_memoization_guarantees: config198 .enable_preserve_existing_memoization_guarantees,199 globals: global_registry,200 shapes,201 module_types,202 module_type_errors: FxHashMap::default(),203 default_nonmutating_hook: None,204 default_mutating_hook: None,205 outlined_functions: Vec::new(),206 uid_known_names: None,207 config,208 }209 }210211 /// Create a child Environment for compiling an outlined function.212 ///213 /// The child shares the same config, globals, and shapes, and receives copies of214 /// all arenas (identifiers, types, scopes, functions) so that references from215 /// the outlined HIR remain valid. Block/scope counters start past the cloned216 /// data to avoid ID conflicts.217 pub fn for_outlined_fn(&self, fn_type: ReactFunctionType) -> Self {218 Self {219 // Start block counter past any existing blocks in the outlined function.220 // The outlined function has BlockId(0), parent may have more. Use parent's221 // counter which is guaranteed to be > any block ID in the outlined function.222 next_block_id_counter: self.next_block_id_counter,223 // Scope counter must be consistent with scopes vec length224 next_scope_id_counter: self.scopes.len() as u32,225 next_mutable_range_id_counter: self.next_mutable_range_id_counter,226 identifiers: self.identifiers.clone(),227 types: self.types.clone(),228 scopes: self.scopes.clone(),229 functions: self.functions.clone(),230 errors: CompilerError::new(),231 fn_type,232 output_mode: self.output_mode,233 code: self.code.clone(),234 filename: self.filename.clone(),235 instrument_fn_name: self.instrument_fn_name.clone(),236 instrument_gating_name: self.instrument_gating_name.clone(),237 hook_guard_name: self.hook_guard_name.clone(),238 renames: Vec::new(),239 reference_node_ids: FxHashSet::default(),240 hoisted_identifiers: FxHashSet::default(),241 validate_preserve_existing_memoization_guarantees: self242 .validate_preserve_existing_memoization_guarantees,243 validate_no_set_state_in_render: self.validate_no_set_state_in_render,244 enable_preserve_existing_memoization_guarantees: self245 .enable_preserve_existing_memoization_guarantees,246 globals: self.globals.clone(),247 shapes: self.shapes.clone(),248 module_types: self.module_types.clone(),249 module_type_errors: self.module_type_errors.clone(),250 config: self.config.clone(),251 default_nonmutating_hook: self.default_nonmutating_hook.clone(),252 default_mutating_hook: self.default_mutating_hook.clone(),253 outlined_functions: Vec::new(),254 uid_known_names: self.uid_known_names.clone(),255 }256 }257258 pub fn next_block_id(&mut self) -> BlockId {259 let id = BlockId(self.next_block_id_counter);260 self.next_block_id_counter += 1;261 id262 }263264 /// Create a new MutableRange with a unique ID.265 /// Use this when creating a logically new range (not copying an existing one).266 /// To copy a range preserving its identity, use `.clone()` instead.267 pub fn new_mutable_range(268 &mut self,269 start: EvaluationOrder,270 end: EvaluationOrder,271 ) -> MutableRange {272 let id = MutableRangeId(self.next_mutable_range_id_counter);273 self.next_mutable_range_id_counter += 1;274 MutableRange { id, start, end }275 }276277 /// Allocate a new Identifier in the arena with default values,278 /// returns its IdentifierId.279 pub fn next_identifier_id(&mut self) -> IdentifierId {280 let id = IdentifierId(self.identifiers.len() as u32);281 let type_id = self.make_type();282 let mutable_range = self.new_mutable_range(EvaluationOrder(0), EvaluationOrder(0));283 self.identifiers.push(Identifier {284 id,285 declaration_id: DeclarationId(id.0),286 name: None,287 mutable_range,288 scope: None,289 type_: type_id,290 loc: None,291 });292 id293 }294295 /// Allocate a new ReactiveScope in the arena, returns its ScopeId.296 pub fn next_scope_id(&mut self) -> ScopeId {297 let id = ScopeId(self.next_scope_id_counter);298 self.next_scope_id_counter += 1;299 let range = self.new_mutable_range(EvaluationOrder(0), EvaluationOrder(0));300 self.scopes.push(ReactiveScope {301 id,302 range,303 dependencies: Vec::new(),304 declarations: Vec::new(),305 reassignments: Vec::new(),306 early_return_value: None,307 merged: Vec::new(),308 loc: None,309 });310 id311 }312313 /// Allocate a new Type in the arena, returns its TypeId.314 pub fn next_type_id(&mut self) -> TypeId {315 let id = TypeId(self.types.len() as u32);316 self.types.push(Type::TypeVar { id });317 id318 }319320 /// Allocate a new Type (TypeVar) in the arena, returns its TypeId.321 pub fn make_type(&mut self) -> TypeId {322 self.next_type_id()323 }324325 pub fn add_function(&mut self, func: HirFunction) -> FunctionId {326 let id = FunctionId(self.functions.len() as u32);327 self.functions.push(func);328 id329 }330331 pub fn record_error(&mut self, detail: CompilerErrorDetail) -> Result<(), CompilerError> {332 if detail.category == ErrorCategory::Invariant {333 let detail_clone = detail.clone();334 self.errors.push_error_detail(detail);335 let mut err = CompilerError::new();336 err.push_error_detail(detail_clone);337 return Err(err);338 }339 self.errors.push_error_detail(detail);340 Ok(())341 }342343 pub fn record_diagnostic(&mut self, diagnostic: CompilerDiagnostic) {344 self.errors.push_diagnostic(diagnostic);345 }346347 pub fn has_errors(&self) -> bool {348 self.errors.has_any_errors()349 }350351 pub fn error_count(&self) -> usize {352 self.errors.details.len()353 }354355 /// Check if any recorded errors have Invariant category.356 /// In TS, Invariant errors throw immediately from recordError(),357 /// which aborts the current operation.358 pub fn has_invariant_errors(&self) -> bool {359 self.errors.has_invariant_errors()360 }361362 pub fn errors(&self) -> &CompilerError {363 &self.errors364 }365366 pub fn take_errors(&mut self) -> CompilerError {367 let mut errors = std::mem::take(&mut self.errors);368 // Mark as not thrown — these are accumulated errors returned at the end369 // of the pipeline, not errors thrown by a pass.370 errors.is_thrown = false;371 errors372 }373374 /// Take errors added after position `since_count`, leaving earlier errors in place.375 /// Used to detect new errors added by a specific pass.376 pub fn take_errors_since(&mut self, since_count: usize) -> CompilerError {377 let mut taken = CompilerError::new();378 if self.errors.details.len() > since_count {379 taken.details = self.errors.details.split_off(since_count);380 }381 taken382 }383384 /// Take only the Invariant errors, leaving non-Invariant errors in place.385 /// In TS, Invariant errors throw as a separate CompilerError, so only386 /// the Invariant error is surfaced.387 pub fn take_invariant_errors(&mut self) -> CompilerError {388 let mut invariant = CompilerError::new();389 let mut remaining = CompilerError::new();390 let old = std::mem::take(&mut self.errors);391 for detail in old.details {392 let is_invariant = match &detail {393 react_compiler_diagnostics::CompilerErrorOrDiagnostic::Diagnostic(d) => {394 d.category == react_compiler_diagnostics::ErrorCategory::Invariant395 }396 react_compiler_diagnostics::CompilerErrorOrDiagnostic::ErrorDetail(d) => {397 d.category == react_compiler_diagnostics::ErrorCategory::Invariant398 }399 };400 if is_invariant {401 invariant.details.push(detail);402 } else {403 remaining.details.push(detail);404 }405 }406 self.errors = remaining;407 invariant408 }409410 /// Check if any recorded errors have Todo category.411 /// In TS, Todo errors throw immediately via CompilerError.throwTodo().412 pub fn has_todo_errors(&self) -> bool {413 self.errors.details.iter().any(|d| match d {414 react_compiler_diagnostics::CompilerErrorOrDiagnostic::Diagnostic(d) => {415 d.category == react_compiler_diagnostics::ErrorCategory::Todo416 }417 react_compiler_diagnostics::CompilerErrorOrDiagnostic::ErrorDetail(d) => {418 d.category == react_compiler_diagnostics::ErrorCategory::Todo419 }420 })421 }422423 /// Take errors that would have been thrown in TS (Invariant and Todo),424 /// leaving other accumulated errors in place.425 pub fn take_thrown_errors(&mut self) -> CompilerError {426 let mut thrown = CompilerError::new();427 let mut remaining = CompilerError::new();428 let old = std::mem::take(&mut self.errors);429 for detail in old.details {430 let is_thrown = match &detail {431 react_compiler_diagnostics::CompilerErrorOrDiagnostic::Diagnostic(d) => {432 d.category == react_compiler_diagnostics::ErrorCategory::Invariant433 || d.category == react_compiler_diagnostics::ErrorCategory::Todo434 }435 react_compiler_diagnostics::CompilerErrorOrDiagnostic::ErrorDetail(d) => {436 d.category == react_compiler_diagnostics::ErrorCategory::Invariant437 || d.category == react_compiler_diagnostics::ErrorCategory::Todo438 }439 };440 if is_thrown {441 thrown.details.push(detail);442 } else {443 remaining.details.push(detail);444 }445 }446 self.errors = remaining;447 thrown448 }449450 /// Check if a binding has been hoisted (via DeclareContext) already.451 pub fn is_hoisted_identifier(&self, binding_id: u32) -> bool {452 self.hoisted_identifiers.contains(&binding_id)453 }454455 /// Mark a binding as hoisted.456 pub fn add_hoisted_identifier(&mut self, binding_id: u32) {457 self.hoisted_identifiers.insert(binding_id);458 }459460 // =========================================================================461 // Type resolution methods (ported from Environment.ts)462 // =========================================================================463464 /// Resolve a non-local binding to its type. Ported from TS `getGlobalDeclaration`.465 ///466 /// The `loc` parameter is used for error diagnostics when validating module type467 /// configurations. Pass `None` if no source location is available.468 pub fn get_global_declaration(469 &mut self,470 binding: &NonLocalBinding,471 loc: Option<SourceLocation>,472 ) -> Result<Option<Global>, CompilerError> {473 match binding {474 NonLocalBinding::ModuleLocal { name, .. } => {475 if is_hook_name(name) {476 Ok(Some(self.get_custom_hook_type()))477 } else {478 Ok(None)479 }480 }481 NonLocalBinding::Global { name, .. } => {482 if let Some(ty) = self.globals.get(name) {483 return Ok(Some(ty.clone()));484 }485 if is_hook_name(name) {486 Ok(Some(self.get_custom_hook_type()))487 } else {488 Ok(None)489 }490 }491 NonLocalBinding::ImportSpecifier {492 name,493 module,494 imported,495 } => {496 if self.is_known_react_module(module) {497 if let Some(ty) = self.globals.get(imported) {498 return Ok(Some(ty.clone()));499 }500 if is_hook_name(imported) || is_hook_name(name) {501 return Ok(Some(self.get_custom_hook_type()));502 }503 return Ok(None);504 }505506 // Try module type provider. We resolve first, then do property507 // lookup on the cloned result to avoid double-borrow of self.508 let module_type = self.resolve_module_type(module);509510 // Check for module type validation errors (hook-name vs hook-type mismatches)511 if let Some(errors) = self.module_type_errors.remove(module.as_str()) {512 if let Some(first_error) = errors.into_iter().next() {513 self.record_error(514 CompilerErrorDetail::new(515 ErrorCategory::Config,516 "Invalid type configuration for module",517 )518 .with_description(format!("{}", first_error))519 .with_loc(loc),520 )?;521 }522 }523524 if let Some(module_type) = module_type {525 if let Some(imported_type) =526 Self::get_property_type_from_shapes(&self.shapes, &module_type, imported)527 {528 return Ok(Some(imported_type));529 }530 }531532 if is_hook_name(imported) || is_hook_name(name) {533 Ok(Some(self.get_custom_hook_type()))534 } else {535 Ok(None)536 }537 }538 NonLocalBinding::ImportDefault { name, module }539 | NonLocalBinding::ImportNamespace { name, module } => {540 let is_default = matches!(binding, NonLocalBinding::ImportDefault { .. });541542 if self.is_known_react_module(module) {543 if let Some(ty) = self.globals.get(name) {544 return Ok(Some(ty.clone()));545 }546 if is_hook_name(name) {547 return Ok(Some(self.get_custom_hook_type()));548 }549 return Ok(None);550 }551552 let module_type = self.resolve_module_type(module);553554 // Check for module type validation errors (hook-name vs hook-type mismatches)555 if let Some(errors) = self.module_type_errors.remove(module.as_str()) {556 if let Some(first_error) = errors.into_iter().next() {557 self.record_error(558 CompilerErrorDetail::new(559 ErrorCategory::Config,560 "Invalid type configuration for module",561 )562 .with_description(format!("{}", first_error))563 .with_loc(loc),564 )?;565 }566 }567568 if let Some(module_type) = module_type {569 let imported_type = if is_default {570 Self::get_property_type_from_shapes(&self.shapes, &module_type, "default")571 } else {572 Some(module_type)573 };574 if let Some(imported_type) = imported_type {575 // Validate hook-name vs hook-type consistency for module name576 let expect_hook = is_hook_name(module);577 let is_hook = self578 .get_hook_kind_for_type(&imported_type)579 .ok()580 .flatten()581 .is_some();582 if expect_hook != is_hook {583 self.record_error(584 CompilerErrorDetail::new(585 ErrorCategory::Config,586 "Invalid type configuration for module",587 )588 .with_description(format!(589 "Expected type for `import ... from '{}'` {} based on the module name",590 module,591 if expect_hook { "to be a hook" } else { "not to be a hook" }592 ))593 .with_loc(loc),594 )?;595 }596 return Ok(Some(imported_type));597 }598 }599600 if is_hook_name(name) {601 Ok(Some(self.get_custom_hook_type()))602 } else {603 Ok(None)604 }605 }606 }607 }608609 /// Static helper: resolve a property type using only the shapes registry.610 /// Used internally to avoid double-borrow of `self`. Includes hook-name611 /// fallback matching TS `getPropertyType`.612 fn get_property_type_from_shapes(613 shapes: &ShapeRegistry,614 receiver: &Type,615 property: &str,616 ) -> Option<Type> {617 let shape_id = match receiver {618 Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(),619 _ => None,620 };621 if let Some(shape_id) = shape_id {622 let shape = shapes.get(shape_id)?;623 if let Some(ty) = shape.properties.get(property) {624 return Some(ty.clone());625 }626 if let Some(ty) = shape.properties.get("*") {627 return Some(ty.clone());628 }629 // Hook-name fallback: callers that need the custom hook type630 // check is_hook_name after this returns None, which produces631 // the same result as the TS getPropertyType hook-name fallback.632 }633 None634 }635636 /// Get the type of a named property on a receiver type.637 /// Ported from TS `getPropertyType`.638 pub fn get_property_type(639 &mut self,640 receiver: &Type,641 property: &str,642 ) -> Result<Option<Type>, CompilerDiagnostic> {643 let shape_id = match receiver {644 Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(),645 _ => None,646 };647 if let Some(shape_id) = shape_id {648 let shape = self.shapes.get(shape_id).ok_or_else(|| {649 CompilerDiagnostic::new(650 ErrorCategory::Invariant,651 format!(652 "[HIR] Forget internal error: cannot resolve shape {}",653 shape_id654 ),655 None,656 )657 })?;658 if let Some(ty) = shape.properties.get(property) {659 return Ok(Some(ty.clone()));660 }661 // Fall through to wildcard662 if let Some(ty) = shape.properties.get("*") {663 return Ok(Some(ty.clone()));664 }665 // If property name looks like a hook, return custom hook type666 if is_hook_name(property) {667 return Ok(Some(self.get_custom_hook_type()));668 }669 return Ok(None);670 }671 // No shape ID — if property looks like a hook, return custom hook type672 if is_hook_name(property) {673 return Ok(Some(self.get_custom_hook_type()));674 }675 Ok(None)676 }677678 /// Get the type of a numeric property on a receiver type.679 /// Ported from the numeric branch of TS `getPropertyType`.680 pub fn get_property_type_numeric(681 &self,682 receiver: &Type,683 ) -> Result<Option<Type>, CompilerDiagnostic> {684 let shape_id = match receiver {685 Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(),686 _ => None,687 };688 if let Some(shape_id) = shape_id {689 let shape = self.shapes.get(shape_id).ok_or_else(|| {690 CompilerDiagnostic::new(691 ErrorCategory::Invariant,692 format!(693 "[HIR] Forget internal error: cannot resolve shape {}",694 shape_id695 ),696 None,697 )698 })?;699 return Ok(shape.properties.get("*").cloned());700 }701 Ok(None)702 }703704 /// Get the fallthrough (wildcard `*`) property type for computed property access.705 /// Ported from TS `getFallthroughPropertyType`.706 pub fn get_fallthrough_property_type(707 &self,708 receiver: &Type,709 ) -> Result<Option<Type>, CompilerDiagnostic> {710 let shape_id = match receiver {711 Type::Object { shape_id } | Type::Function { shape_id, .. } => shape_id.as_deref(),712 _ => None,713 };714 if let Some(shape_id) = shape_id {715 let shape = self.shapes.get(shape_id).ok_or_else(|| {716 CompilerDiagnostic::new(717 ErrorCategory::Invariant,718 format!(719 "[HIR] Forget internal error: cannot resolve shape {}",720 shape_id721 ),722 None,723 )724 })?;725 return Ok(shape.properties.get("*").cloned());726 }727 Ok(None)728 }729730 /// Get the function signature for a function type.731 /// Ported from TS `getFunctionSignature`.732 pub fn get_function_signature(733 &self,734 ty: &Type,735 ) -> Result<Option<&FunctionSignature>, CompilerDiagnostic> {736 let shape_id = match ty {737 Type::Function { shape_id, .. } => shape_id.as_deref(),738 _ => return Ok(None),739 };740 if let Some(shape_id) = shape_id {741 let shape = self.shapes.get(shape_id).ok_or_else(|| {742 CompilerDiagnostic::new(743 ErrorCategory::Invariant,744 format!(745 "[HIR] Forget internal error: cannot resolve shape {}",746 shape_id747 ),748 None,749 )750 })?;751 return Ok(shape.function_type.as_ref());752 }753 Ok(None)754 }755756 /// Get the hook kind for a type, if it represents a hook.757 /// Ported from TS `getHookKindForType` in HIR.ts.758 pub fn get_hook_kind_for_type(759 &self,760 ty: &Type,761 ) -> Result<Option<&HookKind>, CompilerDiagnostic> {762 Ok(self763 .get_function_signature(ty)?764 .and_then(|sig| sig.hook_kind.as_ref()))765 }766767 /// Resolve the module type provider for a given module name.768 /// Caches results. Checks pre-resolved provider results first, then falls769 /// back to `defaultModuleTypeProvider` (hardcoded).770 fn resolve_module_type(&mut self, module_name: &str) -> Option<Global> {771 if let Some(cached) = self.module_types.get(module_name) {772 return cached.clone();773 }774775 // Check pre-resolved provider results first, then fall back to default776 let module_config = self777 .config778 .module_type_provider779 .as_ref()780 .and_then(|map| map.get(module_name).cloned())781 .or_else(|| default_module_type_provider(module_name));782783 let module_type = module_config.map(|config| {784 let mut type_errors: Vec<String> = Vec::new();785 let ty = globals::install_type_config_with_errors(786 &mut self.globals,787 &mut self.shapes,788 &config,789 module_name,790 (),791 &mut type_errors,792 );793 // Store errors for later reporting when the import is actually used794 for err in type_errors {795 self.module_type_errors796 .entry(module_name.to_string())797 .or_default()798 .push(err);799 }800 ty801 });802 self.module_types803 .insert(module_name.to_string(), module_type.clone());804 module_type805 }806807 fn is_known_react_module(&self, module_name: &str) -> bool {808 let lower = module_name.to_lowercase();809 lower == "react" || lower == "react-dom"810 }811812 fn get_custom_hook_type(&mut self) -> Global {813 if self.config.enable_assume_hooks_follow_rules_of_react {814 if self.default_nonmutating_hook.is_none() {815 self.default_nonmutating_hook = Some(default_nonmutating_hook(&mut self.shapes));816 }817 self.default_nonmutating_hook.clone().unwrap()818 } else {819 if self.default_mutating_hook.is_none() {820 self.default_mutating_hook = Some(default_mutating_hook(&mut self.shapes));821 }822 self.default_mutating_hook.clone().unwrap()823 }824 }825826 /// Public accessor for the custom hook type, used by InferTypes for827 /// property resolution fallback when a property name looks like a hook.828 pub fn get_custom_hook_type_opt(&mut self) -> Option<Global> {829 Some(self.get_custom_hook_type())830 }831832 /// Get a reference to the shapes registry.833 pub fn shapes(&self) -> &ShapeRegistry {834 &self.shapes835 }836837 /// Get a reference to the globals registry.838 pub fn globals(&self) -> &GlobalRegistry {839 &self.globals840 }841842 /// Generate a globally unique identifier name, analogous to TS843 /// `generateGloballyUniqueIdentifierName` which delegates to Babel's844 /// `scope.generateUidIdentifier`. Matches Babel's naming convention:845 /// first name is `_<name>`, subsequent are `_<name>2`, `_<name>3`, etc.846 /// Also applies Babel's `toIdentifier` sanitization on the input name.847 ///848 /// Like Babel's `generateUid`, checks for collisions against existing849 /// bindings (source-level identifier names) and previously generated UIDs,850 /// rather than using a blind counter.851 pub fn generate_globally_unique_identifier_name(&mut self, name: Option<&str>) -> String {852 let base = name.unwrap_or("temp");853 // Apply Babel's toIdentifier sanitization:854 // 1. Replace non-identifier chars with '-'855 // 2. Strip leading '-' and digits856 // 3. CamelCase: replace '-' sequences + optional following char with uppercase of that char857 let mut dashed = String::new();858 for c in base.chars() {859 if c.is_ascii_alphanumeric() || c == '_' || c == '$' {860 dashed.push(c);861 } else {862 dashed.push('-');863 }864 }865 // Strip leading dashes and digits866 let trimmed = dashed.trim_start_matches(|c: char| c == '-' || c.is_ascii_digit());867 // CamelCase conversion: replace sequences of '-' followed by optional char with uppercase868 let mut camel = String::new();869 let mut chars = trimmed.chars().peekable();870 while let Some(c) = chars.next() {871 if c == '-' {872 while chars.peek() == Some(&'-') {873 chars.next();874 }875 if let Some(next) = chars.next() {876 for uc in next.to_uppercase() {877 camel.push(uc);878 }879 }880 } else {881 camel.push(c);882 }883 }884 if camel.is_empty() {885 camel = "temp".to_string();886 }887 // Strip leading '_' and trailing digits (Babel's generateUid behavior)888 let stripped = camel.trim_start_matches('_');889 let stripped = stripped.trim_end_matches(|c: char| c.is_ascii_digit());890 let uid_base = if stripped.is_empty() {891 "temp"892 } else {893 stripped894 };895896 // Lazily build the set of known names from existing identifiers.897 // This approximates Babel's hasBinding/hasGlobal/hasReference checks.898 if self.uid_known_names.is_none() {899 let mut known = FxHashSet::default();900 for id in &self.identifiers {901 if let Some(name) = &id.name {902 known.insert(name.value().to_string());903 }904 }905 self.uid_known_names = Some(known);906 }907908 // Find a name that doesn't collide, matching Babel's generateUid loop909 let mut i = 1u32;910 let uid = loop {911 let candidate = if i == 1 {912 format!("_{}", uid_base)913 } else {914 format!("_{}{}", uid_base, i)915 };916 i += 1;917 if !self.uid_known_names.as_ref().unwrap().contains(&candidate) {918 break candidate;919 }920 };921922 // Register the generated name so subsequent calls see it923 self.uid_known_names.as_mut().unwrap().insert(uid.clone());924925 uid926 }927928 /// Seed the UID known names set with external names (e.g. from ProgramContext).929 /// This ensures UID generation avoids names generated by previous function compilations,930 /// matching Babel's behavior where the program scope accumulates all generated UIDs.931 pub fn seed_uid_known_names(&mut self, names: &FxHashSet<String>) {932 match &mut self.uid_known_names {933 Some(existing) => existing.extend(names.iter().cloned()),934 None => self.uid_known_names = Some(names.clone()),935 }936 }937938 /// Return the UID known names accumulated during this compilation.939 pub fn take_uid_known_names(&mut self) -> Option<FxHashSet<String>> {940 self.uid_known_names.take()941 }942943 /// Record an outlined function (extracted during outlineFunctions or outlineJSX).944 /// Corresponds to TS `env.outlineFunction(fn, type)`.945 pub fn outline_function(&mut self, func: HirFunction, fn_type: Option<ReactFunctionType>) {946 self.outlined_functions947 .push(OutlinedFunctionEntry { func, fn_type });948 }949950 /// Get the outlined functions accumulated during compilation.951 pub fn get_outlined_functions(&self) -> &[OutlinedFunctionEntry] {952 &self.outlined_functions953 }954955 /// Take the outlined functions, leaving the vec empty.956 pub fn take_outlined_functions(&mut self) -> Vec<OutlinedFunctionEntry> {957 std::mem::take(&mut self.outlined_functions)958 }959960 /// Whether memoization is enabled for this compilation.961 /// Ported from TS `get enableMemoization()` in Environment.ts.962 /// Returns true for client/lint modes, false for SSR.963 pub fn enable_memoization(&self) -> bool {964 match self.output_mode {965 OutputMode::Client | OutputMode::Lint => true,966 OutputMode::Ssr => false,967 }968 }969970 /// Whether validations are enabled for this compilation.971 /// Ported from TS `get enableValidations()` in Environment.ts.972 pub fn enable_validations(&self) -> bool {973 match self.output_mode {974 OutputMode::Client | OutputMode::Lint | OutputMode::Ssr => true,975 }976 }977978 // =========================================================================979 // Name resolution helpers980 // =========================================================================981982 /// Get the user-visible name for an identifier.983 ///984 /// First checks the identifier's own name. If None, looks for another985 /// identifier with the same `declaration_id` that has a name. This handles986 /// SSA identifiers that don't carry names but share a declaration_id with987 /// the original named identifier from lowering.988 ///989 /// This is analogous to `identifierName` on Babel's SourceLocation,990 /// which the parser sets on every identifier node.991 pub fn identifier_name_for_id(&self, id: IdentifierId) -> Option<String> {992 let ident = &self.identifiers[id.0 as usize];993 if let Some(name) = &ident.name {994 return Some(name.value().to_string());995 }996 // Fall back: find another identifier with the same declaration_id that has a Named name997 let decl_id = ident.declaration_id;998 for other in &self.identifiers {999 if other.declaration_id == decl_id {1000 if let Some(IdentifierName::Named(name)) = &other.name {1001 return Some(name.clone());1002 }1003 }1004 }1005 None1006 }10071008 // =========================================================================1009 // ID-based type helper methods1010 // =========================================================================10111012 /// Check whether the function type for an identifier has a noAlias signature.1013 /// Looks up the identifier's type and checks its function signature.1014 pub fn has_no_alias_signature(&self, identifier_id: IdentifierId) -> bool {1015 let ty = &self.types[self.identifiers[identifier_id.0 as usize].type_.0 as usize];1016 self.get_function_signature(ty)1017 .ok()1018 .flatten()1019 .map_or(false, |sig| sig.no_alias)1020 }10211022 /// Get the hook kind for an identifier, if its type represents a hook.1023 /// Looks up the identifier's type and delegates to `get_hook_kind_for_type`.1024 pub fn get_hook_kind_for_id(1025 &self,1026 identifier_id: IdentifierId,1027 ) -> Result<Option<&HookKind>, CompilerDiagnostic> {1028 let ty = &self.types[self.identifiers[identifier_id.0 as usize].type_.0 as usize];1029 self.get_hook_kind_for_type(ty)1030 }1031}10321033impl Default for Environment {1034 fn default() -> Self {1035 Self::new()1036 }1037}10381039/// Check if a name matches the React hook naming convention: `use[A-Z0-9]`.1040/// Ported from TS `isHookName` in Environment.ts.1041pub fn is_hook_name(name: &str) -> bool {1042 if name.len() < 4 {1043 return false;1044 }1045 if !name.starts_with("use") {1046 return false;1047 }1048 let fourth_char = name.as_bytes()[3];1049 fourth_char.is_ascii_uppercase() || fourth_char.is_ascii_digit()1050}10511052/// Returns true if the name follows React naming conventions (component or hook).1053/// Components start with an uppercase letter; hooks match `use[A-Z0-9]`.1054pub fn is_react_like_name(name: &str) -> bool {1055 if name.is_empty() {1056 return false;1057 }1058 let first_char = name.as_bytes()[0];1059 if first_char.is_ascii_uppercase() {1060 return true;1061 }1062 is_hook_name(name)1063}10641065#[cfg(test)]1066mod tests {1067 use super::*;10681069 #[test]1070 fn test_is_hook_name() {1071 assert!(is_hook_name("useState"));1072 assert!(is_hook_name("useEffect"));1073 assert!(is_hook_name("useMyHook"));1074 assert!(is_hook_name("use3rdParty"));1075 assert!(!is_hook_name("use"));1076 assert!(!is_hook_name("used"));1077 assert!(!is_hook_name("useless"));1078 assert!(!is_hook_name("User"));1079 assert!(!is_hook_name("foo"));1080 }10811082 #[test]1083 fn test_environment_has_globals() {1084 let env = Environment::new();1085 assert!(env.globals().contains_key("useState"));1086 assert!(env.globals().contains_key("useEffect"));1087 assert!(env.globals().contains_key("useRef"));1088 assert!(env.globals().contains_key("Math"));1089 assert!(env.globals().contains_key("console"));1090 assert!(env.globals().contains_key("Array"));1091 assert!(env.globals().contains_key("Object"));1092 }10931094 #[test]1095 fn test_get_property_type_array() {1096 let mut env = Environment::new();1097 let array_type = Type::Object {1098 shape_id: Some("BuiltInArray".to_string()),1099 };1100 let map_type = env.get_property_type(&array_type, "map").unwrap();1101 assert!(map_type.is_some());1102 let push_type = env.get_property_type(&array_type, "push").unwrap();1103 assert!(push_type.is_some());1104 let nonexistent = env1105 .get_property_type(&array_type, "nonExistentMethod")1106 .unwrap();1107 assert!(nonexistent.is_none());1108 }11091110 #[test]1111 fn test_get_function_signature() {1112 let env = Environment::new();1113 let use_state_type = env.globals().get("useState").unwrap();1114 let sig = env.get_function_signature(use_state_type).unwrap();1115 assert!(sig.is_some());1116 let sig = sig.unwrap();1117 assert!(sig.hook_kind.is_some());1118 assert_eq!(sig.hook_kind.as_ref().unwrap(), &HookKind::UseState);1119 }11201121 #[test]1122 fn test_get_global_declaration() {1123 let mut env = Environment::new();1124 // Global binding1125 let binding = NonLocalBinding::Global {1126 name: "Math".to_string(),1127 };1128 let result = env.get_global_declaration(&binding, None).unwrap();1129 assert!(result.is_some());11301131 // Import from react1132 let binding = NonLocalBinding::ImportSpecifier {1133 name: "useState".to_string(),1134 module: "react".to_string(),1135 imported: "useState".to_string(),1136 };1137 let result = env.get_global_declaration(&binding, None).unwrap();1138 assert!(result.is_some());11391140 // Unknown global1141 let binding = NonLocalBinding::Global {1142 name: "unknownThing".to_string(),1143 };1144 let result = env.get_global_declaration(&binding, None).unwrap();1145 assert!(result.is_none());11461147 // Hook-like name gets default hook type1148 let binding = NonLocalBinding::Global {1149 name: "useCustom".to_string(),1150 };1151 let result = env.get_global_declaration(&binding, None).unwrap();1152 assert!(result.is_some());1153 }1154}
Code quality findings 35
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning
correctness
unwrap-usage
self.default_nonmutating_hook.clone().unwrap()
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning
correctness
unwrap-usage
self.default_mutating_hook.clone().unwrap()
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning
correctness
unwrap-usage
if !self.uid_known_names.as_ref().unwrap().contains(&candidate) {
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning
correctness
unwrap-usage
self.uid_known_names.as_mut().unwrap().insert(uid.clone());
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
let ident = &self.identifiers[id.0 as usize];
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
let ty = &self.types[self.identifiers[identifier_id.0 as usize].type_.0 as usize];
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
let ty = &self.types[self.identifiers[identifier_id.0 as usize].type_.0 as usize];
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
/// Check if a name matches the React hook naming convention: `use[A-Z0-9]`.
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
let fourth_char = name.as_bytes()[3];
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
/// Components start with an uppercase letter; hooks match `use[A-Z0-9]`.
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
let first_char = name.as_bytes()[0];
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning
correctness
unwrap-usage
let result = env.get_global_declaration(&binding, None).unwrap();
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning
correctness
unwrap-usage
let result = env.get_global_declaration(&binding, None).unwrap();
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning
correctness
unwrap-usage
let result = env.get_global_declaration(&binding, None).unwrap();
Info: Wildcard imports (`use some::path::*;`) can obscure the origin of names and lead to conflicts. Prefer importing specific items explicitly.
info
maintainability
wildcard-import
use crate::*;
Performance Info: Calling .to_string() (especially on &str) allocates a new String. If done repeatedly in loops, consider alternatives like working with &str or using crates like `itoa`/`ryu` for number-to-string conversion.
info
performance
to-string-in-loop
shape_id: Some(BUILT_IN_MIXED_READONLY_ID.to_string()),
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info
performance
push-without-reserve
invariant.details.push(detail);
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info
correctness
match-wildcard
let shape_id = match receiver {
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info
correctness
match-wildcard
let shape_id = match receiver {
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info
correctness
match-wildcard
let shape_id = match receiver {
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info
correctness
match-wildcard
let shape_id = match receiver {
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info
correctness
match-wildcard
let shape_id = match ty {
Performance Info: Calling .to_string() (especially on &str) allocates a new String. If done repeatedly in loops, consider alternatives like working with &str or using crates like `itoa`/`ryu` for number-to-string conversion.
info
performance
to-string-in-loop
.entry(module_name.to_string())
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info
performance
push-without-reserve
.push(err);
Performance Info: Calling .to_string() (especially on &str) allocates a new String. If done repeatedly in loops, consider alternatives like working with &str or using crates like `itoa`/`ryu` for number-to-string conversion.
info
performance
to-string-in-loop
.insert(module_name.to_string(), module_type.clone());
Performance Info: Frequent cloning, especially of Strings, Vecs, or other heap-allocated types inside loops, can be expensive. Consider using references/borrowing where possible.
info
performance
clone-in-loop
.insert(module_name.to_string(), module_type.clone());
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info
performance
push-without-reserve
dashed.push(c);
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info
performance
push-without-reserve
dashed.push('-');
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info
performance
push-without-reserve
camel.push(uc);
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info
performance
push-without-reserve
camel.push(c);
Performance Info: Calling .to_string() (especially on &str) allocates a new String. If done repeatedly in loops, consider alternatives like working with &str or using crates like `itoa`/`ryu` for number-to-string conversion.
info
performance
to-string-in-loop
camel = "temp".to_string();
Performance Info: Calling .to_string() (especially on &str) allocates a new String. If done repeatedly in loops, consider alternatives like working with &str or using crates like `itoa`/`ryu` for number-to-string conversion.
info
performance
to-string-in-loop
known.insert(name.value().to_string());
Performance Info: Calling .to_string() (especially on &str) allocates a new String. If done repeatedly in loops, consider alternatives like working with &str or using crates like `itoa`/`ryu` for number-to-string conversion.
info
performance
to-string-in-loop
return Some(name.value().to_string());
Performance Info: Frequent cloning, especially of Strings, Vecs, or other heap-allocated types inside loops, can be expensive. Consider using references/borrowing where possible.
info
performance
clone-in-loop
return Some(name.clone());
Info: Wildcard imports (`use some::path::*;`) can obscure the origin of names and lead to conflicts. Prefer importing specific items explicitly.
info
maintainability
wildcard-import
use super::*;