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 hooks usage rules.7//!8//! Port of ValidateHooksUsage.ts.9//! Ensures hooks are called unconditionally, not passed as values,10//! and not called dynamically. Also validates that hooks are not11//! called inside function expressions.1213use rustc_hash::{FxBuildHasher, FxHashMap};1415use indexmap::IndexMap;16use react_compiler_diagnostics::{17 CompilerDiagnostic, CompilerError, CompilerErrorDetail, ErrorCategory, SourceLocation,18};19use react_compiler_hir::dominator::compute_unconditional_blocks;20use react_compiler_hir::environment::{Environment, is_hook_name};21use react_compiler_hir::object_shape::HookKind;22use react_compiler_hir::visitors::{each_pattern_operand, each_terminal_operand};23use react_compiler_hir::{24 FunctionId, HirFunction, Identifier, IdentifierId, InstructionValue, ParamPattern, Place,25 PropertyLiteral, Type, visitors,26};2728/// Value classification for hook validation.29#[derive(Debug, Clone, Copy, PartialEq, Eq)]30enum Kind {31 Error,32 KnownHook,33 PotentialHook,34 Global,35 Local,36}3738fn join_kinds(a: Kind, b: Kind) -> Kind {39 if a == Kind::Error || b == Kind::Error {40 Kind::Error41 } else if a == Kind::KnownHook || b == Kind::KnownHook {42 Kind::KnownHook43 } else if a == Kind::PotentialHook || b == Kind::PotentialHook {44 Kind::PotentialHook45 } else if a == Kind::Global || b == Kind::Global {46 Kind::Global47 } else {48 Kind::Local49 }50}5152fn get_kind_for_place(53 place: &Place,54 value_kinds: &FxHashMap<IdentifierId, Kind>,55 identifiers: &[Identifier],56) -> Kind {57 let known_kind = value_kinds.get(&place.identifier).copied();58 let ident = &identifiers[place.identifier.0 as usize];59 if let Some(ref name) = ident.name {60 if is_hook_name(name.value()) {61 return join_kinds(known_kind.unwrap_or(Kind::Local), Kind::PotentialHook);62 }63 }64 known_kind.unwrap_or(Kind::Local)65}6667fn ident_is_hook_name(identifier_id: IdentifierId, identifiers: &[Identifier]) -> bool {68 let ident = &identifiers[identifier_id.0 as usize];69 if let Some(ref name) = ident.name {70 is_hook_name(name.value())71 } else {72 false73 }74}7576fn get_hook_kind_for_id<'a>(77 identifier_id: IdentifierId,78 identifiers: &[Identifier],79 types: &[Type],80 env: &'a Environment,81) -> Result<Option<&'a HookKind>, CompilerDiagnostic> {82 let identifier = &identifiers[identifier_id.0 as usize];83 let ty = &types[identifier.type_.0 as usize];84 env.get_hook_kind_for_type(ty)85}8687fn visit_place(88 place: &Place,89 value_kinds: &FxHashMap<IdentifierId, Kind>,90 errors_by_loc: &mut IndexMap<SourceLocation, CompilerErrorDetail, FxBuildHasher>,91 env: &mut Environment,92) -> Result<(), CompilerError> {93 let kind = value_kinds.get(&place.identifier).copied();94 if kind == Some(Kind::KnownHook) {95 record_invalid_hook_usage_error(place, errors_by_loc, env)?;96 }97 Ok(())98}99100fn record_conditional_hook_error(101 place: &Place,102 value_kinds: &mut FxHashMap<IdentifierId, Kind>,103 errors_by_loc: &mut IndexMap<SourceLocation, CompilerErrorDetail, FxBuildHasher>,104 env: &mut Environment,105) -> Result<(), CompilerError> {106 value_kinds.insert(place.identifier, Kind::Error);107 let reason = "Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)".to_string();108 if let Some(loc) = place.loc {109 let previous = errors_by_loc.get(&loc);110 if previous.is_none() || previous.unwrap().reason != reason {111 errors_by_loc.insert(112 loc,113 CompilerErrorDetail {114 category: ErrorCategory::Hooks,115 reason,116 description: None,117 loc: Some(loc),118 suggestions: None,119 },120 );121 }122 } else {123 env.record_error(CompilerErrorDetail {124 category: ErrorCategory::Hooks,125 reason,126 description: None,127 loc: None,128 suggestions: None,129 })?;130 }131 Ok(())132}133134fn record_invalid_hook_usage_error(135 place: &Place,136 errors_by_loc: &mut IndexMap<SourceLocation, CompilerErrorDetail, FxBuildHasher>,137 env: &mut Environment,138) -> Result<(), CompilerError> {139 let reason = "Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values".to_string();140 if let Some(loc) = place.loc {141 if !errors_by_loc.contains_key(&loc) {142 errors_by_loc.insert(143 loc,144 CompilerErrorDetail {145 category: ErrorCategory::Hooks,146 reason,147 description: None,148 loc: Some(loc),149 suggestions: None,150 },151 );152 }153 } else {154 env.record_error(CompilerErrorDetail {155 category: ErrorCategory::Hooks,156 reason,157 description: None,158 loc: None,159 suggestions: None,160 })?;161 }162 Ok(())163}164165fn record_dynamic_hook_usage_error(166 place: &Place,167 errors_by_loc: &mut IndexMap<SourceLocation, CompilerErrorDetail, FxBuildHasher>,168 env: &mut Environment,169) -> Result<(), CompilerError> {170 let reason = "Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks".to_string();171 if let Some(loc) = place.loc {172 if !errors_by_loc.contains_key(&loc) {173 errors_by_loc.insert(174 loc,175 CompilerErrorDetail {176 category: ErrorCategory::Hooks,177 reason,178 description: None,179 loc: Some(loc),180 suggestions: None,181 },182 );183 }184 } else {185 env.record_error(CompilerErrorDetail {186 category: ErrorCategory::Hooks,187 reason,188 description: None,189 loc: None,190 suggestions: None,191 })?;192 }193 Ok(())194}195196/// Validates hooks usage rules for a function.197pub fn validate_hooks_usage(198 func: &HirFunction,199 env: &mut Environment,200) -> Result<(), react_compiler_diagnostics::CompilerDiagnostic> {201 let unconditional_blocks = compute_unconditional_blocks(func, env.next_block_id().0)?;202 let mut errors_by_loc: IndexMap<SourceLocation, CompilerErrorDetail, FxBuildHasher> =203 IndexMap::default();204 let mut value_kinds: FxHashMap<IdentifierId, Kind> = FxHashMap::default();205206 // Process params207 for param in &func.params {208 let place = match param {209 ParamPattern::Place(p) => p,210 ParamPattern::Spread(s) => &s.place,211 };212 let kind = get_kind_for_place(place, &value_kinds, &env.identifiers);213 value_kinds.insert(place.identifier, kind);214 }215216 // Process blocks217 for (_block_id, block) in &func.body.blocks {218 // Process phis219 for phi in &block.phis {220 let mut kind = if ident_is_hook_name(phi.place.identifier, &env.identifiers) {221 Kind::PotentialHook222 } else {223 Kind::Local224 };225 for (_, operand) in &phi.operands {226 if let Some(&operand_kind) = value_kinds.get(&operand.identifier) {227 kind = join_kinds(kind, operand_kind);228 }229 }230 value_kinds.insert(phi.place.identifier, kind);231 }232233 // Process instructions234 for &instr_id in &block.instructions {235 let instr = &func.instructions[instr_id.0 as usize];236 let lvalue_id = instr.lvalue.identifier;237238 match &instr.value {239 InstructionValue::LoadGlobal { .. } => {240 if get_hook_kind_for_id(lvalue_id, &env.identifiers, &env.types, env)?.is_some()241 {242 value_kinds.insert(lvalue_id, Kind::KnownHook);243 } else {244 value_kinds.insert(lvalue_id, Kind::Global);245 }246 }247 InstructionValue::LoadContext { place, .. }248 | InstructionValue::LoadLocal { place, .. } => {249 visit_place(place, &value_kinds, &mut errors_by_loc, env)?;250 let kind = get_kind_for_place(place, &value_kinds, &env.identifiers);251 value_kinds.insert(lvalue_id, kind);252 }253 InstructionValue::StoreLocal { lvalue, value, .. }254 | InstructionValue::StoreContext { lvalue, value, .. } => {255 visit_place(value, &value_kinds, &mut errors_by_loc, env)?;256 let kind = join_kinds(257 get_kind_for_place(value, &value_kinds, &env.identifiers),258 get_kind_for_place(&lvalue.place, &value_kinds, &env.identifiers),259 );260 value_kinds.insert(lvalue.place.identifier, kind);261 value_kinds.insert(lvalue_id, kind);262 }263 InstructionValue::ComputedLoad { object, .. } => {264 visit_place(object, &value_kinds, &mut errors_by_loc, env)?;265 let kind = get_kind_for_place(object, &value_kinds, &env.identifiers);266 let lvalue_kind =267 get_kind_for_place(&instr.lvalue, &value_kinds, &env.identifiers);268 value_kinds.insert(lvalue_id, join_kinds(lvalue_kind, kind));269 }270 InstructionValue::PropertyLoad {271 object, property, ..272 } => {273 let object_kind = get_kind_for_place(object, &value_kinds, &env.identifiers);274 let is_hook_property = match property {275 PropertyLiteral::String(s) => is_hook_name(s),276 PropertyLiteral::Number(_) => false,277 };278 let kind = match object_kind {279 Kind::Error => Kind::Error,280 Kind::KnownHook => {281 if is_hook_property {282 Kind::KnownHook283 } else {284 Kind::Local285 }286 }287 Kind::PotentialHook => Kind::PotentialHook,288 Kind::Global => {289 if is_hook_property {290 Kind::KnownHook291 } else {292 Kind::Global293 }294 }295 Kind::Local => {296 if is_hook_property {297 Kind::PotentialHook298 } else {299 Kind::Local300 }301 }302 };303 value_kinds.insert(lvalue_id, kind);304 }305 InstructionValue::CallExpression { callee, args, .. } => {306 let callee_kind = get_kind_for_place(callee, &value_kinds, &env.identifiers);307 let is_hook_callee =308 callee_kind == Kind::KnownHook || callee_kind == Kind::PotentialHook;309 if is_hook_callee && !unconditional_blocks.contains(&block.id) {310 record_conditional_hook_error(311 callee,312 &mut value_kinds,313 &mut errors_by_loc,314 env,315 )?;316 } else if callee_kind == Kind::PotentialHook {317 record_dynamic_hook_usage_error(callee, &mut errors_by_loc, env)?;318 }319 // Visit all operands except callee320 for arg in args {321 let place = match arg {322 react_compiler_hir::PlaceOrSpread::Place(p) => p,323 react_compiler_hir::PlaceOrSpread::Spread(s) => &s.place,324 };325 visit_place(place, &value_kinds, &mut errors_by_loc, env)?;326 }327 }328 InstructionValue::MethodCall {329 receiver,330 property,331 args,332 ..333 } => {334 let callee_kind = get_kind_for_place(property, &value_kinds, &env.identifiers);335 let is_hook_callee =336 callee_kind == Kind::KnownHook || callee_kind == Kind::PotentialHook;337 if is_hook_callee && !unconditional_blocks.contains(&block.id) {338 record_conditional_hook_error(339 property,340 &mut value_kinds,341 &mut errors_by_loc,342 env,343 )?;344 } else if callee_kind == Kind::PotentialHook {345 record_dynamic_hook_usage_error(property, &mut errors_by_loc, env)?;346 }347 // Visit receiver and args (not property)348 visit_place(receiver, &value_kinds, &mut errors_by_loc, env)?;349 for arg in args {350 let place = match arg {351 react_compiler_hir::PlaceOrSpread::Place(p) => p,352 react_compiler_hir::PlaceOrSpread::Spread(s) => &s.place,353 };354 visit_place(place, &value_kinds, &mut errors_by_loc, env)?;355 }356 }357 InstructionValue::Destructure { lvalue, value, .. } => {358 visit_place(value, &value_kinds, &mut errors_by_loc, env)?;359 let object_kind = get_kind_for_place(value, &value_kinds, &env.identifiers);360 // Process instr.lvalue and all pattern operands (matching TS eachInstructionLValue)361 let pattern_places = each_pattern_operand(&lvalue.pattern);362 let all_lvalues =363 std::iter::once(instr.lvalue.clone()).chain(pattern_places.into_iter());364 for place in all_lvalues {365 let is_hook_property =366 ident_is_hook_name(place.identifier, &env.identifiers);367 let kind = match object_kind {368 Kind::Error => Kind::Error,369 Kind::KnownHook => Kind::KnownHook,370 Kind::PotentialHook => Kind::PotentialHook,371 Kind::Global => {372 if is_hook_property {373 Kind::KnownHook374 } else {375 Kind::Global376 }377 }378 Kind::Local => {379 if is_hook_property {380 Kind::PotentialHook381 } else {382 Kind::Local383 }384 }385 };386 value_kinds.insert(place.identifier, kind);387 }388 }389 InstructionValue::ObjectMethod { lowered_func, .. }390 | InstructionValue::FunctionExpression { lowered_func, .. } => {391 visit_function_expression(env, lowered_func.func)?;392 }393 _ => {394 // For all other instructions: visit operands, set lvalue kinds395 // Matches TS which uses eachInstructionOperand + eachInstructionLValue396 visit_all_operands(&instr.value, &value_kinds, &mut errors_by_loc, env)?;397 // Set kind for instr.lvalue398 let kind = get_kind_for_place(&instr.lvalue, &value_kinds, &env.identifiers);399 value_kinds.insert(lvalue_id, kind);400 // Also set kind for value-level lvalues (e.g. DeclareLocal, PrefixUpdate, PostfixUpdate)401 for lv in visitors::each_instruction_value_lvalue(&instr.value) {402 let lv_kind = get_kind_for_place(&lv, &value_kinds, &env.identifiers);403 value_kinds.insert(lv.identifier, lv_kind);404 }405 }406 }407 }408409 // Visit terminal operands410 for place in each_terminal_operand(&block.terminal) {411 visit_place(&place, &value_kinds, &mut errors_by_loc, env)?;412 }413 }414415 // Record all accumulated errors (in insertion order, matching TS Map iteration)416 for (_, error_detail) in errors_by_loc {417 env.record_error(error_detail)?;418 }419 Ok(())420}421422/// Visit a function expression to check for hook calls inside it.423/// Processes instructions in order, visiting nested functions immediately424/// (before processing subsequent calls) to match TS error ordering.425fn visit_function_expression(426 env: &mut Environment,427 func_id: FunctionId,428) -> Result<(), CompilerError> {429 // Collect items in instruction order to process them sequentially.430 // Each item is either a call to check or a nested function to visit.431 enum Item {432 Call(IdentifierId, Option<SourceLocation>),433 NestedFunc(FunctionId),434 }435436 let func = &env.functions[func_id.0 as usize];437 let mut items: Vec<Item> = Vec::new();438439 for (_block_id, block) in &func.body.blocks {440 for &instr_id in &block.instructions {441 let instr = &func.instructions[instr_id.0 as usize];442 match &instr.value {443 InstructionValue::ObjectMethod { lowered_func, .. }444 | InstructionValue::FunctionExpression { lowered_func, .. } => {445 items.push(Item::NestedFunc(lowered_func.func));446 }447 InstructionValue::CallExpression { callee, .. } => {448 items.push(Item::Call(callee.identifier, callee.loc));449 }450 InstructionValue::MethodCall { property, .. } => {451 items.push(Item::Call(property.identifier, property.loc));452 }453 _ => {}454 }455 }456 }457458 // Process items in instruction order (matching TS which visits nested459 // functions immediately before processing subsequent calls)460 for item in items {461 match item {462 Item::Call(identifier_id, loc) => {463 let identifier = &env.identifiers[identifier_id.0 as usize];464 let ty = &env.types[identifier.type_.0 as usize];465 let hook_kind = env.get_hook_kind_for_type(ty).ok().flatten().cloned();466 if let Some(hook_kind) = hook_kind {467 let description = format!(468 "Cannot call {} within a function expression",469 if hook_kind == HookKind::Custom {470 "hook"471 } else {472 hook_kind_display(&hook_kind)473 }474 );475 env.record_error(CompilerErrorDetail {476 category: ErrorCategory::Hooks,477 reason: "Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)".to_string(),478 description: Some(description),479 loc,480 suggestions: None,481 })?;482 }483 }484 Item::NestedFunc(nested_func_id) => {485 visit_function_expression(env, nested_func_id)?;486 }487 }488 }489 Ok(())490}491492fn hook_kind_display(kind: &HookKind) -> &'static str {493 match kind {494 HookKind::UseContext => "useContext",495 HookKind::UseState => "useState",496 HookKind::UseActionState => "useActionState",497 HookKind::UseReducer => "useReducer",498 HookKind::UseRef => "useRef",499 HookKind::UseEffect => "useEffect",500 HookKind::UseLayoutEffect => "useLayoutEffect",501 HookKind::UseInsertionEffect => "useInsertionEffect",502 HookKind::UseMemo => "useMemo",503 HookKind::UseCallback => "useCallback",504 HookKind::UseTransition => "useTransition",505 HookKind::UseImperativeHandle => "useImperativeHandle",506 HookKind::UseEffectEvent => "useEffectEvent",507 HookKind::UseOptimistic => "useOptimistic",508 HookKind::Custom => "hook",509 }510}511512/// Visit all operands of an instruction value (generic fallback).513/// Uses the canonical `each_instruction_value_operand` from visitors.514fn visit_all_operands(515 value: &InstructionValue,516 value_kinds: &FxHashMap<IdentifierId, Kind>,517 errors_by_loc: &mut IndexMap<SourceLocation, CompilerErrorDetail, FxBuildHasher>,518 env: &mut Environment,519) -> Result<(), CompilerError> {520 let operands = visitors::each_instruction_value_operand(value, &*env);521 for place in &operands {522 visit_place(place, value_kinds, errors_by_loc, env)?;523 }524 Ok(())525}