1use foldhash::HashMap;
2use foldhash::HashSet;
3use mago_word::ascii_lowercase_word;
4use mago_word::empty_word;
5
6use mago_word::Word;
7use mago_word::WordSet;
8
9use crate::context::ScopeContext;
10use crate::diff::CodebaseDiff;
11use crate::identifier::function_like::FunctionLikeIdentifier;
12use crate::identifier::method::MethodIdentifier;
13use crate::symbol::SymbolIdentifier;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub enum ReferenceSource {
20 Symbol(bool, Word),
24 ClassLikeMember(bool, Word, Word),
29 File(bool, Word),
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub enum ReferenceOrigin {
37 Symbol(SymbolIdentifier),
39 File(Word),
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
45pub enum SymbolReferenceKind {
46 Body,
48 Signature,
50 OverriddenMember,
52 FunctionLikeReturn,
54 PropertyRead,
56 PropertyWrite,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Default)]
66#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
67#[allow(clippy::struct_field_names)]
68pub struct SymbolReferences {
69 symbol_references_to_symbols: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
73
74 symbol_references_to_symbols_in_signature: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
77
78 symbol_references_to_overridden_members: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
81
82 functionlike_references_to_functionlike_returns: HashMap<FunctionLikeIdentifier, HashSet<FunctionLikeIdentifier>>,
85
86 file_references_to_symbols: HashMap<Word, HashSet<SymbolIdentifier>>,
90
91 file_references_to_symbols_in_signature: HashMap<Word, HashSet<SymbolIdentifier>>,
94
95 property_write_references: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
99
100 property_read_references: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
104
105 file_property_write_references: HashMap<Word, HashSet<SymbolIdentifier>>,
107
108 file_property_read_references: HashMap<Word, HashSet<SymbolIdentifier>>,
110}
111
112impl SymbolReferences {
113 #[inline]
115 #[must_use]
116 pub fn new() -> Self {
117 Self::default()
118 }
119
120 #[inline]
122 #[must_use]
123 pub fn is_empty(&self) -> bool {
124 self.symbol_references_to_symbols.is_empty()
125 && self.symbol_references_to_symbols_in_signature.is_empty()
126 && self.symbol_references_to_overridden_members.is_empty()
127 && self.functionlike_references_to_functionlike_returns.is_empty()
128 && self.file_references_to_symbols.is_empty()
129 && self.file_references_to_symbols_in_signature.is_empty()
130 && self.property_write_references.is_empty()
131 && self.property_read_references.is_empty()
132 && self.file_property_write_references.is_empty()
133 && self.file_property_read_references.is_empty()
134 }
135
136 #[inline]
138 pub fn count_body_references(&self) -> usize {
139 self.symbol_references_to_symbols.values().map(std::collections::HashSet::len).sum::<usize>()
140 + self.file_references_to_symbols.values().map(std::collections::HashSet::len).sum::<usize>()
141 }
142
143 #[inline]
145 pub fn count_signature_references(&self) -> usize {
146 self.symbol_references_to_symbols_in_signature.values().map(std::collections::HashSet::len).sum::<usize>()
147 + self.file_references_to_symbols_in_signature.values().map(std::collections::HashSet::len).sum::<usize>()
148 }
149
150 #[inline]
154 #[must_use]
155 pub fn total_map_entries(&self) -> usize {
156 self.symbol_references_to_symbols.len()
157 + self.symbol_references_to_symbols_in_signature.len()
158 + self.symbol_references_to_overridden_members.len()
159 + self.functionlike_references_to_functionlike_returns.len()
160 + self.file_references_to_symbols.len()
161 + self.file_references_to_symbols_in_signature.len()
162 + self.property_write_references.len()
163 + self.property_read_references.len()
164 + self.file_property_write_references.len()
165 + self.file_property_read_references.len()
166 }
167
168 #[inline]
177 #[must_use]
178 pub fn count_referencing_symbols(&self, symbol: &SymbolIdentifier, in_signature: bool) -> usize {
179 let map = if in_signature {
180 &self.symbol_references_to_symbols_in_signature
181 } else {
182 &self.symbol_references_to_symbols
183 };
184
185 let files =
186 if in_signature { &self.file_references_to_symbols_in_signature } else { &self.file_references_to_symbols };
187
188 map.values().filter(|referenced_set| referenced_set.contains(symbol)).count()
189 + files.values().filter(|referenced_set| referenced_set.contains(symbol)).count()
190 }
191
192 #[inline]
202 #[must_use]
203 pub fn count_property_reads(&self, property: &SymbolIdentifier) -> usize {
204 self.property_read_references.values().filter(|read_set| read_set.contains(property)).count()
205 + self.file_property_read_references.values().filter(|read_set| read_set.contains(property)).count()
206 }
207
208 #[inline]
218 #[must_use]
219 pub fn count_property_writes(&self, property: &SymbolIdentifier) -> usize {
220 self.property_write_references.values().filter(|write_set| write_set.contains(property)).count()
221 + self.file_property_write_references.values().filter(|write_set| write_set.contains(property)).count()
222 }
223
224 #[inline]
234 pub fn add_symbol_reference_to_class_member(
235 &mut self,
236 referencing_symbol: Word,
237 class_member: SymbolIdentifier,
238 in_signature: bool,
239 ) {
240 self.add_symbol_reference_to_symbol(referencing_symbol, class_member.0, false);
242
243 let key = (referencing_symbol, empty_word());
245 if in_signature {
246 self.symbol_references_to_symbols_in_signature.entry(key).or_default().insert(class_member);
247 } else {
248 self.symbol_references_to_symbols.entry(key).or_default().insert(class_member);
249 }
250 }
251
252 #[inline]
261 pub fn add_symbol_reference_to_symbol(&mut self, referencing_symbol: Word, symbol: Word, in_signature: bool) {
262 if referencing_symbol == symbol {
263 return;
264 }
265
266 let referencing_key = (referencing_symbol, empty_word());
268 let referenced_key = (symbol, empty_word());
269
270 if in_signature {
271 self.symbol_references_to_symbols_in_signature.entry(referencing_key).or_default().insert(referenced_key);
272 } else {
273 if let Some(sig_refs) = self.symbol_references_to_symbols_in_signature.get(&referencing_key)
275 && sig_refs.contains(&referenced_key)
276 {
277 return;
278 }
279 self.symbol_references_to_symbols.entry(referencing_key).or_default().insert(referenced_key);
280 }
281 }
282
283 #[inline]
285 pub fn add_symbol_reference(
286 &mut self,
287 referencing: SymbolIdentifier,
288 referenced: SymbolIdentifier,
289 in_signature: bool,
290 ) {
291 match (referencing.1.is_empty(), referenced.1.is_empty()) {
292 (true, true) => self.add_symbol_reference_to_symbol(referencing.0, referenced.0, in_signature),
293 (true, false) => self.add_symbol_reference_to_class_member(referencing.0, referenced, in_signature),
294 (false, true) => self.add_class_member_reference_to_symbol(referencing, referenced.0, in_signature),
295 (false, false) => self.add_class_member_reference_to_class_member(referencing, referenced, in_signature),
296 }
297 }
298
299 #[inline]
301 pub fn add_reference(&mut self, referencing: ReferenceOrigin, referenced: SymbolIdentifier, in_signature: bool) {
302 match referencing {
303 ReferenceOrigin::Symbol(referencing) => {
304 self.add_symbol_reference(referencing, referenced, in_signature);
305 }
306 ReferenceOrigin::File(file) => {
307 self.add_file_reference_to_class_member(file, referenced, in_signature);
308 }
309 }
310 }
311
312 #[inline]
314 pub fn add_property_read_reference(&mut self, referencing: ReferenceOrigin, property: SymbolIdentifier) {
315 self.add_reference(referencing, property, false);
316 match referencing {
317 ReferenceOrigin::Symbol(symbol) => {
318 self.property_read_references.entry(symbol).or_default().insert(property);
319 }
320 ReferenceOrigin::File(file) => {
321 self.file_property_read_references.entry(file).or_default().insert(property);
322 }
323 }
324 }
325
326 #[inline]
328 pub fn add_property_write_reference(&mut self, referencing: ReferenceOrigin, property: SymbolIdentifier) {
329 self.add_reference(referencing, property, false);
330 match referencing {
331 ReferenceOrigin::Symbol(symbol) => {
332 self.property_write_references.entry(symbol).or_default().insert(property);
333 }
334 ReferenceOrigin::File(file) => {
335 self.file_property_write_references.entry(file).or_default().insert(property);
336 }
337 }
338 }
339
340 #[inline]
342 pub fn add_overridden_member_reference(&mut self, referencing: SymbolIdentifier, overridden: SymbolIdentifier) {
343 self.symbol_references_to_overridden_members.entry(referencing).or_default().insert(overridden);
344 }
345
346 #[inline]
348 pub fn add_functionlike_return_reference(&mut self, referencing: SymbolIdentifier, referenced: SymbolIdentifier) {
349 let referencing = if referencing.1.is_empty() {
350 FunctionLikeIdentifier::Function(referencing.0)
351 } else {
352 FunctionLikeIdentifier::Method(referencing.0, referencing.1)
353 };
354 let referenced = if referenced.1.is_empty() {
355 FunctionLikeIdentifier::Function(referenced.0)
356 } else {
357 FunctionLikeIdentifier::Method(referenced.0, referenced.1)
358 };
359
360 self.add_reference_to_functionlike_return(referencing, referenced);
361 }
362
363 #[inline]
373 pub fn add_class_member_reference_to_class_member(
374 &mut self,
375 referencing_class_member: SymbolIdentifier,
376 class_member: SymbolIdentifier,
377 in_signature: bool,
378 ) {
379 if referencing_class_member == class_member {
380 return;
381 }
382
383 self.add_symbol_reference_to_symbol(referencing_class_member.0, class_member.0, false);
385 self.add_class_member_reference_to_symbol(referencing_class_member, class_member.0, false);
386
387 if in_signature {
389 self.symbol_references_to_symbols_in_signature
390 .entry(referencing_class_member)
391 .or_default()
392 .insert(class_member);
393 } else {
394 self.symbol_references_to_symbols.entry(referencing_class_member).or_default().insert(class_member);
397 }
398 }
399
400 #[inline]
410 pub fn add_class_member_reference_to_symbol(
411 &mut self,
412 referencing_class_member: SymbolIdentifier,
413 symbol: Word,
414 in_signature: bool,
415 ) {
416 if referencing_class_member.0 == symbol {
417 return;
418 }
419
420 self.add_symbol_reference_to_symbol(referencing_class_member.0, symbol, false);
422
423 let referenced_key = (symbol, empty_word());
425
426 if in_signature {
427 self.symbol_references_to_symbols_in_signature
428 .entry(referencing_class_member)
429 .or_default()
430 .insert(referenced_key);
431 } else {
432 if let Some(sig_refs) = self.symbol_references_to_symbols_in_signature.get(&referencing_class_member)
434 && sig_refs.contains(&referenced_key)
435 {
436 return;
437 }
438 self.symbol_references_to_symbols.entry(referencing_class_member).or_default().insert(referenced_key);
439 }
440 }
441
442 #[inline]
446 pub fn add_file_reference_to_class_member(
447 &mut self,
448 file_name: Word,
449 class_member: SymbolIdentifier,
450 in_signature: bool,
451 ) {
452 if !class_member.1.is_empty() {
453 self.add_file_reference_to_class_member(file_name, (class_member.0, empty_word()), false);
454 }
455
456 if in_signature {
457 self.file_references_to_symbols_in_signature.entry(file_name).or_default().insert(class_member);
458 } else {
459 if let Some(sig_refs) = self.file_references_to_symbols_in_signature.get(&file_name)
461 && sig_refs.contains(&class_member)
462 {
463 return;
464 }
465 self.file_references_to_symbols.entry(file_name).or_default().insert(class_member);
466 }
467 }
468
469 #[inline]
472 pub fn add_reference_to_class_member(
473 &mut self,
474 scope: &ScopeContext<'_>,
475 class_member: SymbolIdentifier,
476 in_signature: bool,
477 ) {
478 self.add_reference(scope.get_reference_origin(), class_member, in_signature);
479 }
480
481 #[inline]
482 pub fn add_reference_for_method_call(&mut self, scope: &ScopeContext<'_>, method: &MethodIdentifier) {
483 self.add_reference_to_class_member(
484 scope,
485 (ascii_lowercase_word(method.get_class_name().as_bytes()), method.get_method_name()),
486 false,
487 );
488 }
489
490 #[inline]
492 pub fn add_reference_for_property_read(&mut self, scope: &ScopeContext<'_>, class_name: Word, property_name: Word) {
493 let normalized_class_name = ascii_lowercase_word(class_name.as_bytes());
494 let class_member = (normalized_class_name, property_name);
495
496 self.add_property_read_reference(scope.get_reference_origin(), class_member);
497 }
498
499 #[inline]
502 pub fn add_reference_for_property_write(
503 &mut self,
504 scope: &ScopeContext<'_>,
505 class_name: Word,
506 property_name: Word,
507 ) {
508 let normalized_class_name = ascii_lowercase_word(class_name.as_bytes());
509 let class_member = (normalized_class_name, property_name);
510
511 self.add_property_write_reference(scope.get_reference_origin(), class_member);
512 }
513
514 #[inline]
517 pub fn add_reference_to_overridden_class_member(&mut self, scope: &ScopeContext, class_member: SymbolIdentifier) {
518 let ReferenceOrigin::Symbol(referencing) = scope.get_reference_origin() else {
519 return;
520 };
521
522 self.symbol_references_to_overridden_members.entry(referencing).or_default().insert(class_member);
523 }
524
525 #[inline]
528 pub fn add_reference_to_symbol(&mut self, scope: &ScopeContext, symbol: Word, in_signature: bool) {
529 self.add_reference(scope.get_reference_origin(), (symbol, empty_word()), in_signature);
530 }
531
532 #[inline]
534 pub fn add_reference_to_functionlike_return(
535 &mut self,
536 referencing_functionlike: FunctionLikeIdentifier,
537 referenced_functionlike: FunctionLikeIdentifier,
538 ) {
539 if referencing_functionlike == referenced_functionlike {
540 return;
541 }
542
543 self.functionlike_references_to_functionlike_returns
544 .entry(referencing_functionlike)
545 .or_default()
546 .insert(referenced_functionlike);
547 }
548
549 #[inline]
552 pub fn extend(&mut self, other: Self) {
553 for (k, v) in other.symbol_references_to_symbols {
554 self.symbol_references_to_symbols.entry(k).or_default().extend(v);
555 }
556 for (k, v) in other.symbol_references_to_symbols_in_signature {
557 self.symbol_references_to_symbols_in_signature.entry(k).or_default().extend(v);
558 }
559 for (k, v) in other.symbol_references_to_overridden_members {
560 self.symbol_references_to_overridden_members.entry(k).or_default().extend(v);
561 }
562 for (k, v) in other.functionlike_references_to_functionlike_returns {
563 self.functionlike_references_to_functionlike_returns.entry(k).or_default().extend(v);
564 }
565
566 for (k, v) in other.file_references_to_symbols {
567 self.file_references_to_symbols.entry(k).or_default().extend(v);
568 }
569
570 for (k, v) in other.file_references_to_symbols_in_signature {
571 self.file_references_to_symbols_in_signature.entry(k).or_default().extend(v);
572 }
573
574 for (k, v) in other.property_write_references {
575 self.property_write_references.entry(k).or_default().extend(v);
576 }
577
578 for (k, v) in other.property_read_references {
579 self.property_read_references.entry(k).or_default().extend(v);
580 }
581
582 for (k, v) in other.file_property_write_references {
583 self.file_property_write_references.entry(k).or_default().extend(v);
584 }
585
586 for (k, v) in other.file_property_read_references {
587 self.file_property_read_references.entry(k).or_default().extend(v);
588 }
589 }
590
591 #[inline]
593 pub fn for_each_reference(&self, mut visit: impl FnMut(ReferenceOrigin, SymbolIdentifier, SymbolReferenceKind)) {
594 for (source, targets) in &self.symbol_references_to_symbols {
595 for target in targets {
596 visit(ReferenceOrigin::Symbol(*source), *target, SymbolReferenceKind::Body);
597 }
598 }
599 for (source, targets) in &self.symbol_references_to_symbols_in_signature {
600 for target in targets {
601 visit(ReferenceOrigin::Symbol(*source), *target, SymbolReferenceKind::Signature);
602 }
603 }
604 for (source, targets) in &self.symbol_references_to_overridden_members {
605 for target in targets {
606 visit(ReferenceOrigin::Symbol(*source), *target, SymbolReferenceKind::OverriddenMember);
607 }
608 }
609 for (source, targets) in &self.functionlike_references_to_functionlike_returns {
610 let Some(source) = function_like_symbol_identifier(source) else {
611 continue;
612 };
613 for target in targets {
614 if let Some(target) = function_like_symbol_identifier(target) {
615 visit(ReferenceOrigin::Symbol(source), target, SymbolReferenceKind::FunctionLikeReturn);
616 }
617 }
618 }
619 for (source, targets) in &self.file_references_to_symbols {
620 for target in targets {
621 visit(ReferenceOrigin::File(*source), *target, SymbolReferenceKind::Body);
622 }
623 }
624 for (source, targets) in &self.file_references_to_symbols_in_signature {
625 for target in targets {
626 visit(ReferenceOrigin::File(*source), *target, SymbolReferenceKind::Signature);
627 }
628 }
629 for (source, targets) in &self.property_read_references {
630 for target in targets {
631 visit(ReferenceOrigin::Symbol(*source), *target, SymbolReferenceKind::PropertyRead);
632 }
633 }
634 for (source, targets) in &self.property_write_references {
635 for target in targets {
636 visit(ReferenceOrigin::Symbol(*source), *target, SymbolReferenceKind::PropertyWrite);
637 }
638 }
639 for (source, targets) in &self.file_property_read_references {
640 for target in targets {
641 visit(ReferenceOrigin::File(*source), *target, SymbolReferenceKind::PropertyRead);
642 }
643 }
644 for (source, targets) in &self.file_property_write_references {
645 for target in targets {
646 visit(ReferenceOrigin::File(*source), *target, SymbolReferenceKind::PropertyWrite);
647 }
648 }
649 }
650
651 #[inline]
662 #[must_use]
663 pub fn get_references_to_symbol(&self, target_symbol: SymbolIdentifier) -> HashSet<&SymbolIdentifier> {
664 let mut referencing_items = HashSet::default();
665 for (referencing_item, referenced_items) in &self.symbol_references_to_symbols {
666 if referenced_items.contains(&target_symbol) {
667 referencing_items.insert(referencing_item);
668 }
669 }
670 for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
671 if referenced_items.contains(&target_symbol) {
672 referencing_items.insert(referencing_item);
673 }
674 }
675 referencing_items
676 }
677
678 #[inline]
680 #[must_use]
681 pub fn has_file_reference_to_symbol(&self, target_symbol: SymbolIdentifier) -> bool {
682 self.file_references_to_symbols.values().any(|references| references.contains(&target_symbol))
683 || self
684 .file_references_to_symbols_in_signature
685 .values()
686 .any(|references| references.contains(&target_symbol))
687 }
688
689 #[inline]
705 #[must_use]
706 pub fn get_invalid_symbols(
707 &self,
708 codebase_diff: &CodebaseDiff,
709 ) -> Option<(HashSet<SymbolIdentifier>, WordSet, WordSet)> {
710 let mut invalid_signatures = HashSet::default();
711 let mut partially_invalid_symbols = WordSet::default();
712
713 let mut sig_reverse_index: HashMap<SymbolIdentifier, Vec<SymbolIdentifier>> = HashMap::default();
714 for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
715 let containing_symbol = (referencing_item.0, empty_word());
716 if codebase_diff.contains_changed_entry(&containing_symbol) {
717 invalid_signatures.insert(*referencing_item);
718 partially_invalid_symbols.insert(referencing_item.0);
719 }
720
721 for referenced in referenced_items {
722 sig_reverse_index.entry(*referenced).or_default().push(*referencing_item);
723 }
724 }
725
726 let mut symbols_to_process = codebase_diff.get_changed().iter().copied().collect::<Vec<_>>();
728 let mut processed_symbols = HashSet::default();
729 let mut expense_counter = 0;
730
731 const EXPENSE_LIMIT: usize = 5000;
732 while let Some(invalidated_item) = symbols_to_process.pop() {
733 if processed_symbols.contains(&invalidated_item) {
734 continue;
735 }
736
737 expense_counter += 1;
738 if expense_counter > EXPENSE_LIMIT {
739 return None;
740 }
741
742 invalid_signatures.insert(invalidated_item);
744 processed_symbols.insert(invalidated_item);
745 if !invalidated_item.1.is_empty() {
746 partially_invalid_symbols.insert(invalidated_item.0);
748 let containing_symbol = (invalidated_item.0, empty_word());
749 if !processed_symbols.contains(&containing_symbol) {
750 symbols_to_process.push(containing_symbol);
751 }
752 }
753
754 if let Some(referencing_items) = sig_reverse_index.get(&invalidated_item) {
756 for referencing_item in referencing_items {
757 if !processed_symbols.contains(referencing_item) {
758 symbols_to_process.push(*referencing_item);
759 }
760
761 invalid_signatures.insert(*referencing_item);
762 if !referencing_item.1.is_empty() {
763 partially_invalid_symbols.insert(referencing_item.0);
764 }
765 }
766 }
767 }
768
769 let mut invalid_bodies = HashSet::default();
772
773 for (referencing_item, referenced_items) in &self.symbol_references_to_symbols {
774 if referenced_items.iter().any(|r| invalid_signatures.contains(r)) {
775 invalid_bodies.insert(*referencing_item);
776 if !referencing_item.1.is_empty() {
777 partially_invalid_symbols.insert(referencing_item.0);
778 }
779 }
780 }
781
782 for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
783 if referenced_items.iter().any(|r| invalid_signatures.contains(r)) {
784 invalid_bodies.insert(*referencing_item);
785 if !referencing_item.1.is_empty() {
786 partially_invalid_symbols.insert(referencing_item.0);
787 }
788 }
789 }
790
791 let mut invalid_files = WordSet::default();
792 for (file, referenced_items) in
793 self.file_references_to_symbols.iter().chain(&self.file_references_to_symbols_in_signature)
794 {
795 if referenced_items.iter().any(|referenced| invalid_signatures.contains(referenced)) {
796 invalid_files.insert(*file);
797 }
798 }
799
800 let mut all_invalid_symbols = invalid_signatures;
801 all_invalid_symbols.extend(invalid_bodies);
802 Some((all_invalid_symbols, partially_invalid_symbols, invalid_files))
803 }
804
805 #[inline]
820 pub fn restore_references_for_safe_symbols(
821 &mut self,
822 previous: &SymbolReferences,
823 safe_symbols: &WordSet,
824 safe_symbol_members: &HashSet<SymbolIdentifier>,
825 ) {
826 let is_safe = |key: &SymbolIdentifier| -> bool {
827 if key.1.is_empty() { safe_symbols.contains(&key.0) } else { safe_symbol_members.contains(key) }
828 };
829
830 for (key, refs) in &previous.symbol_references_to_symbols {
832 if is_safe(key) && !self.symbol_references_to_symbols.contains_key(key) {
833 self.symbol_references_to_symbols.insert(*key, refs.clone());
834 }
835 }
836
837 for (key, refs) in &previous.symbol_references_to_overridden_members {
839 if is_safe(key) && !self.symbol_references_to_overridden_members.contains_key(key) {
840 self.symbol_references_to_overridden_members.insert(*key, refs.clone());
841 }
842 }
843
844 for (key, refs) in &previous.functionlike_references_to_functionlike_returns {
846 let sym_key = match key {
847 FunctionLikeIdentifier::Function(name) => (*name, mago_word::empty_word()),
848 FunctionLikeIdentifier::Method(class, method) => (*class, *method),
849 _ => continue,
850 };
851
852 if is_safe(&sym_key) && !self.functionlike_references_to_functionlike_returns.contains_key(key) {
853 self.functionlike_references_to_functionlike_returns.insert(*key, refs.clone());
854 }
855 }
856
857 for (key, refs) in &previous.property_write_references {
859 if is_safe(key) && !self.property_write_references.contains_key(key) {
860 self.property_write_references.insert(*key, refs.clone());
861 }
862 }
863
864 for (key, refs) in &previous.property_read_references {
866 if is_safe(key) && !self.property_read_references.contains_key(key) {
867 self.property_read_references.insert(*key, refs.clone());
868 }
869 }
870 }
871
872 #[inline]
883 pub fn remove_body_references_for_symbols(
884 &mut self,
885 symbols_and_members: &HashSet<SymbolIdentifier>,
886 file_names: &[Word],
887 ) {
888 for key in symbols_and_members {
890 self.symbol_references_to_symbols.remove(key);
891 self.symbol_references_to_overridden_members.remove(key);
892 self.property_write_references.remove(key);
893 self.property_read_references.remove(key);
894 }
895
896 self.functionlike_references_to_functionlike_returns.retain(|key, _| {
898 let sym_key = match key {
899 FunctionLikeIdentifier::Function(name) => (*name, mago_word::empty_word()),
900 FunctionLikeIdentifier::Method(class, method) => (*class, *method),
901 _ => return true,
902 };
903
904 !symbols_and_members.contains(&sym_key)
905 });
906
907 for name in file_names {
909 self.file_references_to_symbols.remove(name);
910 self.file_property_write_references.remove(name);
911 self.file_property_read_references.remove(name);
912 }
913 }
914
915 #[inline]
920 pub fn remove_references_from_files(&mut self, file_names: &WordSet) {
921 for name in file_names {
922 self.file_references_to_symbols.remove(name);
923 self.file_references_to_symbols_in_signature.remove(name);
924 self.file_property_write_references.remove(name);
925 self.file_property_read_references.remove(name);
926 }
927 }
928
929 #[inline]
931 pub fn retain_references_from_files(&mut self, file_names: &WordSet) {
932 self.file_references_to_symbols.retain(|name, _| file_names.contains(name));
933 self.file_references_to_symbols_in_signature.retain(|name, _| file_names.contains(name));
934 self.file_property_write_references.retain(|name, _| file_names.contains(name));
935 self.file_property_read_references.retain(|name, _| file_names.contains(name));
936 }
937
938 #[inline]
944 pub fn remove_references_from_invalid_symbols(&mut self, invalid_symbols_and_members: &HashSet<SymbolIdentifier>) {
945 self.symbol_references_to_symbols
947 .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
948 self.symbol_references_to_symbols_in_signature
949 .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
950 self.symbol_references_to_overridden_members
951 .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
952 self.property_write_references
953 .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
954 self.property_read_references
955 .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
956 }
957
958 #[inline]
967 pub fn retain_safe_symbol_references(
968 &mut self,
969 safe_symbols: &WordSet,
970 safe_symbol_members: &HashSet<SymbolIdentifier>,
971 ) {
972 let is_safe = |key: &SymbolIdentifier| -> bool {
973 if key.1.is_empty() { safe_symbols.contains(&key.0) } else { safe_symbol_members.contains(key) }
974 };
975
976 self.symbol_references_to_symbols.retain(|k, _| is_safe(k));
977 self.symbol_references_to_symbols_in_signature.retain(|k, _| is_safe(k));
978 self.symbol_references_to_overridden_members.retain(|k, _| is_safe(k));
979 self.property_write_references.retain(|k, _| is_safe(k));
980 self.property_read_references.retain(|k, _| is_safe(k));
981
982 self.functionlike_references_to_functionlike_returns.retain(|key, _| {
983 let sym_key = match key {
984 FunctionLikeIdentifier::Function(name) => (*name, mago_word::empty_word()),
985 FunctionLikeIdentifier::Method(class, method) => (*class, *method),
986 _ => return true, };
988
989 is_safe(&sym_key)
990 });
991 }
992
993 pub fn remove_dirty_symbol_references(&mut self, dirty_symbols: &HashSet<SymbolIdentifier>) {
999 for key in dirty_symbols {
1000 self.symbol_references_to_symbols.remove(key);
1001 self.symbol_references_to_symbols_in_signature.remove(key);
1002 self.symbol_references_to_overridden_members.remove(key);
1003 self.property_write_references.remove(key);
1004 self.property_read_references.remove(key);
1005
1006 let fl_key = if key.1.is_empty() {
1007 FunctionLikeIdentifier::Function(key.0)
1008 } else {
1009 FunctionLikeIdentifier::Method(key.0, key.1)
1010 };
1011
1012 self.functionlike_references_to_functionlike_returns.remove(&fl_key);
1013 }
1014 }
1015}
1016
1017fn function_like_symbol_identifier(identifier: &FunctionLikeIdentifier) -> Option<SymbolIdentifier> {
1018 match identifier {
1019 FunctionLikeIdentifier::Function(name) => Some((*name, empty_word())),
1020 FunctionLikeIdentifier::Method(class, method) => Some((*class, *method)),
1021 FunctionLikeIdentifier::Closure(_) => None,
1022 }
1023}
1024
1025#[cfg(test)]
1026#[allow(clippy::unwrap_used, clippy::expect_used)]
1027mod tests {
1028 use super::*;
1029 use mago_word::empty_word;
1030 use mago_word::word;
1031
1032 fn make_refs_with_body(entries: Vec<(SymbolIdentifier, Vec<SymbolIdentifier>)>) -> SymbolReferences {
1033 let mut refs = SymbolReferences::new();
1034 for (key, values) in entries {
1035 let set: HashSet<SymbolIdentifier> = values.into_iter().collect();
1036 refs.symbol_references_to_symbols.insert(key, set);
1037 }
1038 refs
1039 }
1040
1041 #[test]
1042 fn test_for_each_reference_visits_every_reference_kind_and_origin() {
1043 let function = (word("consumer"), empty_word());
1044 let signature_function = (word("signature_consumer"), empty_word());
1045 let class = word("service");
1046 let method = (class, word("method"));
1047 let property = (class, word("$property"));
1048 let file_property = (class, word("$file_property"));
1049 let override_source = (word("child"), word("method"));
1050 let return_source = (word("return_consumer"), empty_word());
1051 let file = word("src/file.php");
1052
1053 let mut references = SymbolReferences::new();
1054 assert!(references.is_empty());
1055 references.add_symbol_reference(function, (class, empty_word()), false);
1056 references.add_symbol_reference(signature_function, (class, empty_word()), true);
1057 references.add_property_read_reference(ReferenceOrigin::Symbol(method), property);
1058 references.add_property_write_reference(ReferenceOrigin::Symbol(function), property);
1059 references.add_overridden_member_reference(override_source, method);
1060 references.add_functionlike_return_reference(return_source, method);
1061 references.add_reference(ReferenceOrigin::File(file), method, false);
1062 references.add_file_reference_to_class_member(file, property, true);
1063 references.add_property_read_reference(ReferenceOrigin::File(file), file_property);
1064 references.add_property_write_reference(ReferenceOrigin::File(file), file_property);
1065 assert!(!references.is_empty());
1066
1067 let mut visited = HashSet::default();
1068 references.for_each_reference(|source, target, kind| {
1069 visited.insert((source, target, kind));
1070 });
1071
1072 assert!(visited.contains(&(
1073 ReferenceOrigin::Symbol(function),
1074 (class, empty_word()),
1075 SymbolReferenceKind::Body
1076 )));
1077 assert!(visited.contains(&(
1078 ReferenceOrigin::Symbol(signature_function),
1079 (class, empty_word()),
1080 SymbolReferenceKind::Signature,
1081 )));
1082 assert!(visited.contains(&(ReferenceOrigin::Symbol(method), property, SymbolReferenceKind::PropertyRead)));
1083 assert!(visited.contains(&(ReferenceOrigin::Symbol(function), property, SymbolReferenceKind::PropertyWrite)));
1084 assert!(visited.contains(&(
1085 ReferenceOrigin::Symbol(override_source),
1086 method,
1087 SymbolReferenceKind::OverriddenMember,
1088 )));
1089 assert!(visited.contains(&(
1090 ReferenceOrigin::Symbol(return_source),
1091 method,
1092 SymbolReferenceKind::FunctionLikeReturn,
1093 )));
1094 assert!(visited.contains(&(ReferenceOrigin::File(file), method, SymbolReferenceKind::Body)));
1095 assert!(visited.contains(&(ReferenceOrigin::File(file), property, SymbolReferenceKind::Signature)));
1096 assert!(visited.contains(&(ReferenceOrigin::File(file), file_property, SymbolReferenceKind::PropertyRead)));
1097 assert!(visited.contains(&(ReferenceOrigin::File(file), file_property, SymbolReferenceKind::PropertyWrite)));
1098 assert_eq!(references.count_property_reads(&file_property), 1);
1099 assert_eq!(references.count_property_writes(&file_property), 1);
1100 }
1101
1102 #[test]
1103 fn test_restore_references_for_safe_symbols_restores_missing_body_refs() {
1104 let class_a = word("class_a");
1105 let class_b = word("class_b");
1106 let method_foo = word("foo");
1107 let method_bar = word("bar");
1108
1109 let previous = make_refs_with_body(vec![
1110 ((class_a, method_foo), vec![(class_b, empty_word())]),
1111 ((class_b, method_bar), vec![(class_a, empty_word())]),
1112 ]);
1113
1114 let mut current = make_refs_with_body(vec![((class_b, method_bar), vec![(class_a, empty_word())])]);
1115
1116 let safe_symbols = WordSet::default();
1117 let mut safe_members = HashSet::default();
1118 safe_members.insert((class_a, method_foo));
1119
1120 current.restore_references_for_safe_symbols(&previous, &safe_symbols, &safe_members);
1121
1122 assert!(current.symbol_references_to_symbols.contains_key(&(class_a, method_foo)));
1123 let restored = ¤t.symbol_references_to_symbols[&(class_a, method_foo)];
1124 assert!(restored.contains(&(class_b, empty_word())));
1125
1126 assert!(current.symbol_references_to_symbols.contains_key(&(class_b, method_bar)));
1127 }
1128
1129 #[test]
1130 fn test_restore_references_does_not_overwrite_existing() {
1131 let class_a = word("class_a");
1132 let class_b = word("class_b");
1133 let class_c = word("class_c");
1134 let method_foo = word("foo");
1135
1136 let previous = make_refs_with_body(vec![((class_a, method_foo), vec![(class_b, empty_word())])]);
1137
1138 let mut current = make_refs_with_body(vec![((class_a, method_foo), vec![(class_c, empty_word())])]);
1139
1140 let safe_symbols = WordSet::default();
1141 let mut safe_members = HashSet::default();
1142 safe_members.insert((class_a, method_foo));
1143
1144 current.restore_references_for_safe_symbols(&previous, &safe_symbols, &safe_members);
1145
1146 let refs = ¤t.symbol_references_to_symbols[&(class_a, method_foo)];
1147 assert!(refs.contains(&(class_c, empty_word())));
1148 assert!(!refs.contains(&(class_b, empty_word())));
1149 }
1150
1151 #[test]
1152 fn test_restore_references_for_safe_top_level_symbols() {
1153 let func_a = word("func_a");
1154 let class_b = word("class_b");
1155
1156 let previous = make_refs_with_body(vec![((func_a, empty_word()), vec![(class_b, empty_word())])]);
1157
1158 let mut current = SymbolReferences::new();
1159
1160 let mut safe_symbols = WordSet::default();
1161 safe_symbols.insert(func_a);
1162 let safe_members = HashSet::default();
1163
1164 current.restore_references_for_safe_symbols(&previous, &safe_symbols, &safe_members);
1165
1166 assert!(current.symbol_references_to_symbols.contains_key(&(func_a, empty_word())));
1167 let restored = ¤t.symbol_references_to_symbols[&(func_a, empty_word())];
1168 assert!(restored.contains(&(class_b, empty_word())));
1169 }
1170
1171 #[test]
1172 fn test_restore_skips_non_safe_symbols() {
1173 let func_a = word("func_a");
1174 let class_b = word("class_b");
1175 let previous = make_refs_with_body(vec![((func_a, empty_word()), vec![(class_b, empty_word())])]);
1176
1177 let mut current = SymbolReferences::new();
1178
1179 let safe_symbols = WordSet::default();
1180 let safe_members = HashSet::default();
1181
1182 current.restore_references_for_safe_symbols(&previous, &safe_symbols, &safe_members);
1183
1184 assert!(!current.symbol_references_to_symbols.contains_key(&(func_a, empty_word())));
1185 }
1186
1187 #[test]
1188 fn test_get_invalid_symbols_basic_cascade() {
1189 let class_a = word("class_a");
1190 let class_b = word("class_b");
1191 let method_foo = word("foo");
1192
1193 let mut refs = SymbolReferences::new();
1194 refs.symbol_references_to_symbols_in_signature.insert((class_b, method_foo), {
1195 let mut set = HashSet::default();
1196 set.insert((class_a, empty_word()));
1197 set
1198 });
1199
1200 let mut diff = crate::diff::CodebaseDiff::new();
1201 let mut changed = HashSet::default();
1202 changed.insert((class_a, empty_word()));
1203 diff = diff.with_changed(changed);
1204
1205 let result = refs.get_invalid_symbols(&diff);
1206 assert!(result.is_some());
1207 let (invalid, partially_invalid, invalid_files) = result.unwrap();
1208
1209 assert!(invalid.contains(&(class_a, empty_word())));
1210 assert!(invalid.contains(&(class_b, method_foo)));
1211 assert!(partially_invalid.contains(&class_b));
1212 assert!(invalid_files.is_empty());
1213 }
1214
1215 #[test]
1216 fn test_get_invalid_symbols_tracks_file_origins() {
1217 let changed_class = word("changed_class");
1218 let file = word("src/bootstrap.php");
1219 let mut references = SymbolReferences::new();
1220 references.add_reference(ReferenceOrigin::File(file), (changed_class, empty_word()), false);
1221
1222 let mut diff = crate::diff::CodebaseDiff::new();
1223 let mut changed = HashSet::default();
1224 changed.insert((changed_class, empty_word()));
1225 diff = diff.with_changed(changed);
1226
1227 let (_, _, invalid_files) = references.get_invalid_symbols(&diff).expect("invalidation should complete");
1228 assert_eq!(invalid_files.len(), 1);
1229 assert!(invalid_files.contains(&file));
1230 }
1231}