compiler/crates/react_compiler_validation/src/validate_hooks_usage.rs RUST 526 lines View on github.com → Search inside
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}

Code quality findings 14

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 = &identifiers[place.identifier.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 ident = &identifiers[identifier_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 identifier = &identifiers[identifier_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 = &types[identifier.type_.0 as usize];
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 previous.is_none() || previous.unwrap().reason != reason {
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 instr = &func.instructions[instr_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 func = &env.functions[func_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 instr = &func.instructions[instr_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 identifier = &env.identifiers[identifier_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 = &env.types[identifier.type_.0 as usize];
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
std::iter::once(instr.lvalue.clone()).chain(pattern_places.into_iter());
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
items.push(Item::NestedFunc(lowered_func.func));
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
items.push(Item::Call(callee.identifier, callee.loc));
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
items.push(Item::Call(property.identifier, property.loc));

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.