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>.
let ident = &identifiers[id.0 as usize];
1// Copyright (c) Meta Platforms, Inc. and affiliates.2//3// This source code is licensed under the MIT license found in the4// LICENSE file in the root directory of this source tree.56//! Validates that useEffect is not used for derived computations which could/should7//! be performed in render.8//!9//! See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state10//!11//! Port of ValidateNoDerivedComputationsInEffects_exp.ts.1213use indexmap::{IndexMap, IndexSet};14use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};1516use react_compiler_diagnostics::{17 CompilerDiagnostic, CompilerDiagnosticDetail, CompilerError, CompilerErrorDetail, ErrorCategory,18};19use react_compiler_hir::environment::Environment;20use react_compiler_hir::visitors::{21 each_instruction_lvalue_ids, each_instruction_operand as canonical_each_instruction_operand,22};23use react_compiler_hir::{24 ArrayElement, BlockId, Effect, EvaluationOrder, FunctionId, HirFunction, Identifier,25 IdentifierId, IdentifierName, InstructionValue, ParamPattern, PlaceOrSpread, ReactFunctionType,26 ReturnVariant, SourceLocation, Type, is_set_state_type, is_use_effect_hook_type,27 is_use_ref_type, is_use_state_type,28};2930/// Get the user-visible name for an identifier, matching Babel's31/// loc.identifierName behavior. First checks the identifier's own name,32/// then falls back to extracting the name from the source code at the33/// given source location. This handles SSA identifiers whose names were34/// lost during compiler passes.35fn get_identifier_name_with_loc(36 id: IdentifierId,37 identifiers: &[Identifier],38 loc: &Option<SourceLocation>,39 source_code: Option<&str>,40) -> Option<String> {41 let ident = &identifiers[id.0 as usize];42 match &ident.name {43 Some(IdentifierName::Named(name)) | Some(IdentifierName::Promoted(name)) => {44 return Some(name.clone());45 }46 _ => {}47 }48 // Fall back: find another identifier with the same declaration_id that has a name.49 let decl_id = ident.declaration_id;50 for other in identifiers {51 if other.declaration_id == decl_id {52 match &other.name {53 Some(IdentifierName::Named(name)) | Some(IdentifierName::Promoted(name)) => {54 return Some(name.clone());55 }56 _ => {}57 }58 }59 }60 // Fall back to extracting from source code using UTF-16 code unit indices.61 // Babel/JS positions use UTF-16 code unit offsets, but Rust strings are UTF-8,62 // so we need to convert between the two.63 if let (Some(loc), Some(code)) = (loc, source_code) {64 let start_utf16 = loc.start.index? as usize;65 let end_utf16 = loc.end.index? as usize;66 if start_utf16 < end_utf16 {67 // Convert UTF-16 code unit offsets to UTF-8 byte offsets68 let mut utf16_pos = 0usize;69 let mut byte_start = None;70 let mut byte_end = None;71 for (byte_idx, ch) in code.char_indices() {72 if utf16_pos == start_utf16 {73 byte_start = Some(byte_idx);74 }75 if utf16_pos == end_utf16 {76 byte_end = Some(byte_idx);77 break;78 }79 utf16_pos += ch.len_utf16();80 }81 // Handle end at the very end of string82 if utf16_pos == end_utf16 && byte_end.is_none() {83 byte_end = Some(code.len());84 }85 if let (Some(start), Some(end)) = (byte_start, byte_end) {86 let slice = &code[start..end];87 if !slice.is_empty()88 && slice89 .chars()90 .all(|c| c.is_alphanumeric() || c == '_' || c == '$')91 {92 return Some(slice.to_string());93 }94 }95 }96 }97 None98}99100const MAX_FIXPOINT_ITERATIONS: usize = 100;101102#[derive(Debug, Clone, Copy, PartialEq, Eq)]103enum TypeOfValue {104 Ignored,105 FromProps,106 FromState,107 FromPropsAndState,108}109110#[derive(Debug, Clone)]111struct DerivationMetadata {112 type_of_value: TypeOfValue,113 place_identifier: IdentifierId,114 place_name: Option<IdentifierName>,115 source_ids: IndexSet<IdentifierId, FxBuildHasher>,116 is_state_source: bool,117}118119/// Metadata about a useEffect call site.120struct EffectMetadata {121 effect_func_id: FunctionId,122 dep_elements: Vec<DepElement>,123}124125#[derive(Debug, Clone)]126struct DepElement {127 identifier: IdentifierId,128 loc: Option<SourceLocation>,129}130131struct ValidationContext {132 /// Map from lvalue identifier to the FunctionId of function expressions133 functions: FxHashMap<IdentifierId, FunctionId>,134 /// Map from lvalue identifier to ArrayExpression elements (candidate deps)135 candidate_dependencies: FxHashMap<IdentifierId, Vec<DepElement>>,136 derivation_cache: DerivationCache,137 effects_cache: FxHashMap<IdentifierId, EffectMetadata>,138 set_state_loads: FxHashMap<IdentifierId, Option<IdentifierId>>,139 set_state_usages: FxHashMap<IdentifierId, FxHashSet<LocKey>>,140}141142/// A hashable key for SourceLocation to use in FxHashSet143#[derive(Debug, Clone, PartialEq, Eq, Hash)]144struct LocKey {145 start_line: u32,146 start_col: u32,147 end_line: u32,148 end_col: u32,149}150151impl LocKey {152 fn from_loc(loc: &Option<SourceLocation>) -> Self {153 match loc {154 Some(loc) => LocKey {155 start_line: loc.start.line,156 start_col: loc.start.column,157 end_line: loc.end.line,158 end_col: loc.end.column,159 },160 None => LocKey {161 start_line: 0,162 start_col: 0,163 end_line: 0,164 end_col: 0,165 },166 }167 }168}169170#[derive(Debug, Clone)]171struct DerivationCache {172 has_changes: bool,173 cache: FxHashMap<IdentifierId, DerivationMetadata>,174 previous_cache: Option<FxHashMap<IdentifierId, DerivationMetadata>>,175}176177impl DerivationCache {178 fn new() -> Self {179 DerivationCache {180 has_changes: false,181 cache: FxHashMap::default(),182 previous_cache: None,183 }184 }185186 fn take_snapshot(&mut self) {187 let mut prev = FxHashMap::default();188 for (key, value) in &self.cache {189 prev.insert(190 *key,191 DerivationMetadata {192 place_identifier: value.place_identifier,193 place_name: value.place_name.clone(),194 source_ids: value.source_ids.clone(),195 type_of_value: value.type_of_value,196 is_state_source: value.is_state_source,197 },198 );199 }200 self.previous_cache = Some(prev);201 }202203 fn check_for_changes(&mut self) {204 let prev = match &self.previous_cache {205 Some(p) => p,206 None => {207 self.has_changes = true;208 return;209 }210 };211212 for (key, value) in &self.cache {213 match prev.get(key) {214 None => {215 self.has_changes = true;216 return;217 }218 Some(prev_value) => {219 if !is_derivation_equal(prev_value, value) {220 self.has_changes = true;221 return;222 }223 }224 }225 }226227 if self.cache.len() != prev.len() {228 self.has_changes = true;229 return;230 }231232 self.has_changes = false;233 }234235 fn snapshot(&mut self) -> bool {236 let has_changes = self.has_changes;237 self.has_changes = false;238 has_changes239 }240241 fn add_derivation_entry(242 &mut self,243 derived_id: IdentifierId,244 derived_name: Option<IdentifierName>,245 source_ids: IndexSet<IdentifierId, FxBuildHasher>,246 type_of_value: TypeOfValue,247 is_state_source: bool,248 ) {249 let mut final_is_source = is_state_source;250 if !final_is_source {251 for source_id in &source_ids {252 if let Some(source_metadata) = self.cache.get(source_id) {253 if source_metadata.is_state_source254 && !matches!(&source_metadata.place_name, Some(IdentifierName::Named(_)))255 {256 final_is_source = true;257 break;258 }259 }260 }261 }262263 self.cache.insert(264 derived_id,265 DerivationMetadata {266 place_identifier: derived_id,267 place_name: derived_name,268 source_ids,269 type_of_value,270 is_state_source: final_is_source,271 },272 );273 }274}275276fn is_derivation_equal(a: &DerivationMetadata, b: &DerivationMetadata) -> bool {277 if a.type_of_value != b.type_of_value {278 return false;279 }280 if a.source_ids.len() != b.source_ids.len() {281 return false;282 }283 for id in &a.source_ids {284 if !b.source_ids.contains(id) {285 return false;286 }287 }288 true289}290291fn join_value(lvalue_type: TypeOfValue, value_type: TypeOfValue) -> TypeOfValue {292 if lvalue_type == TypeOfValue::Ignored {293 return value_type;294 }295 if value_type == TypeOfValue::Ignored {296 return lvalue_type;297 }298 if lvalue_type == value_type {299 return lvalue_type;300 }301 TypeOfValue::FromPropsAndState302}303304fn get_root_set_state(305 key: IdentifierId,306 loads: &FxHashMap<IdentifierId, Option<IdentifierId>>,307 visited: &mut FxHashSet<IdentifierId>,308) -> Option<IdentifierId> {309 if visited.contains(&key) {310 return None;311 }312 visited.insert(key);313314 match loads.get(&key) {315 None => None,316 Some(None) => Some(key),317 Some(Some(parent_id)) => get_root_set_state(*parent_id, loads, visited),318 }319}320321fn maybe_record_set_state_for_instr(322 instr: &react_compiler_hir::Instruction,323 env: &Environment,324 set_state_loads: &mut FxHashMap<IdentifierId, Option<IdentifierId>>,325 set_state_usages: &mut FxHashMap<IdentifierId, FxHashSet<LocKey>>,326) {327 let identifiers = &env.identifiers;328 let types = &env.types;329330 let all_lvalues = each_instruction_lvalue_ids(instr);331 for &lvalue_id in &all_lvalues {332 // Check if this is a LoadLocal from a known setState333 if let InstructionValue::LoadLocal { place, .. } = &instr.value {334 if set_state_loads.contains_key(&place.identifier) {335 set_state_loads.insert(lvalue_id, Some(place.identifier));336 } else {337 // Only check root setState if not a LoadLocal from a known chain338 let lvalue_ident = &identifiers[lvalue_id.0 as usize];339 let lvalue_ty = &types[lvalue_ident.type_.0 as usize];340 if is_set_state_type(lvalue_ty) {341 set_state_loads.insert(lvalue_id, None);342 }343 }344 } else {345 // Check if lvalue is a setState type (root setState)346 let lvalue_ident = &identifiers[lvalue_id.0 as usize];347 let lvalue_ty = &types[lvalue_ident.type_.0 as usize];348 if is_set_state_type(lvalue_ty) {349 set_state_loads.insert(lvalue_id, None);350 }351 }352353 let root = get_root_set_state(lvalue_id, set_state_loads, &mut FxHashSet::default());354 if let Some(root_id) = root {355 set_state_usages.entry(root_id).or_insert_with(|| {356 let mut set = FxHashSet::default();357 set.insert(LocKey::from_loc(&instr.lvalue.loc));358 set359 });360 }361 }362}363364fn is_mutable_at(365 env: &Environment,366 eval_order: EvaluationOrder,367 identifier_id: IdentifierId,368) -> bool {369 env.identifiers[identifier_id.0 as usize]370 .mutable_range371 .contains(eval_order)372}373374pub fn validate_no_derived_computations_in_effects_exp(375 func: &HirFunction,376 env: &Environment,377) -> Result<CompilerError, CompilerDiagnostic> {378 let identifiers = &env.identifiers;379380 let mut context = ValidationContext {381 functions: FxHashMap::default(),382 candidate_dependencies: FxHashMap::default(),383 derivation_cache: DerivationCache::new(),384 effects_cache: FxHashMap::default(),385 set_state_loads: FxHashMap::default(),386 set_state_usages: FxHashMap::default(),387 };388389 // Initialize derivation cache based on function type390 if func.fn_type == ReactFunctionType::Hook {391 for param in &func.params {392 if let ParamPattern::Place(place) = param {393 let name = identifiers[place.identifier.0 as usize].name.clone();394 context.derivation_cache.cache.insert(395 place.identifier,396 DerivationMetadata {397 place_identifier: place.identifier,398 place_name: name,399 source_ids: IndexSet::default(),400 type_of_value: TypeOfValue::FromProps,401 is_state_source: true,402 },403 );404 }405 }406 } else if func.fn_type == ReactFunctionType::Component {407 if let Some(param) = func.params.first() {408 if let ParamPattern::Place(place) = param {409 let name = identifiers[place.identifier.0 as usize].name.clone();410 context.derivation_cache.cache.insert(411 place.identifier,412 DerivationMetadata {413 place_identifier: place.identifier,414 place_name: name,415 source_ids: IndexSet::default(),416 type_of_value: TypeOfValue::FromProps,417 is_state_source: true,418 },419 );420 }421 }422 }423424 // Fixpoint iteration425 let mut is_first_pass = true;426 let mut iteration_count = 0;427 loop {428 context.derivation_cache.take_snapshot();429430 for (_block_id, block) in &func.body.blocks {431 record_phi_derivations(block, &mut context, env);432 for &instr_id in &block.instructions {433 let instr = &func.instructions[instr_id.0 as usize];434 record_instruction_derivations(instr, &mut context, is_first_pass, func, env)?;435 }436 }437438 context.derivation_cache.check_for_changes();439 is_first_pass = false;440 iteration_count += 1;441 assert!(442 iteration_count < MAX_FIXPOINT_ITERATIONS,443 "[ValidateNoDerivedComputationsInEffects] Fixpoint iteration failed to converge."444 );445446 if !context.derivation_cache.snapshot() {447 break;448 }449 }450451 // Validate all effect sites452 let mut errors = CompilerError::new();453 let effects_cache: Vec<(IdentifierId, FunctionId, Vec<DepElement>)> = context454 .effects_cache455 .iter()456 .map(|(k, v)| (*k, v.effect_func_id, v.dep_elements.clone()))457 .collect();458459 for (_key, effect_func_id, dep_elements) in &effects_cache {460 validate_effect(461 *effect_func_id,462 dep_elements,463 &mut context,464 func,465 env,466 &mut errors,467 );468 }469470 Ok(errors)471}472473fn record_phi_derivations(474 block: &react_compiler_hir::BasicBlock,475 context: &mut ValidationContext,476 env: &Environment,477) {478 let identifiers = &env.identifiers;479 for phi in &block.phis {480 let mut type_of_value = TypeOfValue::Ignored;481 let mut source_ids: IndexSet<IdentifierId, FxBuildHasher> = IndexSet::default();482483 for (_block_id, operand) in &phi.operands {484 if let Some(operand_metadata) = context.derivation_cache.cache.get(&operand.identifier)485 {486 type_of_value = join_value(type_of_value, operand_metadata.type_of_value);487 source_ids.insert(operand.identifier);488 }489 }490491 if type_of_value != TypeOfValue::Ignored {492 let name = identifiers[phi.place.identifier.0 as usize].name.clone();493 context.derivation_cache.add_derivation_entry(494 phi.place.identifier,495 name,496 source_ids,497 type_of_value,498 false,499 );500 }501 }502}503504fn record_instruction_derivations(505 instr: &react_compiler_hir::Instruction,506 context: &mut ValidationContext,507 is_first_pass: bool,508 _outer_func: &HirFunction,509 env: &Environment,510) -> Result<(), CompilerDiagnostic> {511 let identifiers = &env.identifiers;512 let types = &env.types;513 let functions = &env.functions;514 let lvalue_id = instr.lvalue.identifier;515516 // maybeRecordSetState517 maybe_record_set_state_for_instr(518 instr,519 env,520 &mut context.set_state_loads,521 &mut context.set_state_usages,522 );523524 let mut type_of_value = TypeOfValue::Ignored;525 let is_source = false;526 let mut sources: IndexSet<IdentifierId, FxBuildHasher> = IndexSet::default();527528 match &instr.value {529 InstructionValue::FunctionExpression { lowered_func, .. } => {530 context.functions.insert(lvalue_id, lowered_func.func);531 // Recurse into the inner function532 let inner_func = &functions[lowered_func.func.0 as usize];533 for (_block_id, block) in &inner_func.body.blocks {534 record_phi_derivations(block, context, env);535 for &inner_instr_id in &block.instructions {536 let inner_instr = &inner_func.instructions[inner_instr_id.0 as usize];537 record_instruction_derivations(538 inner_instr,539 context,540 is_first_pass,541 inner_func,542 env,543 )?;544 }545 }546 }547 InstructionValue::CallExpression { callee, args, .. } => {548 let callee_type = &types[identifiers[callee.identifier.0 as usize].type_.0 as usize];549 if is_use_effect_hook_type(callee_type) && args.len() == 2 {550 if let (551 react_compiler_hir::PlaceOrSpread::Place(arg0),552 react_compiler_hir::PlaceOrSpread::Place(arg1),553 ) = (&args[0], &args[1])554 {555 let effect_function = context.functions.get(&arg0.identifier).copied();556 let deps = context557 .candidate_dependencies558 .get(&arg1.identifier)559 .cloned();560 if let (Some(effect_func_id), Some(dep_elements)) = (effect_function, deps) {561 context.effects_cache.insert(562 arg0.identifier,563 EffectMetadata {564 effect_func_id,565 dep_elements,566 },567 );568 }569 }570 }571572 // Check if lvalue is useState type573 let lvalue_type = &types[identifiers[lvalue_id.0 as usize].type_.0 as usize];574 if is_use_state_type(lvalue_type) {575 let name = identifiers[lvalue_id.0 as usize].name.clone();576 context.derivation_cache.add_derivation_entry(577 lvalue_id,578 name,579 IndexSet::default(),580 TypeOfValue::FromState,581 true,582 );583 return Ok(());584 }585 }586 InstructionValue::MethodCall { property, args, .. } => {587 let prop_type = &types[identifiers[property.identifier.0 as usize].type_.0 as usize];588 if is_use_effect_hook_type(prop_type) && args.len() == 2 {589 if let (590 react_compiler_hir::PlaceOrSpread::Place(arg0),591 react_compiler_hir::PlaceOrSpread::Place(arg1),592 ) = (&args[0], &args[1])593 {594 let effect_function = context.functions.get(&arg0.identifier).copied();595 let deps = context596 .candidate_dependencies597 .get(&arg1.identifier)598 .cloned();599 if let (Some(effect_func_id), Some(dep_elements)) = (effect_function, deps) {600 context.effects_cache.insert(601 arg0.identifier,602 EffectMetadata {603 effect_func_id,604 dep_elements,605 },606 );607 }608 }609 }610611 // Check if lvalue is useState type612 let lvalue_type = &types[identifiers[lvalue_id.0 as usize].type_.0 as usize];613 if is_use_state_type(lvalue_type) {614 let name = identifiers[lvalue_id.0 as usize].name.clone();615 context.derivation_cache.add_derivation_entry(616 lvalue_id,617 name,618 IndexSet::default(),619 TypeOfValue::FromState,620 true,621 );622 return Ok(());623 }624 }625 InstructionValue::ArrayExpression { elements, .. } => {626 let dep_elements: Vec<DepElement> = elements627 .iter()628 .filter_map(|el| match el {629 ArrayElement::Place(p) => Some(DepElement {630 identifier: p.identifier,631 loc: p.loc,632 }),633 _ => None,634 })635 .collect();636 context637 .candidate_dependencies638 .insert(lvalue_id, dep_elements);639 }640 _ => {}641 }642643 // Collect operand derivations644 for (operand_id, operand_loc) in each_instruction_operand(instr, env) {645 // Track setState usages646 if context.set_state_loads.contains_key(&operand_id) {647 let root = get_root_set_state(648 operand_id,649 &context.set_state_loads,650 &mut FxHashSet::default(),651 );652 if let Some(root_id) = root {653 if let Some(usages) = context.set_state_usages.get_mut(&root_id) {654 usages.insert(LocKey::from_loc(&operand_loc));655 }656 }657 }658659 if let Some(operand_metadata) = context.derivation_cache.cache.get(&operand_id) {660 type_of_value = join_value(type_of_value, operand_metadata.type_of_value);661 sources.insert(operand_id);662 }663 }664665 if type_of_value == TypeOfValue::Ignored {666 return Ok(());667 }668669 // Record derivation for ALL lvalue places (including destructured variables)670 for &lv_id in &each_instruction_lvalue_ids(instr) {671 let name = identifiers[lv_id.0 as usize].name.clone();672 context.derivation_cache.add_derivation_entry(673 lv_id,674 name,675 sources.clone(),676 type_of_value,677 is_source,678 );679 }680681 if matches!(&instr.value, InstructionValue::FunctionExpression { .. }) {682 // Don't record mutation effects for FunctionExpressions683 return Ok(());684 }685686 // Handle mutable operands687 for operand in each_instruction_operand_with_effect(instr, env) {688 if operand.effect.is_mutable() {689 if is_mutable_at(env, instr.id, operand.id) {690 if let Some(existing) = context.derivation_cache.cache.get_mut(&operand.id) {691 existing.type_of_value = join_value(type_of_value, existing.type_of_value);692 } else {693 let name = identifiers[operand.id.0 as usize].name.clone();694 context.derivation_cache.add_derivation_entry(695 operand.id,696 name,697 sources.clone(),698 type_of_value,699 false,700 );701 }702 }703 } else if matches!(operand.effect, Effect::Unknown) {704 return Err(CompilerDiagnostic::new(705 ErrorCategory::Invariant,706 "Unexpected unknown effect",707 None,708 ));709 }710 // Freeze | Read => no-op711 }712 Ok(())713}714715struct OperandWithEffect {716 id: IdentifierId,717 effect: Effect,718}719720/// Collects operand (IdentifierId, loc) pairs from an instruction.721/// Thin wrapper around canonical `each_instruction_operand` that maps Places to (id, loc) pairs.722fn each_instruction_operand(723 instr: &react_compiler_hir::Instruction,724 env: &Environment,725) -> Vec<(IdentifierId, Option<SourceLocation>)> {726 canonical_each_instruction_operand(instr, env)727 .into_iter()728 .map(|place| (place.identifier, place.loc))729 .collect()730}731732/// Collects operands with their effects.733/// Thin wrapper around canonical `each_instruction_operand` that maps Places to OperandWithEffect.734fn each_instruction_operand_with_effect(735 instr: &react_compiler_hir::Instruction,736 env: &Environment,737) -> Vec<OperandWithEffect> {738 canonical_each_instruction_operand(instr, env)739 .into_iter()740 .map(|place| OperandWithEffect {741 id: place.identifier,742 effect: place.effect,743 })744 .collect()745}746747// =============================================================================748// Tree building and rendering (for error messages)749// =============================================================================750751struct TreeNode {752 name: String,753 type_of_value: TypeOfValue,754 is_source: bool,755 children: Vec<TreeNode>,756}757758fn build_tree_node(759 source_id: IdentifierId,760 context: &ValidationContext,761 visited: &FxHashSet<String>,762) -> Vec<TreeNode> {763 let source_metadata = match context.derivation_cache.cache.get(&source_id) {764 Some(m) => m,765 None => return Vec::new(),766 };767768 if source_metadata.is_state_source {769 if let Some(IdentifierName::Named(name)) = &source_metadata.place_name {770 return vec![TreeNode {771 name: name.clone(),772 type_of_value: source_metadata.type_of_value,773 is_source: true,774 children: Vec::new(),775 }];776 }777 }778779 let mut children: Vec<TreeNode> = Vec::new();780 let mut named_siblings: IndexSet<String, FxBuildHasher> = IndexSet::default();781782 for child_id in &source_metadata.source_ids {783 assert_ne!(784 *child_id, source_id,785 "Unexpected self-reference: a value should not have itself as a source"786 );787788 let mut new_visited = visited.clone();789 if let Some(IdentifierName::Named(name)) = &source_metadata.place_name {790 new_visited.insert(name.clone());791 }792793 let child_nodes = build_tree_node(*child_id, context, &new_visited);794 for child_node in child_nodes {795 if !named_siblings.contains(&child_node.name) {796 named_siblings.insert(child_node.name.clone());797 children.push(child_node);798 }799 }800 }801802 if let Some(IdentifierName::Named(name)) = &source_metadata.place_name {803 if !visited.contains(name) {804 return vec![TreeNode {805 name: name.clone(),806 type_of_value: source_metadata.type_of_value,807 is_source: source_metadata.is_state_source,808 children,809 }];810 }811 }812813 children814}815816fn render_tree(817 node: &TreeNode,818 indent: &str,819 is_last: bool,820 props_set: &mut IndexSet<String, FxBuildHasher>,821 state_set: &mut IndexSet<String, FxBuildHasher>,822) -> String {823 let prefix = format!(824 "{}{}",825 indent,826 if is_last {827 "\u{2514}\u{2500}\u{2500} "828 } else {829 "\u{251c}\u{2500}\u{2500} "830 }831 );832 let child_indent = format!("{}{}", indent, if is_last { " " } else { "\u{2502} " });833834 let mut result = format!("{}{}", prefix, node.name);835836 if node.is_source {837 let type_label = match node.type_of_value {838 TypeOfValue::FromProps => {839 props_set.insert(node.name.clone());840 "Prop"841 }842 TypeOfValue::FromState => {843 state_set.insert(node.name.clone());844 "State"845 }846 _ => {847 props_set.insert(node.name.clone());848 state_set.insert(node.name.clone());849 "Prop and State"850 }851 };852 result += &format!(" ({})", type_label);853 }854855 if !node.children.is_empty() {856 result += "\n";857 for (index, child) in node.children.iter().enumerate() {858 let is_last_child = index == node.children.len() - 1;859 result += &render_tree(child, &child_indent, is_last_child, props_set, state_set);860 if index < node.children.len() - 1 {861 result += "\n";862 }863 }864 }865866 result867}868869fn get_fn_local_deps(870 func_id: Option<FunctionId>,871 env: &Environment,872) -> Option<FxHashSet<IdentifierId>> {873 let func_id = func_id?;874 let inner = &env.functions[func_id.0 as usize];875 let mut deps: FxHashSet<IdentifierId> = FxHashSet::default();876877 for (_block_id, block) in &inner.body.blocks {878 for &instr_id in &block.instructions {879 let instr = &inner.instructions[instr_id.0 as usize];880 if let InstructionValue::LoadLocal { place, .. } = &instr.value {881 deps.insert(place.identifier);882 }883 }884 }885886 Some(deps)887}888889fn validate_effect(890 effect_func_id: FunctionId,891 dependencies: &[DepElement],892 context: &mut ValidationContext,893 _outer_func: &HirFunction,894 env: &Environment,895 errors: &mut CompilerError,896) {897 let identifiers = &env.identifiers;898 let types = &env.types;899 let functions = &env.functions;900 let effect_function = &functions[effect_func_id.0 as usize];901 let mut seen_blocks: FxHashSet<BlockId> = FxHashSet::default();902903 struct DerivedSetStateCall {904 callee_loc: Option<SourceLocation>,905 callee_id: IdentifierId,906 callee_identifier_name: Option<String>,907 source_ids: IndexSet<IdentifierId, FxBuildHasher>,908 }909910 let mut effect_derived_set_state_calls: Vec<DerivedSetStateCall> = Vec::new();911 let mut effect_set_state_usages: FxHashMap<IdentifierId, FxHashSet<LocKey>> =912 FxHashMap::default();913914 // Consider setStates in the effect's dependency array as being part of effectSetStateUsages915 for dep in dependencies {916 let root = get_root_set_state(917 dep.identifier,918 &context.set_state_loads,919 &mut FxHashSet::default(),920 );921 if let Some(root_id) = root {922 let mut set = FxHashSet::default();923 set.insert(LocKey::from_loc(&dep.loc));924 effect_set_state_usages.insert(root_id, set);925 }926 }927928 let mut cleanup_function_deps: Option<FxHashSet<IdentifierId>> = None;929 let mut globals: FxHashSet<IdentifierId> = FxHashSet::default();930931 for (_block_id, block) in &effect_function.body.blocks {932 // Check for return -> cleanup function933 if let react_compiler_hir::Terminal::Return {934 value,935 return_variant: ReturnVariant::Explicit,936 ..937 } = &block.terminal938 {939 let func_id = context.functions.get(&value.identifier).copied();940 cleanup_function_deps = get_fn_local_deps(func_id, env);941 }942943 // Skip if block has a back edge (pred not yet seen)944 let has_back_edge = block.preds.iter().any(|pred| !seen_blocks.contains(pred));945 if has_back_edge {946 return;947 }948949 for &instr_id in &block.instructions {950 let instr = &effect_function.instructions[instr_id.0 as usize];951952 // Early return if any instruction derives from a ref953 let lvalue_type =954 &types[identifiers[instr.lvalue.identifier.0 as usize].type_.0 as usize];955 if is_use_ref_type(lvalue_type) {956 return;957 }958959 // maybeRecordSetState for effect instructions960 maybe_record_set_state_for_instr(961 instr,962 env,963 &mut context.set_state_loads,964 &mut effect_set_state_usages,965 );966967 // Track setState usages for operands968 for (operand_id, operand_loc) in each_instruction_operand(instr, env) {969 if context.set_state_loads.contains_key(&operand_id) {970 let root = get_root_set_state(971 operand_id,972 &context.set_state_loads,973 &mut FxHashSet::default(),974 );975 if let Some(root_id) = root {976 if let Some(usages) = effect_set_state_usages.get_mut(&root_id) {977 usages.insert(LocKey::from_loc(&operand_loc));978 }979 }980 }981 }982983 match &instr.value {984 InstructionValue::CallExpression { callee, args, .. } => {985 let callee_type =986 &types[identifiers[callee.identifier.0 as usize].type_.0 as usize];987 if is_set_state_type(callee_type) && args.len() == 1 {988 if let react_compiler_hir::PlaceOrSpread::Place(arg0) = &args[0] {989 let callee_metadata =990 context.derivation_cache.cache.get(&callee.identifier);991992 // If the setState comes from a source other than local state, skip993 if let Some(cm) = callee_metadata {994 if cm.type_of_value != TypeOfValue::FromState {995 continue;996 }997 } else {998 continue;999 }10001001 let arg_metadata = context.derivation_cache.cache.get(&arg0.identifier);1002 if let Some(am) = arg_metadata {1003 // Get the user-visible identifier name, matching Babel's1004 // loc.identifierName. Falls back to extracting from source code.1005 let callee_ident_name = get_identifier_name_with_loc(1006 callee.identifier,1007 identifiers,1008 &callee.loc,1009 env.code.as_deref(),1010 );1011 effect_derived_set_state_calls.push(DerivedSetStateCall {1012 callee_loc: callee.loc,1013 callee_id: callee.identifier,1014 callee_identifier_name: callee_ident_name,1015 source_ids: am.source_ids.clone(),1016 });1017 }1018 }1019 } else {1020 // Check if callee is from props/propsAndState -> bail1021 let callee_metadata =1022 context.derivation_cache.cache.get(&callee.identifier);1023 if let Some(cm) = callee_metadata {1024 if cm.type_of_value == TypeOfValue::FromProps1025 || cm.type_of_value == TypeOfValue::FromPropsAndState1026 {1027 return;1028 }1029 }10301031 if globals.contains(&callee.identifier) {1032 return;1033 }1034 }1035 }1036 InstructionValue::LoadGlobal { .. } => {1037 globals.insert(instr.lvalue.identifier);1038 for (operand_id, _) in each_instruction_operand(instr, env) {1039 globals.insert(operand_id);1040 }1041 }1042 _ => {}1043 }1044 }1045 seen_blocks.insert(block.id);1046 }10471048 // Emit errors for derived setState calls1049 for derived in &effect_derived_set_state_calls {1050 let root_set_state_call = get_root_set_state(1051 derived.callee_id,1052 &context.set_state_loads,1053 &mut FxHashSet::default(),1054 );1055 if let Some(root_id) = root_set_state_call {1056 let effect_usage_count = effect_set_state_usages1057 .get(&root_id)1058 .map(|s| s.len())1059 .unwrap_or(0);1060 let total_usage_count = context1061 .set_state_usages1062 .get(&root_id)1063 .map(|s| s.len())1064 .unwrap_or(0);1065 if effect_set_state_usages.contains_key(&root_id)1066 && context.set_state_usages.contains_key(&root_id)1067 && effect_usage_count == total_usage_count - 11068 {1069 let mut props_set: IndexSet<String, FxBuildHasher> = IndexSet::default();1070 let mut state_set: IndexSet<String, FxBuildHasher> = IndexSet::default();10711072 let mut root_nodes_map: IndexMap<String, TreeNode, FxBuildHasher> =1073 IndexMap::default();1074 for id in &derived.source_ids {1075 let nodes = build_tree_node(*id, context, &FxHashSet::default());1076 for node in nodes {1077 if !root_nodes_map.contains_key(&node.name) {1078 root_nodes_map.insert(node.name.clone(), node);1079 }1080 }1081 }1082 let root_nodes: Vec<&TreeNode> = root_nodes_map.values().collect();10831084 let trees: Vec<String> = root_nodes1085 .iter()1086 .enumerate()1087 .map(|(index, node)| {1088 render_tree(1089 node,1090 "",1091 index == root_nodes.len() - 1,1092 &mut props_set,1093 &mut state_set,1094 )1095 })1096 .collect();10971098 // Check cleanup function dependencies1099 let should_skip = if let Some(ref cleanup_deps) = cleanup_function_deps {1100 derived1101 .source_ids1102 .iter()1103 .any(|dep| cleanup_deps.contains(dep))1104 } else {1105 false1106 };1107 if should_skip {1108 return;1109 }11101111 let mut root_sources = String::new();1112 if !props_set.is_empty() {1113 let props_list: Vec<&str> = props_set.iter().map(|s| s.as_str()).collect();1114 root_sources += &format!("Props: [{}]", props_list.join(", "));1115 }1116 if !state_set.is_empty() {1117 if !root_sources.is_empty() {1118 root_sources += "\n";1119 }1120 let state_list: Vec<&str> = state_set.iter().map(|s| s.as_str()).collect();1121 root_sources += &format!("State: [{}]", state_list.join(", "));1122 }11231124 let description = format!(1125 "Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\n\1126 This setState call is setting a derived value that depends on the following reactive sources:\n\n\1127 {}\n\n\1128 Data Flow Tree:\n\1129 {}\n\n\1130 See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",1131 root_sources,1132 trees.join("\n"),1133 );11341135 errors.push_diagnostic(1136 CompilerDiagnostic::new(1137 ErrorCategory::EffectDerivationsOfState,1138 "You might not need an effect. Derive values in render, not effects.",1139 Some(description),1140 )1141 .with_detail(CompilerDiagnosticDetail::Error {1142 loc: derived.callee_loc,1143 message: Some(1144 "This should be computed during render, not in an effect".to_string(),1145 ),1146 identifier_name: derived.callee_identifier_name.clone(),1147 }),1148 );1149 }1150 }1151 }1152}11531154// =============================================================================1155// Non-exp version: ValidateNoDerivedComputationsInEffects1156// Port of ValidateNoDerivedComputationsInEffects.ts1157// =============================================================================11581159/// Non-experimental version of the derived-computations-in-effects validation.1160/// Records errors directly on the Environment (matching TS `env.recordError()` behavior).1161pub fn validate_no_derived_computations_in_effects(1162 func: &HirFunction,1163 env: &mut Environment,1164) -> Result<(), CompilerError> {1165 // Phase 1: Collect effect call sites (func_id + resolved deps).1166 // Done with only immutable borrows of env fields.1167 let effects_to_validate: Vec<(FunctionId, Vec<IdentifierId>)> = {1168 let ids = &env.identifiers;1169 let tys = &env.types;1170 let mut candidate_deps: FxHashMap<IdentifierId, Vec<IdentifierId>> = FxHashMap::default();1171 let mut functions_map: FxHashMap<IdentifierId, FunctionId> = FxHashMap::default();1172 let mut locals_map: FxHashMap<IdentifierId, IdentifierId> = FxHashMap::default();1173 let mut result = Vec::new();11741175 for (_, block) in &func.body.blocks {1176 for &iid in &block.instructions {1177 let instr = &func.instructions[iid.0 as usize];1178 match &instr.value {1179 InstructionValue::LoadLocal { place, .. } => {1180 locals_map.insert(instr.lvalue.identifier, place.identifier);1181 }1182 InstructionValue::ArrayExpression { elements, .. } => {1183 let elem_ids: Vec<IdentifierId> = elements1184 .iter()1185 .filter_map(|e| match e {1186 ArrayElement::Place(p) => Some(p.identifier),1187 _ => None,1188 })1189 .collect();1190 if elem_ids.len() == elements.len() {1191 candidate_deps.insert(instr.lvalue.identifier, elem_ids);1192 }1193 }1194 InstructionValue::FunctionExpression { lowered_func, .. } => {1195 functions_map.insert(instr.lvalue.identifier, lowered_func.func);1196 }1197 InstructionValue::CallExpression { callee, args, .. } => {1198 let callee_ty = &tys[ids[callee.identifier.0 as usize].type_.0 as usize];1199 if is_use_effect_hook_type(callee_ty) && args.len() == 2 {1200 if let (PlaceOrSpread::Place(arg0), PlaceOrSpread::Place(arg1)) =1201 (&args[0], &args[1])1202 {1203 if let (Some(&func_id), Some(dep_elements)) = (1204 functions_map.get(&arg0.identifier),1205 candidate_deps.get(&arg1.identifier),1206 ) {1207 if !dep_elements.is_empty() {1208 let resolved: Vec<IdentifierId> = dep_elements1209 .iter()1210 .map(|d| locals_map.get(d).copied().unwrap_or(*d))1211 .collect();1212 result.push((func_id, resolved));1213 }1214 }1215 }1216 }1217 }1218 InstructionValue::MethodCall { property, args, .. } => {1219 let callee_ty = &tys[ids[property.identifier.0 as usize].type_.0 as usize];1220 if is_use_effect_hook_type(callee_ty) && args.len() == 2 {1221 if let (PlaceOrSpread::Place(arg0), PlaceOrSpread::Place(arg1)) =1222 (&args[0], &args[1])1223 {1224 if let (Some(&func_id), Some(dep_elements)) = (1225 functions_map.get(&arg0.identifier),1226 candidate_deps.get(&arg1.identifier),1227 ) {1228 if !dep_elements.is_empty() {1229 let resolved: Vec<IdentifierId> = dep_elements1230 .iter()1231 .map(|d| locals_map.get(d).copied().unwrap_or(*d))1232 .collect();1233 result.push((func_id, resolved));1234 }1235 }1236 }1237 }1238 }1239 _ => {}1240 }1241 }1242 }1243 result1244 };12451246 // Phase 2: Validate each collected effect and record error details.1247 // Uses ErrorDetail (flat loc format) to match TS behavior where1248 // env.recordError(new CompilerErrorDetail({...})) is used.1249 for (func_id, resolved_deps) in effects_to_validate {1250 let details = validate_effect_non_exp(1251 &env.functions[func_id.0 as usize],1252 &resolved_deps,1253 &env.identifiers,1254 &env.types,1255 );1256 for detail in details {1257 env.record_error(detail)?;1258 }1259 }1260 Ok(())1261}12621263fn validate_effect_non_exp(1264 effect_func: &HirFunction,1265 effect_deps: &[IdentifierId],1266 ids: &[Identifier],1267 tys: &[Type],1268) -> Vec<CompilerErrorDetail> {1269 // Check that the effect function only captures effect deps and setState1270 for ctx in &effect_func.context {1271 let ctx_ty = &tys[ids[ctx.identifier.0 as usize].type_.0 as usize];1272 if is_set_state_type(ctx_ty) {1273 continue;1274 } else if effect_deps.iter().any(|d| *d == ctx.identifier) {1275 continue;1276 } else {1277 return Vec::new();1278 }1279 }12801281 // Check that all effect deps are actually used in the function1282 for dep in effect_deps {1283 if !effect_func.context.iter().any(|c| c.identifier == *dep) {1284 return Vec::new();1285 }1286 }12871288 let mut seen_blocks: FxHashSet<BlockId> = FxHashSet::default();1289 let mut dep_values: FxHashMap<IdentifierId, Vec<IdentifierId>> = FxHashMap::default();1290 for dep in effect_deps {1291 dep_values.insert(*dep, vec![*dep]);1292 }12931294 let mut set_state_locs: Vec<SourceLocation> = Vec::new();12951296 for (_, block) in &effect_func.body.blocks {1297 for &pred in &block.preds {1298 if !seen_blocks.contains(&pred) {1299 return Vec::new();1300 }1301 }13021303 for phi in &block.phis {1304 let mut aggregate: FxHashSet<IdentifierId> = FxHashSet::default();1305 for operand in phi.operands.values() {1306 if let Some(deps) = dep_values.get(&operand.identifier) {1307 for d in deps {1308 aggregate.insert(*d);1309 }1310 }1311 }1312 if !aggregate.is_empty() {1313 dep_values.insert(phi.place.identifier, aggregate.into_iter().collect());1314 }1315 }13161317 for &iid in &block.instructions {1318 let instr = &effect_func.instructions[iid.0 as usize];1319 match &instr.value {1320 InstructionValue::Primitive { .. }1321 | InstructionValue::JSXText { .. }1322 | InstructionValue::LoadGlobal { .. } => {}1323 InstructionValue::LoadLocal { place, .. } => {1324 if let Some(deps) = dep_values.get(&place.identifier) {1325 dep_values.insert(instr.lvalue.identifier, deps.clone());1326 }1327 }1328 InstructionValue::ComputedLoad { .. }1329 | InstructionValue::PropertyLoad { .. }1330 | InstructionValue::BinaryExpression { .. }1331 | InstructionValue::TemplateLiteral { .. }1332 | InstructionValue::CallExpression { .. }1333 | InstructionValue::MethodCall { .. } => {1334 let mut aggregate: FxHashSet<IdentifierId> = FxHashSet::default();1335 for operand in non_exp_value_operands(&instr.value) {1336 if let Some(deps) = dep_values.get(&operand) {1337 for d in deps {1338 aggregate.insert(*d);1339 }1340 }1341 }1342 if !aggregate.is_empty() {1343 dep_values.insert(instr.lvalue.identifier, aggregate.into_iter().collect());1344 }13451346 if let InstructionValue::CallExpression { callee, args, .. } = &instr.value {1347 let callee_ty = &tys[ids[callee.identifier.0 as usize].type_.0 as usize];1348 if is_set_state_type(callee_ty) && args.len() == 1 {1349 if let PlaceOrSpread::Place(arg) = &args[0] {1350 if let Some(deps) = dep_values.get(&arg.identifier) {1351 let dep_set: FxHashSet<_> = deps.iter().collect();1352 if dep_set.len() == effect_deps.len() {1353 if let Some(loc) = callee.loc {1354 set_state_locs.push(loc);1355 }1356 } else {1357 return Vec::new();1358 }1359 } else {1360 return Vec::new();1361 }1362 }1363 }1364 }1365 }1366 _ => {1367 return Vec::new();1368 }1369 }1370 }13711372 match &block.terminal {1373 react_compiler_hir::Terminal::Return { value, .. }1374 | react_compiler_hir::Terminal::Throw { value, .. } => {1375 if dep_values.contains_key(&value.identifier) {1376 return Vec::new();1377 }1378 }1379 react_compiler_hir::Terminal::If { test, .. }1380 | react_compiler_hir::Terminal::Branch { test, .. } => {1381 if dep_values.contains_key(&test.identifier) {1382 return Vec::new();1383 }1384 }1385 react_compiler_hir::Terminal::Switch { test, .. } => {1386 if dep_values.contains_key(&test.identifier) {1387 return Vec::new();1388 }1389 }1390 _ => {}1391 }13921393 seen_blocks.insert(block.id);1394 }13951396 set_state_locs1397 .into_iter()1398 .map(|loc| {1399 CompilerErrorDetail {1400 category: ErrorCategory::EffectDerivationsOfState,1401 reason: "Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)".to_string(),1402 description: None,1403 loc: Some(loc),1404 suggestions: None,1405 }1406 })1407 .collect()1408}14091410/// Collects operand IdentifierIds for a subset of instruction variants used1411/// by `validate_effect_non_exp`.1412///1413/// NOTE: This intentionally does NOT use the canonical `each_instruction_value_operand`1414/// because: (1) `validate_effect_non_exp` only matches specific variants1415/// (ComputedLoad, PropertyLoad, BinaryExpression, TemplateLiteral, CallExpression,1416/// MethodCall), so FunctionExpression/ObjectMethod context handling is unnecessary;1417/// and (2) the caller does not have access to `env` which the canonical function requires1418/// for resolving function expression context captures.1419fn non_exp_value_operands(value: &InstructionValue) -> Vec<IdentifierId> {1420 match value {1421 InstructionValue::ComputedLoad {1422 object, property, ..1423 } => {1424 vec![object.identifier, property.identifier]1425 }1426 InstructionValue::PropertyLoad { object, .. } => vec![object.identifier],1427 InstructionValue::BinaryExpression { left, right, .. } => {1428 vec![left.identifier, right.identifier]1429 }1430 InstructionValue::TemplateLiteral { subexprs, .. } => {1431 subexprs.iter().map(|s| s.identifier).collect()1432 }1433 InstructionValue::CallExpression { callee, args, .. } => {1434 let mut op_ids = vec![callee.identifier];1435 for a in args {1436 match a {1437 PlaceOrSpread::Place(p) => op_ids.push(p.identifier),1438 PlaceOrSpread::Spread(s) => op_ids.push(s.place.identifier),1439 }1440 }1441 op_ids1442 }1443 InstructionValue::MethodCall {1444 receiver,1445 property,1446 args,1447 ..1448 } => {1449 let mut op_ids = vec![receiver.identifier, property.identifier];1450 for a in args {1451 match a {1452 PlaceOrSpread::Place(p) => op_ids.push(p.identifier),1453 PlaceOrSpread::Spread(s) => op_ids.push(s.place.identifier),1454 }1455 }1456 op_ids1457 }1458 _ => Vec::new(),1459 }1460}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.