main
rs 1,798 lines 73.3 KB
Raw
1 use rustc_hash::{FxHashMap, FxHashSet};
2
3 use react_compiler_diagnostics::{
4 CompilerDiagnostic, CompilerDiagnosticDetail, CompilerSuggestion, CompilerSuggestionOperation,
5 ErrorCategory, SourceLocation,
6 };
7 use react_compiler_hir::environment::Environment;
8 use react_compiler_hir::environment_config::ExhaustiveEffectDepsMode;
9 use react_compiler_hir::visitors::{
10 each_instruction_value_lvalue, each_instruction_value_operand_with_functions,
11 each_terminal_operand,
12 };
13 use react_compiler_hir::{
14 ArrayElement, BlockId, DependencyPathEntry, HirFunction, Identifier, IdentifierId,
15 InstructionKind, InstructionValue, ManualMemoDependency, ManualMemoDependencyRoot,
16 NonLocalBinding, ParamPattern, Place, PlaceOrSpread, PropertyLiteral, Terminal, Type,
17 };
18
19 /// Port of ValidateExhaustiveDependencies.ts
20 ///
21 /// Validates that existing manual memoization is exhaustive and does not
22 /// have extraneous dependencies. The goal is to ensure auto-memoization
23 /// will not substantially change program behavior.
24 ///
25 /// Note: takes `&mut HirFunction` (deviating from the read-only validation convention)
26 /// because it sets `has_invalid_deps` on StartMemoize instructions when validation
27 /// errors are found, so that ValidatePreservedManualMemoization can skip those blocks.
28 pub fn validate_exhaustive_dependencies(
29 func: &mut HirFunction,
30 env: &mut Environment,
31 ) -> Result<(), CompilerDiagnostic> {
32 let reactive = collect_reactive_identifiers(func, &env.functions);
33 let validate_memo = env.config.validate_exhaustive_memoization_dependencies;
34 let validate_effect = env.config.validate_exhaustive_effect_dependencies.clone();
35
36 let mut temporaries: FxHashMap<IdentifierId, Temporary> = FxHashMap::default();
37 for param in &func.params {
38 let place = match param {
39 ParamPattern::Place(p) => p,
40 ParamPattern::Spread(s) => &s.place,
41 };
42 temporaries.insert(
43 place.identifier,
44 Temporary::Local {
45 identifier: place.identifier,
46 path: Vec::new(),
47 context: false,
48 loc: place.loc,
49 },
50 );
51 }
52
53 let mut start_memo: Option<StartMemoInfo> = None;
54 let mut memo_locals: FxHashSet<IdentifierId> = FxHashSet::default();
55
56 // Callbacks struct holding the mutable state
57 let mut callbacks = Callbacks {
58 start_memo: &mut start_memo,
59 memo_locals: &mut memo_locals,
60 validate_memo,
61 validate_effect: validate_effect.clone(),
62 reactive: &reactive,
63 diagnostics: Vec::new(),
64 invalid_memo_ids: FxHashSet::default(),
65 };
66
67 collect_dependencies(
68 func,
69 &env.identifiers,
70 &env.types,
71 &env.functions,
72 &mut temporaries,
73 &mut Some(&mut callbacks),
74 false,
75 )?;
76
77 // Set has_invalid_deps on StartMemoize instructions that had validation errors
78 if !callbacks.invalid_memo_ids.is_empty() {
79 for instr in func.instructions.iter_mut() {
80 if let InstructionValue::StartMemoize {
81 manual_memo_id,
82 has_invalid_deps,
83 ..
84 } = &mut instr.value
85 {
86 if callbacks.invalid_memo_ids.contains(manual_memo_id) {
87 *has_invalid_deps = true;
88 }
89 }
90 }
91 }
92
93 // Record all diagnostics on the environment
94 for diagnostic in callbacks.diagnostics {
95 env.record_diagnostic(diagnostic);
96 }
97 Ok(())
98 }
99
100 // =============================================================================
101 // Internal types
102 // =============================================================================
103
104 /// Info extracted from a StartMemoize instruction
105 struct StartMemoInfo {
106 manual_memo_id: u32,
107 deps: Option<Vec<ManualMemoDependency>>,
108 deps_loc: Option<Option<SourceLocation>>,
109 #[allow(dead_code)]
110 loc: Option<SourceLocation>,
111 }
112
113 /// A temporary value tracked during dependency collection
114 #[derive(Debug, Clone)]
115 enum Temporary {
116 Local {
117 identifier: IdentifierId,
118 path: Vec<DependencyPathEntry>,
119 context: bool,
120 loc: Option<SourceLocation>,
121 },
122 Global {
123 binding: NonLocalBinding,
124 },
125 Aggregate {
126 dependencies: Vec<InferredDependency>,
127 loc: Option<SourceLocation>,
128 },
129 }
130
131 /// An inferred dependency (Local or Global)
132 #[derive(Debug, Clone)]
133 enum InferredDependency {
134 Local {
135 identifier: IdentifierId,
136 path: Vec<DependencyPathEntry>,
137 #[allow(dead_code)]
138 context: bool,
139 loc: Option<SourceLocation>,
140 },
141 Global {
142 binding: NonLocalBinding,
143 },
144 }
145
146 /// Hashable key for deduplicating inferred dependencies in a Set
147 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
148 enum InferredDependencyKey {
149 Local {
150 identifier: IdentifierId,
151 path_key: String,
152 },
153 Global {
154 name: String,
155 },
156 }
157
158 fn dep_to_key(dep: &InferredDependency) -> InferredDependencyKey {
159 match dep {
160 InferredDependency::Local {
161 identifier, path, ..
162 } => InferredDependencyKey::Local {
163 identifier: *identifier,
164 path_key: path_to_string(path),
165 },
166 InferredDependency::Global { binding } => InferredDependencyKey::Global {
167 name: binding.name().to_string(),
168 },
169 }
170 }
171
172 fn path_to_string(path: &[DependencyPathEntry]) -> String {
173 path.iter()
174 .map(|p| format!("{}{}", if p.optional { "?." } else { "." }, p.property))
175 .collect::<Vec<_>>()
176 .join("")
177 }
178
179 /// Callbacks for StartMemoize/FinishMemoize/Effect events
180 struct Callbacks<'a> {
181 start_memo: &'a mut Option<StartMemoInfo>,
182 #[allow(dead_code)]
183 memo_locals: &'a mut FxHashSet<IdentifierId>,
184 validate_memo: bool,
185 validate_effect: ExhaustiveEffectDepsMode,
186 reactive: &'a FxHashSet<IdentifierId>,
187 diagnostics: Vec<CompilerDiagnostic>,
188 /// manual_memo_ids that had validation errors (to set has_invalid_deps)
189 invalid_memo_ids: FxHashSet<u32>,
190 }
191
192 // =============================================================================
193 // Helper: type checking functions
194 // =============================================================================
195
196 fn is_effect_event_function_type(ty: &Type) -> bool {
197 matches!(ty, Type::Function { shape_id: Some(id), .. } if id == "BuiltInEffectEventFunction")
198 }
199
200 fn is_stable_type(ty: &Type) -> bool {
201 match ty {
202 Type::Function {
203 shape_id: Some(id), ..
204 } => matches!(
205 id.as_str(),
206 "BuiltInSetState"
207 | "BuiltInSetActionState"
208 | "BuiltInDispatch"
209 | "BuiltInStartTransition"
210 | "BuiltInSetOptimistic"
211 ),
212 Type::Object { shape_id: Some(id) } => matches!(id.as_str(), "BuiltInUseRefId"),
213 _ => false,
214 }
215 }
216
217 fn is_effect_hook(ty: &Type) -> bool {
218 matches!(ty, Type::Function { shape_id: Some(id), .. }
219 if id == "BuiltInUseEffectHook"
220 || id == "BuiltInUseLayoutEffectHook"
221 || id == "BuiltInUseInsertionEffectHook"
222 )
223 }
224
225 fn is_primitive_type(ty: &Type) -> bool {
226 matches!(ty, Type::Primitive)
227 }
228
229 fn is_use_ref_type(ty: &Type) -> bool {
230 matches!(ty, Type::Object { shape_id: Some(id) } if id == "BuiltInUseRefId")
231 }
232
233 fn get_identifier_type<'a>(
234 id: IdentifierId,
235 identifiers: &'a [Identifier],
236 types: &'a [Type],
237 ) -> &'a Type {
238 let ident = &identifiers[id.0 as usize];
239 &types[ident.type_.0 as usize]
240 }
241
242 fn get_identifier_name(id: IdentifierId, identifiers: &[Identifier]) -> Option<String> {
243 identifiers[id.0 as usize]
244 .name
245 .as_ref()
246 .map(|n| n.value().to_string())
247 }
248
249 // =============================================================================
250 // Path helpers (matching TS areEqualPaths, isSubPath, isSubPathIgnoringOptionals)
251 // =============================================================================
252
253 fn are_equal_paths(a: &[DependencyPathEntry], b: &[DependencyPathEntry]) -> bool {
254 a.len() == b.len()
255 && a.iter()
256 .zip(b.iter())
257 .all(|(ai, bi)| ai.property == bi.property && ai.optional == bi.optional)
258 }
259
260 fn is_sub_path(subpath: &[DependencyPathEntry], path: &[DependencyPathEntry]) -> bool {
261 subpath.len() <= path.len()
262 && subpath
263 .iter()
264 .zip(path.iter())
265 .all(|(a, b)| a.property == b.property && a.optional == b.optional)
266 }
267
268 fn is_sub_path_ignoring_optionals(
269 subpath: &[DependencyPathEntry],
270 path: &[DependencyPathEntry],
271 ) -> bool {
272 subpath.len() <= path.len()
273 && subpath
274 .iter()
275 .zip(path.iter())
276 .all(|(a, b)| a.property == b.property)
277 }
278
279 // =============================================================================
280 // Collect reactive identifiers
281 // =============================================================================
282
283 fn collect_reactive_identifiers(
284 func: &HirFunction,
285 functions: &[HirFunction],
286 ) -> FxHashSet<IdentifierId> {
287 let mut reactive = FxHashSet::default();
288 for (_block_id, block) in &func.body.blocks {
289 for &instr_id in &block.instructions {
290 let instr = &func.instructions[instr_id.0 as usize];
291 // Check instruction lvalue
292 if instr.lvalue.reactive {
293 reactive.insert(instr.lvalue.identifier);
294 }
295 // Check inner lvalues (Destructure patterns, StoreLocal, DeclareLocal, etc.)
296 // Matches TS eachInstructionLValue which yields both instr.lvalue and
297 // eachInstructionValueLValue(instr.value)
298 for lvalue in each_instruction_value_lvalue(&instr.value) {
299 if lvalue.reactive {
300 reactive.insert(lvalue.identifier);
301 }
302 }
303 for operand in each_instruction_value_operand_with_functions(&instr.value, functions) {
304 if operand.reactive {
305 reactive.insert(operand.identifier);
306 }
307 }
308 }
309 for operand in each_terminal_operand(&block.terminal) {
310 if operand.reactive {
311 reactive.insert(operand.identifier);
312 }
313 }
314 }
315 reactive
316 }
317
318 // =============================================================================
319 // findOptionalPlaces
320 // =============================================================================
321
322 fn find_optional_places(func: &HirFunction) -> FxHashMap<IdentifierId, bool> {
323 let mut optionals: FxHashMap<IdentifierId, bool> = FxHashMap::default();
324 let mut visited: FxHashSet<BlockId> = FxHashSet::default();
325
326 for (_block_id, block) in &func.body.blocks {
327 if visited.contains(&block.id) {
328 continue;
329 }
330 if let Terminal::Optional {
331 test,
332 fallthrough: optional_fallthrough,
333 optional,
334 ..
335 } = &block.terminal
336 {
337 visited.insert(block.id);
338 let mut test_block_id = *test;
339 let mut queue: Vec<Option<bool>> = vec![Some(*optional)];
340
341 'outer: loop {
342 let test_block = &func.body.blocks[&test_block_id];
343 visited.insert(test_block.id);
344 match &test_block.terminal {
345 Terminal::Branch {
346 test: test_place,
347 consequent,
348 fallthrough,
349 ..
350 } => {
351 let is_optional = queue
352 .pop()
353 .expect("Expected an optional value for each optional test condition");
354 if let Some(opt) = is_optional {
355 optionals.insert(test_place.identifier, opt);
356 }
357 if fallthrough == optional_fallthrough {
358 // Found the end of the optional chain
359 let consequent_block = &func.body.blocks[consequent];
360 if let Some(last_id) = consequent_block.instructions.last() {
361 let last_instr = &func.instructions[last_id.0 as usize];
362 if let InstructionValue::StoreLocal { value, .. } =
363 &last_instr.value
364 {
365 if let Some(opt) = is_optional {
366 optionals.insert(value.identifier, opt);
367 }
368 }
369 }
370 break 'outer;
371 } else {
372 test_block_id = *fallthrough;
373 }
374 }
375 Terminal::Optional {
376 optional: opt,
377 test: inner_test,
378 ..
379 } => {
380 queue.push(Some(*opt));
381 test_block_id = *inner_test;
382 }
383 Terminal::Logical {
384 test: inner_test, ..
385 }
386 | Terminal::Ternary {
387 test: inner_test, ..
388 } => {
389 queue.push(None);
390 test_block_id = *inner_test;
391 }
392 Terminal::Sequence {
393 block: seq_block, ..
394 } => {
395 test_block_id = *seq_block;
396 }
397 Terminal::MaybeThrow { continuation, .. } => {
398 test_block_id = *continuation;
399 }
400 _ => {
401 // Unexpected terminal in optional — skip rather than panic
402 break 'outer;
403 }
404 }
405 }
406 // TS asserts queue.length === 0 here, but we skip the assertion
407 // to avoid panicking on edge cases.
408 }
409 }
410
411 optionals
412 }
413
414 // =============================================================================
415 // Dependency collection
416 // =============================================================================
417
418 fn add_dependency(
419 dep: &Temporary,
420 dependencies: &mut Vec<InferredDependency>,
421 dep_keys: &mut FxHashSet<InferredDependencyKey>,
422 locals: &FxHashSet<IdentifierId>,
423 ) {
424 match dep {
425 Temporary::Aggregate {
426 dependencies: agg_deps,
427 ..
428 } => {
429 for d in agg_deps {
430 add_dependency_inferred(d, dependencies, dep_keys, locals);
431 }
432 }
433 Temporary::Global { binding } => {
434 let inferred = InferredDependency::Global {
435 binding: binding.clone(),
436 };
437 let key = dep_to_key(&inferred);
438 if dep_keys.insert(key) {
439 dependencies.push(inferred);
440 }
441 }
442 Temporary::Local {
443 identifier,
444 path,
445 context,
446 loc,
447 } => {
448 if !locals.contains(identifier) {
449 let inferred = InferredDependency::Local {
450 identifier: *identifier,
451 path: path.clone(),
452 context: *context,
453 loc: *loc,
454 };
455 let key = dep_to_key(&inferred);
456 if dep_keys.insert(key) {
457 dependencies.push(inferred);
458 }
459 }
460 }
461 }
462 }
463
464 fn add_dependency_inferred(
465 dep: &InferredDependency,
466 dependencies: &mut Vec<InferredDependency>,
467 dep_keys: &mut FxHashSet<InferredDependencyKey>,
468 locals: &FxHashSet<IdentifierId>,
469 ) {
470 match dep {
471 InferredDependency::Global { .. } => {
472 let key = dep_to_key(dep);
473 if dep_keys.insert(key) {
474 dependencies.push(dep.clone());
475 }
476 }
477 InferredDependency::Local { identifier, .. } => {
478 if !locals.contains(identifier) {
479 let key = dep_to_key(dep);
480 if dep_keys.insert(key) {
481 dependencies.push(dep.clone());
482 }
483 }
484 }
485 }
486 }
487
488 fn visit_candidate_dependency(
489 place: &Place,
490 temporaries: &FxHashMap<IdentifierId, Temporary>,
491 dependencies: &mut Vec<InferredDependency>,
492 dep_keys: &mut FxHashSet<InferredDependencyKey>,
493 locals: &FxHashSet<IdentifierId>,
494 ) {
495 if let Some(dep) = temporaries.get(&place.identifier) {
496 add_dependency(dep, dependencies, dep_keys, locals);
497 }
498 }
499
500 fn collect_dependencies(
501 func: &HirFunction,
502 identifiers: &[Identifier],
503 types: &[Type],
504 functions: &[HirFunction],
505 temporaries: &mut FxHashMap<IdentifierId, Temporary>,
506 callbacks: &mut Option<&mut Callbacks<'_>>,
507 is_function_expression: bool,
508 ) -> Result<Temporary, CompilerDiagnostic> {
509 let optionals = find_optional_places(func);
510 let mut locals: FxHashSet<IdentifierId> = FxHashSet::default();
511
512 if is_function_expression {
513 for param in &func.params {
514 let place = match param {
515 ParamPattern::Place(p) => p,
516 ParamPattern::Spread(s) => &s.place,
517 };
518 locals.insert(place.identifier);
519 }
520 }
521
522 let mut dependencies: Vec<InferredDependency> = Vec::new();
523 let mut dep_keys: FxHashSet<InferredDependencyKey> = FxHashSet::default();
524
525 // Saved state for when we're inside a memo block (StartMemoize..FinishMemoize).
526 // In TS, `dependencies` and `locals` are shared by reference between the main
527 // collection loop and the callbacks — StartMemoize clears them, FinishMemoize
528 // reads and clears them. We simulate this by saving/restoring.
529 let mut saved_dependencies: Option<Vec<InferredDependency>> = None;
530 let mut saved_dep_keys: Option<FxHashSet<InferredDependencyKey>> = None;
531 let mut saved_locals: Option<FxHashSet<IdentifierId>> = None;
532
533 for (_block_id, block) in &func.body.blocks {
534 // Process phis
535 for phi in &block.phis {
536 let mut deps: Vec<InferredDependency> = Vec::new();
537 for (_pred_id, operand) in &phi.operands {
538 if let Some(dep) = temporaries.get(&operand.identifier) {
539 match dep {
540 Temporary::Aggregate {
541 dependencies: agg, ..
542 } => {
543 deps.extend(agg.iter().cloned());
544 }
545 Temporary::Local {
546 identifier,
547 path,
548 context,
549 loc,
550 } => {
551 deps.push(InferredDependency::Local {
552 identifier: *identifier,
553 path: path.clone(),
554 context: *context,
555 loc: *loc,
556 });
557 }
558 Temporary::Global { binding } => {
559 deps.push(InferredDependency::Global {
560 binding: binding.clone(),
561 });
562 }
563 }
564 }
565 }
566 if deps.is_empty() {
567 continue;
568 } else if deps.len() == 1 {
569 let dep = &deps[0];
570 match dep {
571 InferredDependency::Local {
572 identifier,
573 path,
574 context,
575 loc,
576 } => {
577 temporaries.insert(
578 phi.place.identifier,
579 Temporary::Local {
580 identifier: *identifier,
581 path: path.clone(),
582 context: *context,
583 loc: *loc,
584 },
585 );
586 }
587 InferredDependency::Global { binding } => {
588 temporaries.insert(
589 phi.place.identifier,
590 Temporary::Global {
591 binding: binding.clone(),
592 },
593 );
594 }
595 }
596 } else {
597 temporaries.insert(
598 phi.place.identifier,
599 Temporary::Aggregate {
600 dependencies: deps,
601 loc: None,
602 },
603 );
604 }
605 }
606
607 // Process instructions
608 for &instr_id in &block.instructions {
609 let instr = &func.instructions[instr_id.0 as usize];
610 let lvalue_id = instr.lvalue.identifier;
611
612 match &instr.value {
613 InstructionValue::LoadGlobal { binding, .. } => {
614 temporaries.insert(
615 lvalue_id,
616 Temporary::Global {
617 binding: binding.clone(),
618 },
619 );
620 }
621 InstructionValue::LoadContext { place, .. }
622 | InstructionValue::LoadLocal { place, .. } => {
623 if let Some(temp) = temporaries.get(&place.identifier).cloned() {
624 match &temp {
625 Temporary::Local { .. } => {
626 // Update loc to the load site
627 let mut updated = temp.clone();
628 if let Temporary::Local { loc, .. } = &mut updated {
629 *loc = place.loc;
630 }
631 temporaries.insert(lvalue_id, updated);
632 }
633 _ => {
634 temporaries.insert(lvalue_id, temp);
635 }
636 }
637 if locals.contains(&place.identifier) {
638 locals.insert(lvalue_id);
639 }
640 }
641 }
642 InstructionValue::DeclareLocal {
643 lvalue: decl_lv, ..
644 } => {
645 temporaries.insert(
646 decl_lv.place.identifier,
647 Temporary::Local {
648 identifier: decl_lv.place.identifier,
649 path: Vec::new(),
650 context: false,
651 loc: decl_lv.place.loc,
652 },
653 );
654 locals.insert(decl_lv.place.identifier);
655 }
656 InstructionValue::StoreLocal {
657 lvalue: store_lv,
658 value: store_val,
659 ..
660 } => {
661 let has_name = identifiers[store_lv.place.identifier.0 as usize]
662 .name
663 .is_some();
664 if !has_name {
665 // Unnamed: propagate temporary
666 if let Some(temp) = temporaries.get(&store_val.identifier).cloned() {
667 temporaries.insert(store_lv.place.identifier, temp);
668 }
669 } else {
670 // Named: visit the value and create a new local
671 visit_candidate_dependency(
672 store_val,
673 temporaries,
674 &mut dependencies,
675 &mut dep_keys,
676 &locals,
677 );
678 if store_lv.kind != InstructionKind::Reassign {
679 temporaries.insert(
680 store_lv.place.identifier,
681 Temporary::Local {
682 identifier: store_lv.place.identifier,
683 path: Vec::new(),
684 context: false,
685 loc: store_lv.place.loc,
686 },
687 );
688 locals.insert(store_lv.place.identifier);
689 }
690 }
691 }
692 InstructionValue::DeclareContext {
693 lvalue: decl_lv, ..
694 } => {
695 temporaries.insert(
696 decl_lv.place.identifier,
697 Temporary::Local {
698 identifier: decl_lv.place.identifier,
699 path: Vec::new(),
700 context: true,
701 loc: decl_lv.place.loc,
702 },
703 );
704 }
705 InstructionValue::StoreContext {
706 lvalue: store_lv,
707 value: store_val,
708 ..
709 } => {
710 visit_candidate_dependency(
711 store_val,
712 temporaries,
713 &mut dependencies,
714 &mut dep_keys,
715 &locals,
716 );
717 if store_lv.kind != InstructionKind::Reassign {
718 temporaries.insert(
719 store_lv.place.identifier,
720 Temporary::Local {
721 identifier: store_lv.place.identifier,
722 path: Vec::new(),
723 context: true,
724 loc: store_lv.place.loc,
725 },
726 );
727 locals.insert(store_lv.place.identifier);
728 }
729 }
730 InstructionValue::Destructure {
731 value: destr_val,
732 lvalue: destr_lv,
733 ..
734 } => {
735 visit_candidate_dependency(
736 destr_val,
737 temporaries,
738 &mut dependencies,
739 &mut dep_keys,
740 &locals,
741 );
742 if destr_lv.kind != InstructionKind::Reassign {
743 for lv_place in each_instruction_value_lvalue(&instr.value) {
744 temporaries.insert(
745 lv_place.identifier,
746 Temporary::Local {
747 identifier: lv_place.identifier,
748 path: Vec::new(),
749 context: false,
750 loc: lv_place.loc,
751 },
752 );
753 locals.insert(lv_place.identifier);
754 }
755 }
756 }
757 InstructionValue::PropertyLoad {
758 object, property, ..
759 } => {
760 // Number properties or ref.current: visit the object directly
761 let is_numeric = matches!(property, PropertyLiteral::Number(_));
762 let is_ref_current =
763 is_use_ref_type(get_identifier_type(object.identifier, identifiers, types))
764 && *property == PropertyLiteral::String("current".to_string());
765
766 if is_numeric || is_ref_current {
767 visit_candidate_dependency(
768 object,
769 temporaries,
770 &mut dependencies,
771 &mut dep_keys,
772 &locals,
773 );
774 } else {
775 // Extend path
776 let obj_temp = temporaries.get(&object.identifier).cloned();
777 if let Some(Temporary::Local {
778 identifier,
779 path,
780 context,
781 ..
782 }) = obj_temp
783 {
784 let optional =
785 optionals.get(&object.identifier).copied().unwrap_or(false);
786 let mut new_path = path.clone();
787 new_path.push(DependencyPathEntry {
788 optional,
789 property: property.clone(),
790 loc: instr.value.loc().copied(),
791 });
792 temporaries.insert(
793 lvalue_id,
794 Temporary::Local {
795 identifier,
796 path: new_path,
797 context,
798 loc: instr.value.loc().copied(),
799 },
800 );
801 }
802 }
803 }
804 InstructionValue::FunctionExpression { lowered_func, .. }
805 | InstructionValue::ObjectMethod { lowered_func, .. } => {
806 let inner_func = &functions[lowered_func.func.0 as usize];
807 let function_deps = collect_dependencies(
808 inner_func,
809 identifiers,
810 types,
811 functions,
812 temporaries,
813 &mut None,
814 true,
815 )?;
816 temporaries.insert(lvalue_id, function_deps.clone());
817 add_dependency(&function_deps, &mut dependencies, &mut dep_keys, &locals);
818 }
819 InstructionValue::StartMemoize {
820 manual_memo_id,
821 deps,
822 deps_loc,
823 loc,
824 ..
825 } => {
826 if let Some(cb) = callbacks.as_mut() {
827 // onStartMemoize — mirrors TS behavior of clearing dependencies and locals
828 *cb.start_memo = Some(StartMemoInfo {
829 manual_memo_id: *manual_memo_id,
830 deps: deps.clone(),
831 deps_loc: *deps_loc,
832 loc: *loc,
833 });
834 // Save current state and clear, matching TS which clears the shared
835 // dependencies/locals sets on StartMemoize
836 saved_dependencies = Some(std::mem::take(&mut dependencies));
837 saved_dep_keys = Some(std::mem::take(&mut dep_keys));
838 saved_locals = Some(std::mem::take(&mut locals));
839 }
840 }
841 InstructionValue::FinishMemoize {
842 manual_memo_id,
843 decl,
844 ..
845 } => {
846 if let Some(cb) = callbacks.as_mut() {
847 // onFinishMemoize — mirrors TS behavior
848 let sm = cb.start_memo.take();
849 if let Some(sm) = sm {
850 assert_eq!(
851 sm.manual_memo_id, *manual_memo_id,
852 "Found FinishMemoize without corresponding StartMemoize"
853 );
854
855 if cb.validate_memo {
856 // Visit the decl to add it as a dependency candidate
857 // (matches TS: visitCandidateDependency(value.decl, ...))
858 visit_candidate_dependency(
859 decl,
860 temporaries,
861 &mut dependencies,
862 &mut dep_keys,
863 &locals,
864 );
865
866 // Use ALL dependencies collected since StartMemoize cleared the set.
867 // This matches TS: `const inferred = Array.from(dependencies)`
868 let inferred: Vec<InferredDependency> = dependencies.clone();
869
870 let diagnostic = validate_dependencies(
871 inferred,
872 &sm.deps.unwrap_or_default(),
873 cb.reactive,
874 sm.deps_loc.unwrap_or(None),
875 ErrorCategory::MemoDependencies,
876 "all",
877 identifiers,
878 types,
879 )?;
880 if let Some(diag) = diagnostic {
881 cb.diagnostics.push(diag);
882 cb.invalid_memo_ids.insert(sm.manual_memo_id);
883 }
884 }
885
886 // Restore saved state (matching TS: dependencies.clear(), locals.clear())
887 // We restore instead of just clearing because we need the outer deps back
888 if let Some(saved) = saved_dependencies.take() {
889 // Merge current memo-block deps into the restored outer deps
890 let memo_deps = std::mem::replace(&mut dependencies, saved);
891 let _memo_keys = std::mem::replace(
892 &mut dep_keys,
893 saved_dep_keys.take().unwrap_or_default(),
894 );
895 locals = saved_locals.take().unwrap_or_default();
896 // Add memo deps to outer deps (they're still valid outer deps)
897 for d in memo_deps {
898 let key = dep_to_key(&d);
899 if dep_keys.insert(key) {
900 dependencies.push(d);
901 }
902 }
903 }
904 }
905 }
906 }
907 InstructionValue::ArrayExpression { elements, loc, .. } => {
908 let mut array_deps: Vec<InferredDependency> = Vec::new();
909 let mut array_keys: FxHashSet<InferredDependencyKey> = FxHashSet::default();
910 let empty_locals = FxHashSet::default();
911 for elem in elements {
912 let place = match elem {
913 ArrayElement::Place(p) => Some(p),
914 ArrayElement::Spread(s) => Some(&s.place),
915 ArrayElement::Hole => None,
916 };
917 if let Some(place) = place {
918 // Visit with empty locals for manual deps
919 visit_candidate_dependency(
920 place,
921 temporaries,
922 &mut array_deps,
923 &mut array_keys,
924 &empty_locals,
925 );
926 // Visit normally
927 visit_candidate_dependency(
928 place,
929 temporaries,
930 &mut dependencies,
931 &mut dep_keys,
932 &locals,
933 );
934 }
935 }
936 temporaries.insert(
937 lvalue_id,
938 Temporary::Aggregate {
939 dependencies: array_deps,
940 loc: *loc,
941 },
942 );
943 }
944 InstructionValue::CallExpression { callee, args, .. } => {
945 // Check if this is an effect hook call
946 if let Some(cb) = callbacks.as_mut() {
947 let callee_ty = get_identifier_type(callee.identifier, identifiers, types);
948 if is_effect_hook(callee_ty)
949 && !matches!(cb.validate_effect, ExhaustiveEffectDepsMode::Off)
950 {
951 if args.len() >= 2 {
952 let fn_arg = match &args[0] {
953 PlaceOrSpread::Place(p) => Some(p),
954 _ => None,
955 };
956 let deps_arg = match &args[1] {
957 PlaceOrSpread::Place(p) => Some(p),
958 _ => None,
959 };
960 if let (Some(fn_place), Some(deps_place)) = (fn_arg, deps_arg) {
961 let fn_deps = temporaries.get(&fn_place.identifier).cloned();
962 let manual_deps =
963 temporaries.get(&deps_place.identifier).cloned();
964 if let (
965 Some(Temporary::Aggregate {
966 dependencies: fn_dep_list,
967 ..
968 }),
969 Some(Temporary::Aggregate {
970 dependencies: manual_dep_list,
971 loc: manual_loc,
972 }),
973 ) = (fn_deps, manual_deps)
974 {
975 let effect_report_mode = match &cb.validate_effect {
976 ExhaustiveEffectDepsMode::All => "all",
977 ExhaustiveEffectDepsMode::MissingOnly => "missing-only",
978 ExhaustiveEffectDepsMode::ExtraOnly => "extra-only",
979 ExhaustiveEffectDepsMode::Off => unreachable!(),
980 };
981 // Convert manual deps to ManualMemoDependency format
982 let manual_memo_deps: Vec<ManualMemoDependency> =
983 manual_dep_list
984 .iter()
985 .map(|dep| match dep {
986 InferredDependency::Local {
987 identifier,
988 path,
989 loc,
990 ..
991 } => ManualMemoDependency {
992 root: ManualMemoDependencyRoot::NamedLocal {
993 value: Place {
994 identifier: *identifier,
995 effect:
996 react_compiler_hir::Effect::Read,
997 reactive: cb
998 .reactive
999 .contains(identifier),
1000 loc: *loc,
1001 },
1002 constant: false,
1003 },
1004 path: path.clone(),
1005 loc: *loc,
1006 },
1007 InferredDependency::Global { binding } => {
1008 ManualMemoDependency {
1009 root:
1010 ManualMemoDependencyRoot::Global {
1011 identifier_name: binding
1012 .name()
1013 .to_string(),
1014 },
1015 path: Vec::new(),
1016 loc: None,
1017 }
1018 }
1019 })
1020 .collect();
1021
1022 let diagnostic = validate_dependencies(
1023 fn_dep_list,
1024 &manual_memo_deps,
1025 cb.reactive,
1026 manual_loc,
1027 ErrorCategory::EffectExhaustiveDependencies,
1028 effect_report_mode,
1029 identifiers,
1030 types,
1031 )?;
1032 if let Some(diag) = diagnostic {
1033 cb.diagnostics.push(diag);
1034 }
1035 }
1036 }
1037 }
1038 }
1039 }
1040
1041 // Visit all operands except for MethodCall's property
1042 for operand in
1043 each_instruction_value_operand_with_functions(&instr.value, functions)
1044 {
1045 visit_candidate_dependency(
1046 &operand,
1047 temporaries,
1048 &mut dependencies,
1049 &mut dep_keys,
1050 &locals,
1051 );
1052 }
1053 }
1054 InstructionValue::MethodCall {
1055 receiver,
1056 property,
1057 args,
1058 ..
1059 } => {
1060 // Check if this is an effect hook call
1061 if let Some(cb) = callbacks.as_mut() {
1062 let prop_ty = get_identifier_type(property.identifier, identifiers, types);
1063 if is_effect_hook(prop_ty)
1064 && !matches!(cb.validate_effect, ExhaustiveEffectDepsMode::Off)
1065 {
1066 if args.len() >= 2 {
1067 let fn_arg = match &args[0] {
1068 PlaceOrSpread::Place(p) => Some(p),
1069 _ => None,
1070 };
1071 let deps_arg = match &args[1] {
1072 PlaceOrSpread::Place(p) => Some(p),
1073 _ => None,
1074 };
1075 if let (Some(fn_place), Some(deps_place)) = (fn_arg, deps_arg) {
1076 let fn_deps = temporaries.get(&fn_place.identifier).cloned();
1077 let manual_deps =
1078 temporaries.get(&deps_place.identifier).cloned();
1079 if let (
1080 Some(Temporary::Aggregate {
1081 dependencies: fn_dep_list,
1082 ..
1083 }),
1084 Some(Temporary::Aggregate {
1085 dependencies: manual_dep_list,
1086 loc: manual_loc,
1087 }),
1088 ) = (fn_deps, manual_deps)
1089 {
1090 let effect_report_mode = match &cb.validate_effect {
1091 ExhaustiveEffectDepsMode::All => "all",
1092 ExhaustiveEffectDepsMode::MissingOnly => "missing-only",
1093 ExhaustiveEffectDepsMode::ExtraOnly => "extra-only",
1094 ExhaustiveEffectDepsMode::Off => unreachable!(),
1095 };
1096 let manual_memo_deps: Vec<ManualMemoDependency> =
1097 manual_dep_list
1098 .iter()
1099 .map(|dep| match dep {
1100 InferredDependency::Local {
1101 identifier,
1102 path,
1103 loc,
1104 ..
1105 } => ManualMemoDependency {
1106 root: ManualMemoDependencyRoot::NamedLocal {
1107 value: Place {
1108 identifier: *identifier,
1109 effect:
1110 react_compiler_hir::Effect::Read,
1111 reactive: cb
1112 .reactive
1113 .contains(identifier),
1114 loc: *loc,
1115 },
1116 constant: false,
1117 },
1118 path: path.clone(),
1119 loc: *loc,
1120 },
1121 InferredDependency::Global { binding } => {
1122 ManualMemoDependency {
1123 root:
1124 ManualMemoDependencyRoot::Global {
1125 identifier_name: binding
1126 .name()
1127 .to_string(),
1128 },
1129 path: Vec::new(),
1130 loc: None,
1131 }
1132 }
1133 })
1134 .collect();
1135
1136 let diagnostic = validate_dependencies(
1137 fn_dep_list,
1138 &manual_memo_deps,
1139 cb.reactive,
1140 manual_loc,
1141 ErrorCategory::EffectExhaustiveDependencies,
1142 effect_report_mode,
1143 identifiers,
1144 types,
1145 )?;
1146 if let Some(diag) = diagnostic {
1147 cb.diagnostics.push(diag);
1148 }
1149 }
1150 }
1151 }
1152 }
1153 }
1154
1155 // Visit operands, skipping the method property itself
1156 visit_candidate_dependency(
1157 receiver,
1158 temporaries,
1159 &mut dependencies,
1160 &mut dep_keys,
1161 &locals,
1162 );
1163 // Skip property — matches TS behavior
1164 for arg in args {
1165 let place = match arg {
1166 PlaceOrSpread::Place(p) => p,
1167 PlaceOrSpread::Spread(s) => &s.place,
1168 };
1169 visit_candidate_dependency(
1170 place,
1171 temporaries,
1172 &mut dependencies,
1173 &mut dep_keys,
1174 &locals,
1175 );
1176 }
1177 }
1178 _ => {
1179 // Default: visit all operands
1180 for operand in
1181 each_instruction_value_operand_with_functions(&instr.value, functions)
1182 {
1183 visit_candidate_dependency(
1184 &operand,
1185 temporaries,
1186 &mut dependencies,
1187 &mut dep_keys,
1188 &locals,
1189 );
1190 }
1191 // Track lvalues as locals
1192 for lv in each_instruction_lvalue_ids(&instr.value, lvalue_id) {
1193 locals.insert(lv);
1194 }
1195 }
1196 }
1197 }
1198
1199 // Terminal operands
1200 for operand in &each_terminal_operand(&block.terminal) {
1201 if optionals.contains_key(&operand.identifier) {
1202 continue;
1203 }
1204 visit_candidate_dependency(
1205 operand,
1206 temporaries,
1207 &mut dependencies,
1208 &mut dep_keys,
1209 &locals,
1210 );
1211 }
1212 }
1213
1214 Ok(Temporary::Aggregate {
1215 dependencies,
1216 loc: None,
1217 })
1218 }
1219
1220 // =============================================================================
1221 // validateDependencies
1222 // =============================================================================
1223
1224 fn validate_dependencies(
1225 mut inferred: Vec<InferredDependency>,
1226 manual_dependencies: &[ManualMemoDependency],
1227 reactive: &FxHashSet<IdentifierId>,
1228 manual_memo_loc: Option<SourceLocation>,
1229 category: ErrorCategory,
1230 exhaustive_deps_report_mode: &str,
1231 identifiers: &[Identifier],
1232 types: &[Type],
1233 ) -> Result<Option<CompilerDiagnostic>, CompilerDiagnostic> {
1234 // Sort dependencies by name and path
1235 inferred.sort_by(|a, b| {
1236 match (a, b) {
1237 (
1238 InferredDependency::Global { binding: ab },
1239 InferredDependency::Global { binding: bb },
1240 ) => ab.name().cmp(bb.name()),
1241 (
1242 InferredDependency::Local {
1243 identifier: a_id,
1244 path: a_path,
1245 ..
1246 },
1247 InferredDependency::Local {
1248 identifier: b_id,
1249 path: b_path,
1250 ..
1251 },
1252 ) => {
1253 let a_name = get_identifier_name(*a_id, identifiers);
1254 let b_name = get_identifier_name(*b_id, identifiers);
1255 match (a_name.as_deref(), b_name.as_deref()) {
1256 (Some(an), Some(bn)) => {
1257 if *a_id != *b_id {
1258 an.cmp(bn)
1259 } else if a_path.len() != b_path.len() {
1260 a_path.len().cmp(&b_path.len())
1261 } else {
1262 // Compare path entries
1263 for (ap, bp) in a_path.iter().zip(b_path.iter()) {
1264 let a_opt = if ap.optional { 0i32 } else { 1 };
1265 let b_opt = if bp.optional { 0i32 } else { 1 };
1266 if a_opt != b_opt {
1267 return a_opt.cmp(&b_opt);
1268 }
1269 let prop_cmp =
1270 ap.property.to_string().cmp(&bp.property.to_string());
1271 if prop_cmp != std::cmp::Ordering::Equal {
1272 return prop_cmp;
1273 }
1274 }
1275 std::cmp::Ordering::Equal
1276 }
1277 }
1278 _ => std::cmp::Ordering::Equal,
1279 }
1280 }
1281 (
1282 InferredDependency::Global { binding: ab },
1283 InferredDependency::Local {
1284 identifier: b_id, ..
1285 },
1286 ) => {
1287 let a_name = ab.name();
1288 let b_name = get_identifier_name(*b_id, identifiers);
1289 match b_name.as_deref() {
1290 Some(bn) => a_name.cmp(bn),
1291 None => std::cmp::Ordering::Equal,
1292 }
1293 }
1294 (
1295 InferredDependency::Local {
1296 identifier: a_id, ..
1297 },
1298 InferredDependency::Global { binding: bb },
1299 ) => {
1300 let a_name = get_identifier_name(*a_id, identifiers);
1301 let b_name = bb.name();
1302 match a_name.as_deref() {
1303 Some(an) => an.cmp(b_name),
1304 None => std::cmp::Ordering::Equal,
1305 }
1306 }
1307 }
1308 });
1309
1310 // Remove redundant inferred dependencies
1311 // retainWhere logic: keep dep[ix] only if no earlier entry is equal or a subpath prefix
1312 // Mirrors TS: retainWhere(inferred, (dep, ix) => {
1313 // const match = inferred.findIndex(prevDep => isEqualTemporary(prevDep, dep) || ...);
1314 // return match === -1 || match >= ix;
1315 // })
1316 {
1317 let snapshot = inferred.clone();
1318 let mut write_index = 0;
1319 for ix in 0..snapshot.len() {
1320 let dep = &snapshot[ix];
1321 let first_match = snapshot.iter().position(|prev_dep| {
1322 is_equal_temporary(prev_dep, dep)
1323 || (matches!(
1324 (prev_dep, dep),
1325 (
1326 InferredDependency::Local { .. },
1327 InferredDependency::Local { .. }
1328 )
1329 ) && {
1330 if let (
1331 InferredDependency::Local {
1332 identifier: prev_id,
1333 path: prev_path,
1334 ..
1335 },
1336 InferredDependency::Local {
1337 identifier: dep_id,
1338 path: dep_path,
1339 ..
1340 },
1341 ) = (prev_dep, dep)
1342 {
1343 prev_id == dep_id && is_sub_path(prev_path, dep_path)
1344 } else {
1345 false
1346 }
1347 })
1348 });
1349
1350 let keep = match first_match {
1351 None => true,
1352 Some(m) => m >= ix,
1353 };
1354 if keep {
1355 inferred[write_index] = snapshot[ix].clone();
1356 write_index += 1;
1357 }
1358 }
1359 inferred.truncate(write_index);
1360 }
1361
1362 // Validate manual deps
1363 let mut matched: FxHashSet<usize> = FxHashSet::default(); // indices into manual_dependencies
1364 let mut missing: Vec<&InferredDependency> = Vec::new();
1365 let mut extra: Vec<&ManualMemoDependency> = Vec::new();
1366
1367 for inferred_dep in &inferred {
1368 match inferred_dep {
1369 InferredDependency::Global { binding } => {
1370 for (i, manual_dep) in manual_dependencies.iter().enumerate() {
1371 if let ManualMemoDependencyRoot::Global { identifier_name } = &manual_dep.root {
1372 if identifier_name == binding.name() {
1373 matched.insert(i);
1374 extra.push(manual_dep);
1375 }
1376 }
1377 }
1378 continue;
1379 }
1380 InferredDependency::Local {
1381 identifier,
1382 path,
1383 loc: _,
1384 ..
1385 } => {
1386 // Skip effect event functions
1387 let ty = get_identifier_type(*identifier, identifiers, types);
1388 if is_effect_event_function_type(ty) {
1389 continue;
1390 }
1391
1392 let mut has_matching = false;
1393 for (i, manual_dep) in manual_dependencies.iter().enumerate() {
1394 if let ManualMemoDependencyRoot::NamedLocal { value, .. } = &manual_dep.root {
1395 if value.identifier == *identifier
1396 && (are_equal_paths(&manual_dep.path, path)
1397 || is_sub_path_ignoring_optionals(&manual_dep.path, path))
1398 {
1399 has_matching = true;
1400 matched.insert(i);
1401 }
1402 }
1403 }
1404
1405 if has_matching || is_optional_dependency(*identifier, reactive, identifiers, types)
1406 {
1407 continue;
1408 }
1409
1410 missing.push(inferred_dep);
1411 }
1412 }
1413 }
1414
1415 // Check for extra dependencies
1416 for (i, dep) in manual_dependencies.iter().enumerate() {
1417 if matched.contains(&i) {
1418 continue;
1419 }
1420 if let ManualMemoDependencyRoot::NamedLocal {
1421 constant, value, ..
1422 } = &dep.root
1423 {
1424 if *constant {
1425 let dep_ty = get_identifier_type(value.identifier, identifiers, types);
1426 // Constant-folded primitives: skip
1427 if !value.reactive && is_primitive_type(dep_ty) {
1428 continue;
1429 }
1430 }
1431 }
1432 extra.push(dep);
1433 }
1434
1435 // Filter based on report mode
1436 let filtered_missing: Vec<&InferredDependency> = if exhaustive_deps_report_mode == "extra-only"
1437 {
1438 Vec::new()
1439 } else {
1440 missing
1441 };
1442 let filtered_extra: Vec<&ManualMemoDependency> =
1443 if exhaustive_deps_report_mode == "missing-only" {
1444 Vec::new()
1445 } else {
1446 extra
1447 };
1448
1449 if filtered_missing.is_empty() && filtered_extra.is_empty() {
1450 return Ok(None);
1451 }
1452
1453 // Build suggestion when we have valid index info (matches TS behavior)
1454 let suggestion = manual_memo_loc.and_then(|loc| {
1455 let start_index = loc.start.index?;
1456 let end_index = loc.end.index?;
1457 let text = format!(
1458 "[{}]",
1459 inferred
1460 .iter()
1461 .filter(|dep| {
1462 match dep {
1463 InferredDependency::Local { identifier, .. } => {
1464 let ty = get_identifier_type(*identifier, identifiers, types);
1465 !is_optional_dependency(*identifier, reactive, identifiers, types)
1466 && !is_effect_event_function_type(ty)
1467 }
1468 InferredDependency::Global { .. } => false,
1469 }
1470 })
1471 .map(|dep| print_inferred_dependency(dep, identifiers))
1472 .collect::<Vec<_>>()
1473 .join(", ")
1474 );
1475 Some(CompilerSuggestion {
1476 op: CompilerSuggestionOperation::Replace,
1477 range: (start_index as usize, end_index as usize),
1478 description: "Update dependencies".to_string(),
1479 text: Some(text),
1480 })
1481 });
1482
1483 let mut diagnostic = create_diagnostic(
1484 category,
1485 &filtered_missing,
1486 &filtered_extra,
1487 suggestion,
1488 identifiers,
1489 )?;
1490
1491 // Add detail items for missing deps
1492 for dep in &filtered_missing {
1493 if let InferredDependency::Local {
1494 identifier,
1495 path: _,
1496 loc,
1497 ..
1498 } = dep
1499 {
1500 let mut hint = String::new();
1501 let ty = get_identifier_type(*identifier, identifiers, types);
1502 if is_stable_type(ty) {
1503 hint = ". Refs, setState functions, and other \"stable\" values generally do not need to be added as dependencies, but this variable may change over time to point to different values".to_string();
1504 }
1505 let dep_str = print_inferred_dependency(dep, identifiers);
1506 diagnostic.details.push(CompilerDiagnosticDetail::Error {
1507 loc: *loc,
1508 message: Some(format!("Missing dependency `{dep_str}`{hint}")),
1509 identifier_name: None,
1510 });
1511 }
1512 }
1513
1514 // Add detail items for extra deps
1515 for dep in &filtered_extra {
1516 match &dep.root {
1517 ManualMemoDependencyRoot::Global { .. } => {
1518 let dep_str = print_manual_memo_dependency(dep, identifiers);
1519 diagnostic.details.push(CompilerDiagnosticDetail::Error {
1520 loc: dep.loc.or(manual_memo_loc),
1521 message: Some(format!(
1522 "Unnecessary dependency `{dep_str}`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change"
1523 )),
1524 identifier_name: None,
1525 });
1526 }
1527 ManualMemoDependencyRoot::NamedLocal { value, .. } => {
1528 // Check if there's a matching inferred dep
1529 let matching_inferred = inferred.iter().find(|inf_dep| {
1530 if let InferredDependency::Local {
1531 identifier: inf_id,
1532 path: inf_path,
1533 ..
1534 } = inf_dep
1535 {
1536 *inf_id == value.identifier
1537 && is_sub_path_ignoring_optionals(inf_path, &dep.path)
1538 } else {
1539 false
1540 }
1541 });
1542
1543 if let Some(matching) = matching_inferred {
1544 if let InferredDependency::Local { identifier, .. } = matching {
1545 let matching_ty = get_identifier_type(*identifier, identifiers, types);
1546 if is_effect_event_function_type(matching_ty) {
1547 let dep_str = print_manual_memo_dependency(dep, identifiers);
1548 diagnostic.details.push(CompilerDiagnosticDetail::Error {
1549 loc: dep.loc.or(manual_memo_loc),
1550 message: Some(format!(
1551 "Functions returned from `useEffectEvent` must not be included in the dependency array. Remove `{dep_str}` from the dependencies."
1552 )),
1553 identifier_name: None,
1554 });
1555 } else if !is_optional_dependency_inferred(
1556 matching,
1557 reactive,
1558 identifiers,
1559 types,
1560 ) {
1561 let dep_str = print_manual_memo_dependency(dep, identifiers);
1562 let inferred_str = print_inferred_dependency(matching, identifiers);
1563 diagnostic.details.push(CompilerDiagnosticDetail::Error {
1564 loc: dep.loc.or(manual_memo_loc),
1565 message: Some(format!(
1566 "Overly precise dependency `{dep_str}`, use `{inferred_str}` instead"
1567 )),
1568 identifier_name: None,
1569 });
1570 } else {
1571 let dep_str = print_manual_memo_dependency(dep, identifiers);
1572 diagnostic.details.push(CompilerDiagnosticDetail::Error {
1573 loc: dep.loc.or(manual_memo_loc),
1574 message: Some(format!("Unnecessary dependency `{dep_str}`")),
1575 identifier_name: None,
1576 });
1577 }
1578 }
1579 } else {
1580 let dep_str = print_manual_memo_dependency(dep, identifiers);
1581 diagnostic.details.push(CompilerDiagnosticDetail::Error {
1582 loc: dep.loc.or(manual_memo_loc),
1583 message: Some(format!("Unnecessary dependency `{dep_str}`")),
1584 identifier_name: None,
1585 });
1586 }
1587 }
1588 }
1589 }
1590
1591 // Add hint showing inferred dependencies when a suggestion was generated
1592 // (matches TS: only adds hint when suggestion != null, using suggestion.text)
1593 if let Some(ref suggestions) = diagnostic.suggestions {
1594 if let Some(suggestion) = suggestions.first() {
1595 if let Some(ref text) = suggestion.text {
1596 diagnostic.details.push(CompilerDiagnosticDetail::Hint {
1597 message: format!("Inferred dependencies: `{text}`"),
1598 });
1599 }
1600 }
1601 }
1602
1603 Ok(Some(diagnostic))
1604 }
1605
1606 // =============================================================================
1607 // Printing helpers
1608 // =============================================================================
1609
1610 fn print_inferred_dependency(dep: &InferredDependency, identifiers: &[Identifier]) -> String {
1611 match dep {
1612 InferredDependency::Global { binding } => binding.name().to_string(),
1613 InferredDependency::Local {
1614 identifier, path, ..
1615 } => {
1616 let name = get_identifier_name(*identifier, identifiers)
1617 .unwrap_or_else(|| "<unnamed>".to_string());
1618 let path_str: String = path
1619 .iter()
1620 .map(|p| format!("{}.{}", if p.optional { "?" } else { "" }, p.property))
1621 .collect();
1622 format!("{name}{path_str}")
1623 }
1624 }
1625 }
1626
1627 fn print_manual_memo_dependency(dep: &ManualMemoDependency, identifiers: &[Identifier]) -> String {
1628 let name = match &dep.root {
1629 ManualMemoDependencyRoot::Global { identifier_name } => identifier_name.clone(),
1630 ManualMemoDependencyRoot::NamedLocal { value, .. } => {
1631 get_identifier_name(value.identifier, identifiers)
1632 .unwrap_or_else(|| "<unnamed>".to_string())
1633 }
1634 };
1635 let path_str: String = dep
1636 .path
1637 .iter()
1638 .map(|p| format!("{}.{}", if p.optional { "?" } else { "" }, p.property))
1639 .collect();
1640 format!("{name}{path_str}")
1641 }
1642
1643 // =============================================================================
1644 // Optional dependency check
1645 // =============================================================================
1646
1647 fn is_optional_dependency(
1648 identifier: IdentifierId,
1649 reactive: &FxHashSet<IdentifierId>,
1650 identifiers: &[Identifier],
1651 types: &[Type],
1652 ) -> bool {
1653 if reactive.contains(&identifier) {
1654 return false;
1655 }
1656 let ty = get_identifier_type(identifier, identifiers, types);
1657 is_stable_type(ty) || is_primitive_type(ty)
1658 }
1659
1660 fn is_optional_dependency_inferred(
1661 dep: &InferredDependency,
1662 reactive: &FxHashSet<IdentifierId>,
1663 identifiers: &[Identifier],
1664 types: &[Type],
1665 ) -> bool {
1666 match dep {
1667 InferredDependency::Local { identifier, .. } => {
1668 is_optional_dependency(*identifier, reactive, identifiers, types)
1669 }
1670 InferredDependency::Global { .. } => false,
1671 }
1672 }
1673
1674 // =============================================================================
1675 // Equality check for temporaries
1676 // =============================================================================
1677
1678 fn is_equal_temporary(a: &InferredDependency, b: &InferredDependency) -> bool {
1679 match (a, b) {
1680 (
1681 InferredDependency::Global { binding: ab },
1682 InferredDependency::Global { binding: bb },
1683 ) => ab.name() == bb.name(),
1684 (
1685 InferredDependency::Local {
1686 identifier: a_id,
1687 path: a_path,
1688 ..
1689 },
1690 InferredDependency::Local {
1691 identifier: b_id,
1692 path: b_path,
1693 ..
1694 },
1695 ) => a_id == b_id && are_equal_paths(a_path, b_path),
1696 _ => false,
1697 }
1698 }
1699
1700 // =============================================================================
1701 // createDiagnostic
1702 // =============================================================================
1703
1704 fn create_diagnostic(
1705 category: ErrorCategory,
1706 missing: &[&InferredDependency],
1707 extra: &[&ManualMemoDependency],
1708 suggestion: Option<CompilerSuggestion>,
1709 _identifiers: &[Identifier],
1710 ) -> Result<CompilerDiagnostic, CompilerDiagnostic> {
1711 let missing_str = if !missing.is_empty() {
1712 Some("missing")
1713 } else {
1714 None
1715 };
1716 let extra_str = if !extra.is_empty() {
1717 Some("extra")
1718 } else {
1719 None
1720 };
1721
1722 let (reason, description) = match category {
1723 ErrorCategory::MemoDependencies => {
1724 let reason_parts: Vec<&str> =
1725 [missing_str, extra_str].iter().filter_map(|x| *x).collect();
1726 let reason = format!("Found {} memoization dependencies", reason_parts.join("/"));
1727
1728 let desc_parts: Vec<&str> = [
1729 if !missing.is_empty() {
1730 Some("Missing dependencies can cause a value to update less often than it should, resulting in stale UI")
1731 } else {
1732 None
1733 },
1734 if !extra.is_empty() {
1735 Some("Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often")
1736 } else {
1737 None
1738 },
1739 ]
1740 .iter()
1741 .filter_map(|x| *x)
1742 .collect();
1743 let description = desc_parts.join(". ");
1744 (reason, description)
1745 }
1746 ErrorCategory::EffectExhaustiveDependencies => {
1747 let reason_parts: Vec<&str> =
1748 [missing_str, extra_str].iter().filter_map(|x| *x).collect();
1749 let reason = format!("Found {} effect dependencies", reason_parts.join("/"));
1750
1751 let desc_parts: Vec<&str> = [
1752 if !missing.is_empty() {
1753 Some("Missing dependencies can cause an effect to fire less often than it should")
1754 } else {
1755 None
1756 },
1757 if !extra.is_empty() {
1758 Some("Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects")
1759 } else {
1760 None
1761 },
1762 ]
1763 .iter()
1764 .filter_map(|x| *x)
1765 .collect();
1766 let description = desc_parts.join(". ");
1767 (reason, description)
1768 }
1769 _ => {
1770 return Err(CompilerDiagnostic::new(
1771 ErrorCategory::Invariant,
1772 format!("Unexpected error category: {:?}", category),
1773 None,
1774 ));
1775 }
1776 };
1777
1778 Ok(CompilerDiagnostic {
1779 category,
1780 reason,
1781 description: Some(description),
1782 details: Vec::new(),
1783 suggestions: suggestion.map(|s| vec![s]),
1784 })
1785 }
1786
1787 /// Collect lvalue identifier ids from instruction value (for the default branch).
1788 /// Thin wrapper around canonical `each_instruction_value_lvalue` that maps to ids.
1789 fn each_instruction_lvalue_ids(
1790 value: &InstructionValue,
1791 lvalue_id: IdentifierId,
1792 ) -> Vec<IdentifierId> {
1793 let mut ids = vec![lvalue_id];
1794 for place in each_instruction_value_lvalue(value) {
1795 ids.push(place.identifier);
1796 }
1797 ids
1798 }