main
rs 774 lines 29.2 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 //! Port of ValidatePreservedManualMemoization.ts
7 //!
8 //! Validates that all explicit manual memoization (useMemo/useCallback) was
9 //! accurately preserved, and that no originally memoized values became
10 //! unmemoized in the output.
11
12 use rustc_hash::{FxHashMap, FxHashSet};
13
14 use react_compiler_diagnostics::{
15 CompilerDiagnostic, CompilerDiagnosticDetail, ErrorCategory, SourceLocation,
16 };
17 use react_compiler_hir::environment::Environment;
18 use react_compiler_hir::{
19 DeclarationId, DependencyPathEntry, Identifier, IdentifierId, IdentifierName, InstructionKind,
20 InstructionValue, ManualMemoDependency, ManualMemoDependencyRoot, Place, ReactiveBlock,
21 ReactiveFunction, ReactiveInstruction, ReactiveScopeBlock, ReactiveStatement, ReactiveValue,
22 ScopeId,
23 };
24
25 /// State tracked during manual memo validation within a StartMemoize..FinishMemoize range.
26 struct ManualMemoBlockState {
27 /// Reassigned temporaries (declaration_id -> set of identifier ids that were reassigned to it).
28 reassignments: FxHashMap<DeclarationId, FxHashSet<IdentifierId>>,
29 /// Source location of the StartMemoize instruction.
30 loc: Option<SourceLocation>,
31 /// Declarations produced within this manual memo block.
32 decls: FxHashSet<DeclarationId>,
33 /// Normalized deps from source (useMemo/useCallback dep array).
34 deps_from_source: Option<Vec<ManualMemoDependency>>,
35 /// Manual memo id from StartMemoize.
36 manual_memo_id: u32,
37 }
38
39 /// Top-level visitor state.
40 struct VisitorState<'a> {
41 env: &'a mut Environment,
42 manual_memo_state: Option<ManualMemoBlockState>,
43 /// Completed (non-pruned) scope IDs.
44 scopes: FxHashSet<ScopeId>,
45 /// Completed pruned scope IDs.
46 pruned_scopes: FxHashSet<ScopeId>,
47 /// Map from identifier ID to its normalized manual memo dependency.
48 temporaries: FxHashMap<IdentifierId, ManualMemoDependency>,
49 }
50
51 /// Validate that manual memoization (useMemo/useCallback) is preserved.
52 ///
53 /// Walks the reactive function looking for StartMemoize/FinishMemoize instructions
54 /// and checks that:
55 /// 1. Dependencies' scopes have completed before the memo block starts
56 /// 2. Memoized values are actually within scopes (not unmemoized)
57 /// 3. Inferred scope dependencies match the source dependencies
58 pub fn validate_preserved_manual_memoization(func: &ReactiveFunction, env: &mut Environment) {
59 let mut state = VisitorState {
60 env,
61 manual_memo_state: None,
62 scopes: FxHashSet::default(),
63 pruned_scopes: FxHashSet::default(),
64 temporaries: FxHashMap::default(),
65 };
66 visit_block(&func.body, &mut state);
67 }
68
69 fn is_named(ident: &Identifier) -> bool {
70 matches!(ident.name, Some(IdentifierName::Named(_)))
71 }
72
73 fn visit_block(block: &ReactiveBlock, state: &mut VisitorState) {
74 for stmt in block {
75 visit_statement(stmt, state);
76 }
77 }
78
79 fn visit_statement(stmt: &ReactiveStatement, state: &mut VisitorState) {
80 match stmt {
81 ReactiveStatement::Instruction(instr) => {
82 visit_instruction(instr, state);
83 }
84 ReactiveStatement::Terminal(terminal) => {
85 visit_terminal(terminal, state);
86 }
87 ReactiveStatement::Scope(scope_block) => {
88 visit_scope(scope_block, state);
89 }
90 ReactiveStatement::PrunedScope(pruned) => {
91 visit_pruned_scope(pruned, state);
92 }
93 }
94 }
95
96 fn visit_terminal(
97 terminal: &react_compiler_hir::ReactiveTerminalStatement,
98 state: &mut VisitorState,
99 ) {
100 use react_compiler_hir::ReactiveTerminal;
101 match &terminal.terminal {
102 ReactiveTerminal::If {
103 consequent,
104 alternate,
105 ..
106 } => {
107 visit_block(consequent, state);
108 if let Some(alt) = alternate {
109 visit_block(alt, state);
110 }
111 }
112 ReactiveTerminal::Switch { cases, .. } => {
113 for case in cases {
114 if let Some(ref block) = case.block {
115 visit_block(block, state);
116 }
117 }
118 }
119 ReactiveTerminal::For { loop_block, .. }
120 | ReactiveTerminal::ForOf { loop_block, .. }
121 | ReactiveTerminal::ForIn { loop_block, .. }
122 | ReactiveTerminal::While { loop_block, .. }
123 | ReactiveTerminal::DoWhile { loop_block, .. } => {
124 visit_block(loop_block, state);
125 }
126 ReactiveTerminal::Label { block, .. } => {
127 visit_block(block, state);
128 }
129 ReactiveTerminal::Try { block, handler, .. } => {
130 visit_block(block, state);
131 visit_block(handler, state);
132 }
133 _ => {}
134 }
135 }
136
137 fn visit_scope(scope_block: &ReactiveScopeBlock, state: &mut VisitorState) {
138 // Traverse the scope's instructions first
139 visit_block(&scope_block.instructions, state);
140
141 // After traversing, validate scope dependencies against manual memo deps
142 if let Some(ref memo_state) = state.manual_memo_state {
143 if let Some(ref deps_from_source) = memo_state.deps_from_source {
144 let scope = &state.env.scopes[scope_block.scope.0 as usize];
145 // `dependencies` still has to be cloned because `env` is passed
146 // mutably below. `temporaries`, `decls` and `deps_from_source` do
147 // not: they live in fields disjoint from `env`, so they can simply
148 // be borrowed.
149 let deps = scope.dependencies.clone();
150 let memo_loc = memo_state.loc;
151 for dep in &deps {
152 validate_inferred_dep(
153 dep.identifier,
154 &dep.path,
155 &state.temporaries,
156 &memo_state.decls,
157 deps_from_source,
158 state.env,
159 memo_loc,
160 );
161 }
162 }
163 }
164
165 // Mark scope and merged scopes as completed
166 let scope = &state.env.scopes[scope_block.scope.0 as usize];
167 let merged = scope.merged.clone();
168 state.scopes.insert(scope_block.scope);
169 for merged_id in merged {
170 state.scopes.insert(merged_id);
171 }
172 }
173
174 fn visit_pruned_scope(
175 pruned: &react_compiler_hir::PrunedReactiveScopeBlock,
176 state: &mut VisitorState,
177 ) {
178 visit_block(&pruned.instructions, state);
179 state.pruned_scopes.insert(pruned.scope);
180 }
181
182 fn visit_instruction(instr: &ReactiveInstruction, state: &mut VisitorState) {
183 // Record temporaries and deps in the instruction's value
184 record_temporaries(instr, state);
185
186 match &instr.value {
187 ReactiveValue::Instruction(InstructionValue::StartMemoize {
188 manual_memo_id,
189 deps,
190 has_invalid_deps,
191 ..
192 }) => {
193 // TS: CompilerError.invariant(state.manualMemoState == null, ...)
194 if state.manual_memo_state.is_some() {
195 return;
196 }
197
198 // TS: if (value.hasInvalidDeps === true) { return; }
199 if *has_invalid_deps {
200 return;
201 }
202
203 let deps_from_source = deps.clone();
204
205 state.manual_memo_state = Some(ManualMemoBlockState {
206 loc: instr.loc,
207 decls: FxHashSet::default(),
208 deps_from_source,
209 manual_memo_id: *manual_memo_id,
210 reassignments: FxHashMap::default(),
211 });
212
213 // Check that each dependency's scope has completed before the memo
214 // TS: for (const {identifier, loc} of eachInstructionValueOperand(value))
215 let operand_places = start_memoize_operands(deps);
216 for place in &operand_places {
217 let ident = &state.env.identifiers[place.identifier.0 as usize];
218 if let Some(scope_id) = ident.scope {
219 if !state.scopes.contains(&scope_id) && !state.pruned_scopes.contains(&scope_id)
220 {
221 let diag = CompilerDiagnostic::new(
222 ErrorCategory::PreserveManualMemo,
223 "Existing memoization could not be preserved",
224 Some(
225 "React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. \
226 This dependency may be mutated later, which could cause the value to change unexpectedly".to_string(),
227 ),
228 )
229 .with_detail(CompilerDiagnosticDetail::Error {
230 loc: place.loc,
231 message: Some(
232 "This dependency may be modified later".to_string(),
233 ),
234 identifier_name: None,
235 });
236 state.env.record_diagnostic(diag);
237 }
238 }
239 }
240 }
241 ReactiveValue::Instruction(InstructionValue::FinishMemoize {
242 decl,
243 pruned,
244 manual_memo_id,
245 ..
246 }) => {
247 if state.manual_memo_state.is_none() {
248 // StartMemoize had invalid deps, skip validation
249 return;
250 }
251
252 // TS: CompilerError.invariant(state.manualMemoState.manualMemoId === value.manualMemoId, ...)
253 if state
254 .manual_memo_state
255 .as_ref()
256 .map_or(true, |s| s.manual_memo_id != *manual_memo_id)
257 {
258 state.manual_memo_state = None;
259 return;
260 }
261
262 let memo_state = state.manual_memo_state.take().unwrap();
263
264 if !pruned {
265 // Check if the declared value is unmemoized
266 let decl_ident = &state.env.identifiers[decl.identifier.0 as usize];
267
268 if decl_ident.scope.is_none() {
269 // If the manual memo was inlined (useMemo -> IIFE), check reassignments
270 let decls_to_check = memo_state
271 .reassignments
272 .get(&decl_ident.declaration_id)
273 .map(|ids| ids.iter().copied().collect::<Vec<_>>())
274 .unwrap_or_else(|| vec![decl.identifier]);
275
276 for id in decls_to_check {
277 if is_unmemoized(id, &state.scopes, &state.env.identifiers) {
278 record_unmemoized_error(decl.loc, state.env);
279 }
280 }
281 } else {
282 // Single identifier with scope
283 if is_unmemoized(decl.identifier, &state.scopes, &state.env.identifiers) {
284 record_unmemoized_error(decl.loc, state.env);
285 }
286 }
287 }
288 }
289 ReactiveValue::Instruction(InstructionValue::StoreLocal { lvalue, value, .. }) => {
290 // Track reassignments from inlining of manual memo
291 if state.manual_memo_state.is_some() && lvalue.kind == InstructionKind::Reassign {
292 let decl_id =
293 state.env.identifiers[lvalue.place.identifier.0 as usize].declaration_id;
294 state
295 .manual_memo_state
296 .as_mut()
297 .unwrap()
298 .reassignments
299 .entry(decl_id)
300 .or_default()
301 .insert(value.identifier);
302 }
303 }
304 ReactiveValue::Instruction(InstructionValue::LoadLocal { place, .. }) => {
305 if state.manual_memo_state.is_some() {
306 let place_ident = &state.env.identifiers[place.identifier.0 as usize];
307 if let Some(ref lvalue) = instr.lvalue {
308 let lvalue_ident = &state.env.identifiers[lvalue.identifier.0 as usize];
309 if place_ident.scope.is_some() && lvalue_ident.scope.is_none() {
310 state
311 .manual_memo_state
312 .as_mut()
313 .unwrap()
314 .reassignments
315 .entry(lvalue_ident.declaration_id)
316 .or_default()
317 .insert(place.identifier);
318 }
319 }
320 }
321 }
322 _ => {}
323 }
324 }
325
326 fn record_unmemoized_error(loc: Option<SourceLocation>, env: &mut Environment) {
327 let diag = CompilerDiagnostic::new(
328 ErrorCategory::PreserveManualMemo,
329 "Existing memoization could not be preserved",
330 Some(
331 "React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output".to_string(),
332 ),
333 )
334 .with_detail(CompilerDiagnosticDetail::Error {
335 loc,
336 message: Some("Could not preserve existing memoization".to_string()),
337 identifier_name: None,
338 });
339 env.record_diagnostic(diag);
340 }
341
342 /// Record temporaries from an instruction.
343 /// TS: `recordTemporaries`
344 fn record_temporaries(instr: &ReactiveInstruction, state: &mut VisitorState) {
345 let lvalue = &instr.lvalue;
346 let lv_id = lvalue.as_ref().map(|lv| lv.identifier);
347 if let Some(id) = lv_id {
348 if state.temporaries.contains_key(&id) {
349 return;
350 }
351 }
352
353 if let Some(ref lvalue) = instr.lvalue {
354 let lv_ident = &state.env.identifiers[lvalue.identifier.0 as usize];
355 if is_named(lv_ident) && state.manual_memo_state.is_some() {
356 state
357 .manual_memo_state
358 .as_mut()
359 .unwrap()
360 .decls
361 .insert(lv_ident.declaration_id);
362 }
363 }
364
365 // Record deps from the instruction value first (before setting lvalue temporary)
366 record_deps_in_value(&instr.value, state);
367
368 // Then set the lvalue temporary (TS always sets this, even for unnamed lvalues)
369 if let Some(ref lvalue) = instr.lvalue {
370 state.temporaries.insert(
371 lvalue.identifier,
372 ManualMemoDependency {
373 root: ManualMemoDependencyRoot::NamedLocal {
374 value: lvalue.clone(),
375 constant: false,
376 },
377 path: Vec::new(),
378 loc: lvalue.loc,
379 },
380 );
381 }
382 }
383
384 /// Record dependencies from a reactive value.
385 /// TS: `recordDepsInValue`
386 fn record_deps_in_value(value: &ReactiveValue, state: &mut VisitorState) {
387 match value {
388 ReactiveValue::SequenceExpression {
389 instructions,
390 value,
391 ..
392 } => {
393 for instr in instructions {
394 visit_instruction(instr, state);
395 }
396 record_deps_in_value(value, state);
397 }
398 ReactiveValue::OptionalExpression { value: inner, .. } => {
399 record_deps_in_value(inner, state);
400 }
401 ReactiveValue::ConditionalExpression {
402 test,
403 consequent,
404 alternate,
405 ..
406 } => {
407 record_deps_in_value(test, state);
408 record_deps_in_value(consequent, state);
409 record_deps_in_value(alternate, state);
410 }
411 ReactiveValue::LogicalExpression { left, right, .. } => {
412 record_deps_in_value(left, state);
413 record_deps_in_value(right, state);
414 }
415 ReactiveValue::Instruction(iv) => {
416 // TS: collectMaybeMemoDependencies(value, this.temporaries, false)
417 // Called for side-effect of building up the dependency chain through
418 // LoadGlobal -> PropertyLoad -> ... The return value is discarded here
419 // (only used in DropManualMemoization's caller), but we need to store
420 // the result in temporaries for the lvalue of the enclosing instruction.
421 // That storage is handled by record_temporaries after this function returns.
422
423 // Track store targets within manual memo blocks
424 // TS: if (value.kind === 'StoreLocal' || value.kind === 'StoreContext' || value.kind === 'Destructure')
425 match iv {
426 InstructionValue::StoreLocal { lvalue, .. }
427 | InstructionValue::StoreContext { lvalue, .. } => {
428 if let Some(ref mut memo_state) = state.manual_memo_state {
429 let ident = &state.env.identifiers[lvalue.place.identifier.0 as usize];
430 memo_state.decls.insert(ident.declaration_id);
431 if is_named(ident) {
432 state.temporaries.insert(
433 lvalue.place.identifier,
434 ManualMemoDependency {
435 root: ManualMemoDependencyRoot::NamedLocal {
436 value: lvalue.place.clone(),
437 constant: false,
438 },
439 path: Vec::new(),
440 loc: lvalue.place.loc,
441 },
442 );
443 }
444 }
445 }
446 InstructionValue::Destructure { lvalue, .. } => {
447 if let Some(ref mut memo_state) = state.manual_memo_state {
448 for place in destructure_lvalue_places(&lvalue.pattern) {
449 let ident = &state.env.identifiers[place.identifier.0 as usize];
450 memo_state.decls.insert(ident.declaration_id);
451 if is_named(ident) {
452 state.temporaries.insert(
453 place.identifier,
454 ManualMemoDependency {
455 root: ManualMemoDependencyRoot::NamedLocal {
456 value: place.clone(),
457 constant: false,
458 },
459 path: Vec::new(),
460 loc: place.loc,
461 },
462 );
463 }
464 }
465 }
466 }
467 _ => {}
468 }
469 }
470 }
471 }
472
473 /// Get operand places from a StartMemoize instruction's deps.
474 fn start_memoize_operands(deps: &Option<Vec<ManualMemoDependency>>) -> Vec<Place> {
475 let mut result = Vec::new();
476 if let Some(deps) = deps {
477 for dep in deps {
478 if let ManualMemoDependencyRoot::NamedLocal { value, .. } = &dep.root {
479 result.push(value.clone());
480 }
481 }
482 }
483 result
484 }
485
486 /// Get lvalue places from a Destructure pattern.
487 fn destructure_lvalue_places(pattern: &react_compiler_hir::Pattern) -> Vec<&Place> {
488 let mut result = Vec::new();
489 match pattern {
490 react_compiler_hir::Pattern::Array(arr) => {
491 for item in &arr.items {
492 match item {
493 react_compiler_hir::ArrayPatternElement::Place(place) => {
494 result.push(place);
495 }
496 react_compiler_hir::ArrayPatternElement::Spread(spread) => {
497 result.push(&spread.place);
498 }
499 react_compiler_hir::ArrayPatternElement::Hole => {}
500 }
501 }
502 }
503 react_compiler_hir::Pattern::Object(obj) => {
504 for entry in &obj.properties {
505 match entry {
506 react_compiler_hir::ObjectPropertyOrSpread::Property(prop) => {
507 result.push(&prop.place);
508 }
509 react_compiler_hir::ObjectPropertyOrSpread::Spread(spread) => {
510 result.push(&spread.place);
511 }
512 }
513 }
514 }
515 }
516 result
517 }
518
519 /// Check if an identifier is unmemoized (has a scope that hasn't completed).
520 fn is_unmemoized(
521 id: IdentifierId,
522 completed_scopes: &FxHashSet<ScopeId>,
523 identifiers: &[Identifier],
524 ) -> bool {
525 let ident = &identifiers[id.0 as usize];
526 if let Some(scope_id) = ident.scope {
527 !completed_scopes.contains(&scope_id)
528 } else {
529 false
530 }
531 }
532
533 // =============================================================================
534 // Dependency comparison (port of compareDeps / validateInferredDep)
535 // =============================================================================
536
537 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
538 enum CompareDependencyResult {
539 Ok = 0,
540 RootDifference = 1,
541 PathDifference = 2,
542 Subpath = 3,
543 RefAccessDifference = 4,
544 }
545
546 fn compare_deps(
547 inferred: &ManualMemoDependency,
548 source: &ManualMemoDependency,
549 ) -> CompareDependencyResult {
550 let roots_equal = match (&inferred.root, &source.root) {
551 (
552 ManualMemoDependencyRoot::Global { identifier_name: a },
553 ManualMemoDependencyRoot::Global { identifier_name: b },
554 ) => a == b,
555 (
556 ManualMemoDependencyRoot::NamedLocal { value: a, .. },
557 ManualMemoDependencyRoot::NamedLocal { value: b, .. },
558 ) => a.identifier == b.identifier,
559 _ => false,
560 };
561 if !roots_equal {
562 return CompareDependencyResult::RootDifference;
563 }
564
565 let min_len = inferred.path.len().min(source.path.len());
566 let mut is_subpath = true;
567 for i in 0..min_len {
568 if inferred.path[i].property != source.path[i].property {
569 is_subpath = false;
570 break;
571 } else if inferred.path[i].optional != source.path[i].optional {
572 return CompareDependencyResult::PathDifference;
573 }
574 }
575
576 if is_subpath
577 && (source.path.len() == inferred.path.len()
578 || (inferred.path.len() >= source.path.len()
579 && !inferred.path.iter().any(|t| {
580 t.property == react_compiler_hir::PropertyLiteral::String("current".to_string())
581 })))
582 {
583 CompareDependencyResult::Ok
584 } else if is_subpath {
585 if source.path.iter().any(|t| {
586 t.property == react_compiler_hir::PropertyLiteral::String("current".to_string())
587 }) || inferred.path.iter().any(|t| {
588 t.property == react_compiler_hir::PropertyLiteral::String("current".to_string())
589 }) {
590 CompareDependencyResult::RefAccessDifference
591 } else {
592 CompareDependencyResult::Subpath
593 }
594 } else {
595 CompareDependencyResult::PathDifference
596 }
597 }
598
599 /// Pretty-print a reactive scope dependency (e.g., `x.a.b?.c`)
600 fn pretty_print_scope_dependency(
601 dep_id: IdentifierId,
602 dep_path: &[DependencyPathEntry],
603 identifiers: &[react_compiler_hir::Identifier],
604 ) -> String {
605 let ident = &identifiers[dep_id.0 as usize];
606 let root_str = match &ident.name {
607 Some(react_compiler_hir::IdentifierName::Named(n)) => n.clone(),
608 Some(react_compiler_hir::IdentifierName::Promoted(n)) => n.clone(),
609 None => "[unnamed]".to_string(),
610 };
611 let path_str: String = dep_path
612 .iter()
613 .map(|entry| {
614 let prop = match &entry.property {
615 react_compiler_hir::PropertyLiteral::String(s) => s.clone(),
616 react_compiler_hir::PropertyLiteral::Number(n) => format!("{}", n),
617 };
618 if entry.optional {
619 format!("?.{}", prop)
620 } else {
621 format!(".{}", prop)
622 }
623 })
624 .collect();
625 format!("{}{}", root_str, path_str)
626 }
627
628 /// Pretty-print a manual memo dependency for error messages.
629 fn print_manual_memo_dependency(
630 dep: &ManualMemoDependency,
631 identifiers: &[react_compiler_hir::Identifier],
632 with_optional: bool,
633 ) -> String {
634 let root_str = match &dep.root {
635 ManualMemoDependencyRoot::NamedLocal { value, .. } => {
636 let ident = &identifiers[value.identifier.0 as usize];
637 match &ident.name {
638 Some(react_compiler_hir::IdentifierName::Named(n)) => n.clone(),
639 Some(react_compiler_hir::IdentifierName::Promoted(n)) => n.clone(),
640 None => "[unnamed]".to_string(),
641 }
642 }
643 ManualMemoDependencyRoot::Global { identifier_name } => identifier_name.clone(),
644 };
645 let path_str: String = dep
646 .path
647 .iter()
648 .map(|entry| {
649 let prop = match &entry.property {
650 react_compiler_hir::PropertyLiteral::String(s) => s.clone(),
651 react_compiler_hir::PropertyLiteral::Number(n) => format!("{}", n),
652 };
653 if with_optional && entry.optional {
654 format!("?.{}", prop)
655 } else {
656 format!(".{}", prop)
657 }
658 })
659 .collect();
660 format!("{}{}", root_str, path_str)
661 }
662
663 fn get_compare_dependency_result_description(result: CompareDependencyResult) -> &'static str {
664 match result {
665 CompareDependencyResult::Ok => "Dependencies equal",
666 CompareDependencyResult::RootDifference | CompareDependencyResult::PathDifference => {
667 "Inferred different dependency than source"
668 }
669 CompareDependencyResult::RefAccessDifference => "Differences in ref.current access",
670 CompareDependencyResult::Subpath => "Inferred less specific property than source",
671 }
672 }
673
674 /// Validate that an inferred dependency matches a source dependency or was produced
675 /// within the manual memo block.
676 fn validate_inferred_dep(
677 dep_id: IdentifierId,
678 dep_path: &[DependencyPathEntry],
679 temporaries: &FxHashMap<IdentifierId, ManualMemoDependency>,
680 decls_within_memo_block: &FxHashSet<DeclarationId>,
681 valid_deps_in_memo_block: &[ManualMemoDependency],
682 env: &mut Environment,
683 memo_location: Option<SourceLocation>,
684 ) {
685 // Normalize the dependency through temporaries
686 let normalized_dep = if let Some(temp) = temporaries.get(&dep_id) {
687 let mut path = temp.path.clone();
688 path.extend_from_slice(dep_path);
689 ManualMemoDependency {
690 root: temp.root.clone(),
691 path,
692 loc: temp.loc,
693 }
694 } else {
695 let ident = &env.identifiers[dep_id.0 as usize];
696 // TS: CompilerError.invariant(dep.identifier.name?.kind === 'named', ...)
697 if !is_named(ident) {
698 return;
699 }
700 ManualMemoDependency {
701 root: ManualMemoDependencyRoot::NamedLocal {
702 value: Place {
703 identifier: dep_id,
704 effect: react_compiler_hir::Effect::Read,
705 reactive: false,
706 loc: ident.loc,
707 },
708 constant: false,
709 },
710 path: dep_path.to_vec(),
711 loc: ident.loc,
712 }
713 };
714
715 // Check if the dep was declared within the memo block
716 if let ManualMemoDependencyRoot::NamedLocal { value, .. } = &normalized_dep.root {
717 let ident = &env.identifiers[value.identifier.0 as usize];
718 if decls_within_memo_block.contains(&ident.declaration_id) {
719 return;
720 }
721 }
722
723 // Compare against each valid source dependency
724 let mut error_diagnostic: Option<CompareDependencyResult> = None;
725 for source_dep in valid_deps_in_memo_block {
726 let result = compare_deps(&normalized_dep, source_dep);
727 if result == CompareDependencyResult::Ok {
728 return;
729 }
730 error_diagnostic = Some(match error_diagnostic {
731 Some(prev) => prev.max(result),
732 None => result,
733 });
734 }
735
736 let ident = &env.identifiers[dep_id.0 as usize];
737
738 let extra = if is_named(ident) {
739 // Use the original dep_id/dep_path (matching TS prettyPrintScopeDependency(dep))
740 let dep_str = pretty_print_scope_dependency(dep_id, dep_path, &env.identifiers);
741 let source_deps_str: String = valid_deps_in_memo_block
742 .iter()
743 .map(|d| print_manual_memo_dependency(d, &env.identifiers, true))
744 .collect::<Vec<_>>()
745 .join(", ");
746 let result_desc = error_diagnostic
747 .map(|d| get_compare_dependency_result_description(d).to_string())
748 .unwrap_or_else(|| "Inferred dependency not present in source".to_string());
749 format!(
750 "The inferred dependency was `{}`, but the source dependencies were [{}]. {}",
751 dep_str, source_deps_str, result_desc
752 )
753 } else {
754 String::new()
755 };
756
757 let description = format!(
758 "React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. \
759 The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. {}",
760 extra
761 );
762
763 let diag = CompilerDiagnostic::new(
764 ErrorCategory::PreserveManualMemo,
765 "Existing memoization could not be preserved",
766 Some(description.trim().to_string()),
767 )
768 .with_detail(CompilerDiagnosticDetail::Error {
769 loc: memo_location,
770 message: Some("Could not preserve existing manual memoization".to_string()),
771 identifier_name: None,
772 });
773 env.record_diagnostic(diag);
774 }