compiler/crates/react_compiler_validation/src/validate_use_memo.rs RUST 320 lines View on github.com → Search inside
1use rustc_hash::{FxHashMap, FxHashSet};23use react_compiler_diagnostics::{4    CompilerDiagnostic, CompilerDiagnosticDetail, CompilerError, ErrorCategory, SourceLocation,5};6use react_compiler_hir::environment::Environment;7use react_compiler_hir::visitors::{8    each_instruction_value_operand_with_functions, each_terminal_operand,9};10use react_compiler_hir::{11    FunctionId, HirFunction, IdentifierId, InstructionValue, ParamPattern, Place, PlaceOrSpread,12    ReturnVariant, Terminal,13};1415/// Validates useMemo() usage patterns.16///17/// Port of ValidateUseMemo.ts.18/// Returns VoidUseMemo errors separately (for logging via logErrors, not as compile errors).19pub fn validate_use_memo(func: &HirFunction, env: &mut Environment) -> CompilerError {20    validate_use_memo_impl(21        func,22        &env.functions,23        &mut env.errors,24        env.config.validate_no_void_use_memo,25    )26}2728/// Information about a FunctionExpression needed for validation.29struct FuncExprInfo {30    func_id: FunctionId,31    loc: Option<SourceLocation>,32}3334fn validate_use_memo_impl(35    func: &HirFunction,36    functions: &[HirFunction],37    errors: &mut CompilerError,38    validate_no_void_use_memo: bool,39) -> CompilerError {40    let mut void_memo_errors = CompilerError::new();41    let mut use_memos: FxHashSet<IdentifierId> = FxHashSet::default();42    let mut react: FxHashSet<IdentifierId> = FxHashSet::default();43    let mut func_exprs: FxHashMap<IdentifierId, FuncExprInfo> = FxHashMap::default();44    let mut unused_use_memos: FxHashMap<IdentifierId, (SourceLocation, Option<String>)> =45        FxHashMap::default();4647    for (_block_id, block) in &func.body.blocks {48        for &instr_id in &block.instructions {49            let instr = &func.instructions[instr_id.0 as usize];50            let lvalue = &instr.lvalue;51            let value = &instr.value;5253            // Remove used operands from unused_use_memos54            if !unused_use_memos.is_empty() {55                for operand_id in each_instruction_value_operand_ids(value, functions) {56                    unused_use_memos.remove(&operand_id);57                }58            }5960            match value {61                InstructionValue::LoadGlobal { binding, .. } => {62                    let name = binding.name();63                    if name == "useMemo" {64                        use_memos.insert(lvalue.identifier);65                    } else if name == "React" {66                        react.insert(lvalue.identifier);67                    }68                }69                InstructionValue::PropertyLoad {70                    object, property, ..71                } => {72                    if react.contains(&object.identifier) {73                        if let react_compiler_hir::PropertyLiteral::String(prop_name) = property {74                            if prop_name == "useMemo" {75                                use_memos.insert(lvalue.identifier);76                            }77                        }78                    }79                }80                InstructionValue::FunctionExpression {81                    lowered_func, loc, ..82                } => {83                    func_exprs.insert(84                        lvalue.identifier,85                        FuncExprInfo {86                            func_id: lowered_func.func,87                            loc: *loc,88                        },89                    );90                }91                InstructionValue::CallExpression { callee, args, .. } => {92                    handle_possible_use_memo_call(93                        functions,94                        errors,95                        &mut void_memo_errors,96                        &use_memos,97                        &func_exprs,98                        &mut unused_use_memos,99                        callee,100                        args,101                        lvalue,102                        validate_no_void_use_memo,103                    );104                }105                InstructionValue::MethodCall { property, args, .. } => {106                    handle_possible_use_memo_call(107                        functions,108                        errors,109                        &mut void_memo_errors,110                        &use_memos,111                        &func_exprs,112                        &mut unused_use_memos,113                        property,114                        args,115                        lvalue,116                        validate_no_void_use_memo,117                    );118                }119                _ => {}120            }121        }122123        // Check terminal operands for unused_use_memos124        if !unused_use_memos.is_empty() {125            for operand_id in each_terminal_operand_ids(&block.terminal) {126                unused_use_memos.remove(&operand_id);127            }128        }129    }130131    // Report unused useMemo results132    if !unused_use_memos.is_empty() {133        for (loc, ident_name) in unused_use_memos.values() {134            void_memo_errors.push_diagnostic(135                CompilerDiagnostic::new(136                    ErrorCategory::VoidUseMemo,137                    "useMemo() result is unused",138                    Some(139                        "This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects"140                            .to_string(),141                    ),142                )143                .with_detail(CompilerDiagnosticDetail::Error {144                    loc: Some(*loc),145                    message: Some("useMemo() result is unused".to_string()),146                    identifier_name: ident_name.clone(),147                }),148            );149        }150    }151152    void_memo_errors153}154155#[allow(clippy::too_many_arguments)]156fn handle_possible_use_memo_call(157    functions: &[HirFunction],158    errors: &mut CompilerError,159    void_memo_errors: &mut CompilerError,160    use_memos: &FxHashSet<IdentifierId>,161    func_exprs: &FxHashMap<IdentifierId, FuncExprInfo>,162    unused_use_memos: &mut FxHashMap<IdentifierId, (SourceLocation, Option<String>)>,163    callee: &Place,164    args: &[PlaceOrSpread],165    lvalue: &Place,166    validate_no_void_use_memo: bool,167) {168    let is_use_memo = use_memos.contains(&callee.identifier);169    if !is_use_memo || args.is_empty() {170        return;171    }172173    let first_arg = match &args[0] {174        PlaceOrSpread::Place(place) => place,175        PlaceOrSpread::Spread(_) => return,176    };177178    let body_info = match func_exprs.get(&first_arg.identifier) {179        Some(info) => info,180        None => return,181    };182183    let body_func = &functions[body_info.func_id.0 as usize];184185    // Validate no parameters186    if !body_func.params.is_empty() {187        let first_param = &body_func.params[0];188        let loc = match first_param {189            ParamPattern::Place(place) => place.loc,190            ParamPattern::Spread(spread) => spread.place.loc,191        };192        errors.push_diagnostic(193            CompilerDiagnostic::new(194                ErrorCategory::UseMemo,195                "useMemo() callbacks may not accept parameters",196                Some(197                    "useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation"198                        .to_string(),199                ),200            )201            .with_detail(CompilerDiagnosticDetail::Error {202                loc,203                message: Some("Callbacks with parameters are not supported".to_string()),204                identifier_name: None,205            }),206        );207    }208209    // Validate not async or generator210    if body_func.is_async || body_func.generator {211        errors.push_diagnostic(212            CompilerDiagnostic::new(213                ErrorCategory::UseMemo,214                "useMemo() callbacks may not be async or generator functions",215                Some(216                    "useMemo() callbacks are called once and must synchronously return a value"217                        .to_string(),218                ),219            )220            .with_detail(CompilerDiagnosticDetail::Error {221                loc: body_info.loc,222                message: Some("Async and generator functions are not supported".to_string()),223                identifier_name: None,224            }),225        );226    }227228    // Validate no context variable assignment229    validate_no_context_variable_assignment(body_func, errors);230231    if validate_no_void_use_memo && !has_non_void_return(body_func) {232        void_memo_errors.push_diagnostic(233            CompilerDiagnostic::new(234                ErrorCategory::VoidUseMemo,235                "useMemo() callbacks must return a value",236                Some(237                    "This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects"238                        .to_string(),239                ),240            )241            .with_detail(CompilerDiagnosticDetail::Error {242                loc: body_info.loc,243                message: Some("useMemo() callbacks must return a value".to_string()),244                identifier_name: None,245            }),246        );247    } else if validate_no_void_use_memo {248        if let Some(callee_loc) = callee.loc {249            // The callee is always useMemo/React.useMemo since we checked is_use_memo above.250            // The identifierName in Babel's AST SourceLocation is "useMemo".251            unused_use_memos.insert(lvalue.identifier, (callee_loc, Some("useMemo".to_string())));252        }253    }254}255256fn validate_no_context_variable_assignment(func: &HirFunction, errors: &mut CompilerError) {257    let context: FxHashSet<IdentifierId> =258        func.context.iter().map(|place| place.identifier).collect();259260    for (_block_id, block) in &func.body.blocks {261        for &instr_id in &block.instructions {262            let instr = &func.instructions[instr_id.0 as usize];263            if let InstructionValue::StoreContext { lvalue, .. } = &instr.value {264                if context.contains(&lvalue.place.identifier) {265                    errors.push_diagnostic(266                        CompilerDiagnostic::new(267                            ErrorCategory::UseMemo,268                            "useMemo() callbacks may not reassign variables declared outside of the callback",269                            Some(270                                "useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function"271                                    .to_string(),272                            ),273                        )274                        .with_detail(CompilerDiagnosticDetail::Error {275                            loc: lvalue.place.loc,276                            message: Some("Cannot reassign variable".to_string()),277                            identifier_name: None,278                        }),279                    );280                }281            }282        }283    }284}285286fn has_non_void_return(func: &HirFunction) -> bool {287    for (_block_id, block) in &func.body.blocks {288        if let Terminal::Return { return_variant, .. } = &block.terminal {289            if matches!(290                return_variant,291                ReturnVariant::Explicit | ReturnVariant::Implicit292            ) {293                return true;294            }295        }296    }297    false298}299300/// Collect all operand IdentifierIds from an InstructionValue.301/// Thin wrapper around canonical `each_instruction_value_operand_with_functions` that maps to ids.302fn each_instruction_value_operand_ids(303    value: &InstructionValue,304    functions: &[HirFunction],305) -> Vec<IdentifierId> {306    each_instruction_value_operand_with_functions(value, functions)307        .into_iter()308        .map(|p| p.identifier)309        .collect()310}311312/// Collect all operand IdentifierIds from a Terminal.313/// Thin wrapper around canonical `each_terminal_operand` that maps to ids.314fn each_terminal_operand_ids(terminal: &Terminal) -> Vec<IdentifierId> {315    each_terminal_operand(terminal)316        .into_iter()317        .map(|p| p.identifier)318        .collect()319}

Code quality findings 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 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 first_arg = match &args[0] {
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 body_func = &functions[body_info.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 first_param = &body_func.params[0];
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];
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
.to_string(),
Info: Usage of `#[allow(...)]` suppresses compiler lints. Ensure the allowance is justified, well-scoped, and ideally temporary. Overuse can hide potential issues.
info maintainability allow-lint
#[allow(clippy::too_many_arguments)]
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
unused_use_memos.insert(lvalue.identifier, (callee_loc, Some("useMemo".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
.to_string(),

Get this view in your editor

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