main
rs 525 lines 21.5 KB
Raw
1 // Copyright (c) Meta Platforms, Inc. and affiliates.
2 //
3 // This source code is licensed under the MIT license found in the
4 // LICENSE file in the root directory of this source tree.
5
6 //! 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 not
11 //! called inside function expressions.
12
13 use rustc_hash::{FxBuildHasher, FxHashMap};
14
15 use indexmap::IndexMap;
16 use react_compiler_diagnostics::{
17 CompilerDiagnostic, CompilerError, CompilerErrorDetail, ErrorCategory, SourceLocation,
18 };
19 use react_compiler_hir::dominator::compute_unconditional_blocks;
20 use react_compiler_hir::environment::{Environment, is_hook_name};
21 use react_compiler_hir::object_shape::HookKind;
22 use react_compiler_hir::visitors::{each_pattern_operand, each_terminal_operand};
23 use react_compiler_hir::{
24 FunctionId, HirFunction, Identifier, IdentifierId, InstructionValue, ParamPattern, Place,
25 PropertyLiteral, Type, visitors,
26 };
27
28 /// Value classification for hook validation.
29 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
30 enum Kind {
31 Error,
32 KnownHook,
33 PotentialHook,
34 Global,
35 Local,
36 }
37
38 fn join_kinds(a: Kind, b: Kind) -> Kind {
39 if a == Kind::Error || b == Kind::Error {
40 Kind::Error
41 } else if a == Kind::KnownHook || b == Kind::KnownHook {
42 Kind::KnownHook
43 } else if a == Kind::PotentialHook || b == Kind::PotentialHook {
44 Kind::PotentialHook
45 } else if a == Kind::Global || b == Kind::Global {
46 Kind::Global
47 } else {
48 Kind::Local
49 }
50 }
51
52 fn 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 }
66
67 fn 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 false
73 }
74 }
75
76 fn 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 }
86
87 fn 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 }
99
100 fn 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 }
133
134 fn 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 }
164
165 fn 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 }
195
196 /// Validates hooks usage rules for a function.
197 pub 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();
205
206 // Process params
207 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 }
215
216 // Process blocks
217 for (_block_id, block) in &func.body.blocks {
218 // Process phis
219 for phi in &block.phis {
220 let mut kind = if ident_is_hook_name(phi.place.identifier, &env.identifiers) {
221 Kind::PotentialHook
222 } else {
223 Kind::Local
224 };
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 }
232
233 // Process instructions
234 for &instr_id in &block.instructions {
235 let instr = &func.instructions[instr_id.0 as usize];
236 let lvalue_id = instr.lvalue.identifier;
237
238 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::KnownHook
283 } else {
284 Kind::Local
285 }
286 }
287 Kind::PotentialHook => Kind::PotentialHook,
288 Kind::Global => {
289 if is_hook_property {
290 Kind::KnownHook
291 } else {
292 Kind::Global
293 }
294 }
295 Kind::Local => {
296 if is_hook_property {
297 Kind::PotentialHook
298 } else {
299 Kind::Local
300 }
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 callee
320 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::KnownHook
374 } else {
375 Kind::Global
376 }
377 }
378 Kind::Local => {
379 if is_hook_property {
380 Kind::PotentialHook
381 } else {
382 Kind::Local
383 }
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 kinds
395 // Matches TS which uses eachInstructionOperand + eachInstructionLValue
396 visit_all_operands(&instr.value, &value_kinds, &mut errors_by_loc, env)?;
397 // Set kind for instr.lvalue
398 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 }
408
409 // Visit terminal operands
410 for place in each_terminal_operand(&block.terminal) {
411 visit_place(&place, &value_kinds, &mut errors_by_loc, env)?;
412 }
413 }
414
415 // 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 }
421
422 /// Visit a function expression to check for hook calls inside it.
423 /// Processes instructions in order, visiting nested functions immediately
424 /// (before processing subsequent calls) to match TS error ordering.
425 fn 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 }
435
436 let func = &env.functions[func_id.0 as usize];
437 let mut items: Vec<Item> = Vec::new();
438
439 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 }
457
458 // Process items in instruction order (matching TS which visits nested
459 // 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 }
491
492 fn 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 }
511
512 /// Visit all operands of an instruction value (generic fallback).
513 /// Uses the canonical `each_instruction_value_operand` from visitors.
514 fn 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 }