mago_codex/
reference.rs

1use ahash::HashMap;
2use ahash::HashSet;
3use mago_atom::ascii_lowercase_atom;
4use mago_atom::empty_atom;
5use serde::Deserialize;
6use serde::Serialize;
7
8use mago_atom::Atom;
9use mago_atom::AtomSet;
10
11use crate::context::ScopeContext;
12use crate::diff::CodebaseDiff;
13use crate::identifier::function_like::FunctionLikeIdentifier;
14use crate::identifier::method::MethodIdentifier;
15use crate::symbol::SymbolIdentifier;
16
17/// Represents the source of a reference, distinguishing between top-level symbols
18/// and members within a class-like structure.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub enum ReferenceSource {
21    /// A reference from a top-level symbol (function, class, enum, trait, interface, constant).
22    /// The bool indicates if the reference occurs within a signature context (true) or body (false).
23    /// The Atom is the name (FQCN or FQN) of the referencing symbol.
24    Symbol(bool, Atom),
25    /// A reference from a member within a class-like structure (method, property, class constant, enum case).
26    /// The bool indicates if the reference occurs within a signature context (true) or body (false).
27    /// The first Atom is the FQCN of the class-like structure.
28    /// The second Atom is the name of the member.
29    ClassLikeMember(bool, Atom, Atom),
30}
31
32/// Holds sets of symbols and members identified as invalid during analysis,
33/// often due to changes detected in `CodebaseDiff`.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
35pub struct InvalidSymbols {
36    /// Set of (Symbol, Member) pairs whose *signatures* are considered invalid.
37    /// An empty member name usually indicates the symbol itself.
38    invalid_symbol_and_member_signatures: HashSet<SymbolIdentifier>,
39    /// Set of (Symbol, Member) pairs whose *bodies* are considered invalid.
40    /// An empty member name usually indicates the symbol itself.
41    invalid_symbol_and_member_bodies: HashSet<SymbolIdentifier>,
42    /// Set of top-level symbols (class FQCN, function FQN) that are partially invalid,
43    /// meaning at least one member's signature or body is invalid, but not necessarily the whole symbol.
44    partially_invalid_symbols: AtomSet,
45}
46
47/// Stores various maps tracking references between symbols (classes, functions, etc.)
48/// and class-like members (methods, properties, constants, etc.) within the codebase.
49///
50/// This is primarily used for dependency analysis, understanding code structure,
51/// and potentially for tasks like dead code detection or impact analysis.
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
53pub struct SymbolReferences {
54    /// Maps a referencing symbol/member `(RefSymbol, RefMember)` to a set of referenced symbols/members `(Symbol, Member)`
55    /// found within the *body* of the referencing context.
56    /// `RefMember` or `Member` being empty usually signifies the symbol itself.
57    symbol_references_to_symbols: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
58
59    /// Maps a referencing symbol/member `(RefSymbol, RefMember)` to a set of referenced symbols/members `(Symbol, Member)`
60    /// found within the *signature* (e.g., type hints, attributes) of the referencing context.
61    symbol_references_to_symbols_in_signature: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
62
63    /// Maps a referencing symbol/member `(RefSymbol, RefMember)` to a set of *overridden* members `(ParentSymbol, Member)`
64    /// that it directly references (e.g., via `parent::method()`).
65    symbol_references_to_overridden_members: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
66
67    /// Maps a referencing function/method (`FunctionLikeIdentifier`) to a set of functions/methods (`FunctionLikeIdentifier`)
68    /// whose return values it references/uses. Used for dead code analysis on return values.
69    functionlike_references_to_functionlike_returns: HashMap<FunctionLikeIdentifier, HashSet<FunctionLikeIdentifier>>,
70}
71
72impl SymbolReferences {
73    /// Creates a new, empty `SymbolReferences` collection.
74    #[inline]
75    pub fn new() -> Self {
76        Self {
77            symbol_references_to_symbols: HashMap::default(),
78            symbol_references_to_symbols_in_signature: HashMap::default(),
79            symbol_references_to_overridden_members: HashMap::default(),
80            functionlike_references_to_functionlike_returns: HashMap::default(),
81        }
82    }
83
84    /// Records that a top-level symbol (e.g., a function) references a class member.
85    ///
86    /// Automatically adds a reference from the referencing symbol to the member's class.
87    ///
88    /// # Arguments
89    /// * `referencing_symbol`: The FQN of the function or global const making the reference.
90    /// * `class_member`: A tuple `(ClassName, MemberName)` being referenced.
91    /// * `in_signature`: `true` if the reference occurs in a signature context, `false` if in the body.
92    #[inline]
93    pub fn add_symbol_reference_to_class_member(
94        &mut self,
95        referencing_symbol: Atom,
96        class_member: SymbolIdentifier,
97        in_signature: bool,
98    ) {
99        // Reference the class itself implicitly (in body context)
100        self.add_symbol_reference_to_symbol(referencing_symbol, class_member.0, false);
101
102        // Use empty member for the referencing symbol key
103        let key = (referencing_symbol, empty_atom());
104        if in_signature {
105            self.symbol_references_to_symbols_in_signature.entry(key).or_default().insert(class_member);
106        } else {
107            self.symbol_references_to_symbols.entry(key).or_default().insert(class_member);
108        }
109    }
110
111    /// Records that a top-level symbol references another top-level symbol.
112    ///
113    /// Skips self-references. Skips body references if already referenced in signature.
114    ///
115    /// # Arguments
116    /// * `referencing_symbol`: The FQN of the symbol making the reference.
117    /// * `symbol`: The FQN of the symbol being referenced.
118    /// * `in_signature`: `true` if the reference occurs in a signature context, `false` if in the body.
119    #[inline]
120    pub fn add_symbol_reference_to_symbol(&mut self, referencing_symbol: Atom, symbol: Atom, in_signature: bool) {
121        if referencing_symbol == symbol {
122            return;
123        }
124
125        // Represent top-level symbols with an empty member identifier
126        let referencing_key = (referencing_symbol, empty_atom());
127        let referenced_key = (symbol, empty_atom());
128
129        if in_signature {
130            self.symbol_references_to_symbols_in_signature.entry(referencing_key).or_default().insert(referenced_key);
131        } else {
132            // If it's already referenced in the signature, don't add as a body reference
133            if let Some(sig_refs) = self.symbol_references_to_symbols_in_signature.get(&referencing_key)
134                && sig_refs.contains(&referenced_key)
135            {
136                return;
137            }
138            self.symbol_references_to_symbols.entry(referencing_key).or_default().insert(referenced_key);
139        }
140    }
141
142    /// Records that a class member references another class member.
143    ///
144    /// Automatically adds references from the referencing member's class to the referenced member's class,
145    /// and from the referencing member to the referenced member's class. Skips self-references.
146    ///
147    /// # Arguments
148    /// * `referencing_class_member`: Tuple `(ClassName, MemberName)` making the reference.
149    /// * `class_member`: Tuple `(ClassName, MemberName)` being referenced.
150    /// * `in_signature`: `true` if the reference occurs in a signature context, `false` if in the body.
151    #[inline]
152    pub fn add_class_member_reference_to_class_member(
153        &mut self,
154        referencing_class_member: SymbolIdentifier,
155        class_member: SymbolIdentifier,
156        in_signature: bool,
157    ) {
158        if referencing_class_member == class_member {
159            return;
160        }
161
162        // Add implicit references between the classes/symbols involved
163        self.add_symbol_reference_to_symbol(referencing_class_member.0, class_member.0, false);
164        self.add_class_member_reference_to_symbol(referencing_class_member, class_member.0, false);
165
166        // Add the direct member-to-member reference
167        if in_signature {
168            self.symbol_references_to_symbols_in_signature
169                .entry(referencing_class_member)
170                .or_default()
171                .insert(class_member);
172        } else {
173            // Check signature refs first? (Consistency with add_symbol_reference_to_symbol might be needed)
174            // Current logic adds to body refs regardless of signature refs for member->member.
175            self.symbol_references_to_symbols.entry(referencing_class_member).or_default().insert(class_member);
176        }
177    }
178
179    /// Records that a class member references a top-level symbol.
180    ///
181    /// Automatically adds a reference from the referencing member's class to the referenced symbol.
182    /// Skips references to the member's own class. Skips body references if already referenced in signature.
183    ///
184    /// # Arguments
185    /// * `referencing_class_member`: Tuple `(ClassName, MemberName)` making the reference.
186    /// * `symbol`: The FQN of the symbol being referenced.
187    /// * `in_signature`: `true` if the reference occurs in a signature context, `false` if in the body.
188    #[inline]
189    pub fn add_class_member_reference_to_symbol(
190        &mut self,
191        referencing_class_member: SymbolIdentifier,
192        symbol: Atom,
193        in_signature: bool,
194    ) {
195        if referencing_class_member.0 == symbol {
196            return;
197        }
198
199        // Add implicit reference from the class to the symbol
200        self.add_symbol_reference_to_symbol(referencing_class_member.0, symbol, false);
201
202        // Represent the referenced symbol with an empty member identifier
203        let referenced_key = (symbol, empty_atom());
204
205        if in_signature {
206            self.symbol_references_to_symbols_in_signature
207                .entry(referencing_class_member)
208                .or_default()
209                .insert(referenced_key);
210        } else {
211            // If already referenced in signature, don't add as body reference
212            if let Some(sig_refs) = self.symbol_references_to_symbols_in_signature.get(&referencing_class_member)
213                && sig_refs.contains(&referenced_key)
214            {
215                return;
216            }
217            self.symbol_references_to_symbols.entry(referencing_class_member).or_default().insert(referenced_key);
218        }
219    }
220
221    /// Convenience method to add a reference *from* the current function context *to* a class member.
222    /// Delegates to appropriate `add_*` methods based on the function context.
223    #[inline]
224    pub fn add_reference_to_class_member(
225        &mut self,
226        scope: &ScopeContext<'_>,
227        class_member: SymbolIdentifier,
228        in_signature: bool,
229    ) {
230        if let Some(referencing_functionlike) = scope.get_function_like_identifier() {
231            match referencing_functionlike {
232                FunctionLikeIdentifier::Function(function_name) => {
233                    self.add_symbol_reference_to_class_member(function_name, class_member, in_signature)
234                }
235                FunctionLikeIdentifier::Method(class_name, function_name) => self
236                    .add_class_member_reference_to_class_member(
237                        (class_name, function_name),
238                        class_member,
239                        in_signature,
240                    ),
241                _ => {
242                    // A reference from a closure can be ignored for now.
243                }
244            }
245        } else if let Some(calling_class) = scope.get_class_like_name() {
246            // Reference from the class scope itself (e.g., property default)
247            self.add_symbol_reference_to_class_member(ascii_lowercase_atom(&calling_class), class_member, in_signature)
248        }
249        // If no context, the reference source is unknown/untracked in this map
250    }
251
252    #[inline]
253    pub fn add_reference_for_method_call(&mut self, scope: &ScopeContext<'_>, method: &MethodIdentifier) {
254        self.add_reference_to_class_member(scope, (*method.get_class_name(), *method.get_method_name()), false);
255    }
256
257    #[inline]
258    pub fn add_reference_for_property_access(
259        &mut self,
260        scope: &ScopeContext<'_>,
261        class_name: Atom,
262        property_name: Atom,
263    ) {
264        self.add_reference_to_class_member(scope, (class_name, property_name), false);
265    }
266
267    /// Convenience method to add a reference *from* the current function context *to* an overridden class member (e.g., `parent::foo`).
268    /// Delegates based on the function context.
269    #[inline]
270    pub fn add_reference_to_overridden_class_member(&mut self, scope: &ScopeContext, class_member: SymbolIdentifier) {
271        let referencing_key = if let Some(referencing_functionlike) = scope.get_function_like_identifier() {
272            match referencing_functionlike {
273                FunctionLikeIdentifier::Function(function_name) => (empty_atom(), function_name),
274                FunctionLikeIdentifier::Method(class_name, function_name) => (class_name, function_name),
275                _ => {
276                    // A reference from a closure can be ignored for now.
277                    return;
278                }
279            }
280        } else if let Some(calling_class) = scope.get_class_like_name() {
281            (ascii_lowercase_atom(&calling_class), empty_atom())
282        } else {
283            return; // Cannot record reference without a source context
284        };
285
286        self.symbol_references_to_overridden_members.entry(referencing_key).or_default().insert(class_member);
287    }
288
289    /// Convenience method to add a reference *from* the current function context *to* a top-level symbol.
290    /// Delegates to appropriate `add_*` methods based on the function context.
291    #[inline]
292    pub fn add_reference_to_symbol(&mut self, scope: &ScopeContext, symbol: Atom, in_signature: bool) {
293        if let Some(referencing_functionlike) = scope.get_function_like_identifier() {
294            match referencing_functionlike {
295                FunctionLikeIdentifier::Function(function_name) => {
296                    self.add_symbol_reference_to_symbol(function_name, symbol, in_signature)
297                }
298                FunctionLikeIdentifier::Method(class_name, function_name) => {
299                    self.add_class_member_reference_to_symbol((class_name, function_name), symbol, in_signature)
300                }
301                _ => {
302                    // Ignore references from closures.
303                }
304            }
305        } else if let Some(calling_class) = scope.get_class_like_name() {
306            self.add_symbol_reference_to_symbol(ascii_lowercase_atom(&calling_class), symbol, in_signature)
307        }
308    }
309
310    /// Records that one function/method references the return value of another. Used for dead code analysis.
311    #[inline]
312    pub fn add_reference_to_functionlike_return(
313        &mut self,
314        referencing_functionlike: FunctionLikeIdentifier,
315        referenced_functionlike: FunctionLikeIdentifier,
316    ) {
317        if referencing_functionlike == referenced_functionlike {
318            return;
319        }
320
321        self.functionlike_references_to_functionlike_returns
322            .entry(referencing_functionlike)
323            .or_default()
324            .insert(referenced_functionlike);
325    }
326
327    /// Merges references from another `SymbolReferences` instance into this one.
328    /// Existing references are extended, not replaced.
329    #[inline]
330    pub fn extend(&mut self, other: Self) {
331        for (k, v) in other.symbol_references_to_symbols {
332            self.symbol_references_to_symbols.entry(k).or_default().extend(v);
333        }
334        for (k, v) in other.symbol_references_to_symbols_in_signature {
335            self.symbol_references_to_symbols_in_signature.entry(k).or_default().extend(v);
336        }
337        for (k, v) in other.symbol_references_to_overridden_members {
338            self.symbol_references_to_overridden_members.entry(k).or_default().extend(v);
339        }
340        for (k, v) in other.functionlike_references_to_functionlike_returns {
341            self.functionlike_references_to_functionlike_returns.entry(k).or_default().extend(v);
342        }
343    }
344
345    /// Computes the set of all unique symbols and members that are referenced *by* any symbol/member
346    /// tracked in the body or signature reference maps.
347    ///
348    /// # Returns
349    ///
350    /// A `HashSet` containing `&(SymbolName, MemberName)` tuples of all referenced items.
351    #[inline]
352    pub fn get_referenced_symbols_and_members(&self) -> HashSet<&SymbolIdentifier> {
353        let mut referenced_items = HashSet::default();
354        for refs in self.symbol_references_to_symbols.values() {
355            referenced_items.extend(refs.iter());
356        }
357        for refs in self.symbol_references_to_symbols_in_signature.values() {
358            referenced_items.extend(refs.iter());
359        }
360
361        referenced_items
362    }
363
364    /// Computes the inverse of the body and signature reference maps.
365    ///
366    /// # Returns
367    ///
368    /// A `HashMap` where the key is the referenced symbol/member `(Symbol, Member)` and the value
369    /// is a `HashSet` of referencing symbols/members `(RefSymbol, RefMember)`.
370    #[inline]
371    pub fn get_back_references(&self) -> HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> {
372        let mut back_refs: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> = HashMap::default();
373
374        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols {
375            for referenced_item in referenced_items {
376                back_refs.entry(*referenced_item).or_default().insert(*referencing_item);
377            }
378        }
379        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
380            for referenced_item in referenced_items {
381                back_refs.entry(*referenced_item).or_default().insert(*referencing_item);
382            }
383        }
384        back_refs
385    }
386
387    /// Finds all symbols/members that reference a specific target symbol/member.
388    /// Checks both body and signature references.
389    ///
390    /// # Arguments
391    ///
392    /// * `target_symbol`: The `(SymbolName, MemberName)` tuple being referenced.
393    ///
394    /// # Returns
395    ///
396    /// A `HashSet` containing `&(RefSymbol, RefMember)` tuples of all items referencing the target.
397    #[inline]
398    pub fn get_references_to_symbol(&self, target_symbol: SymbolIdentifier) -> HashSet<&SymbolIdentifier> {
399        let mut referencing_items = HashSet::default();
400        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols {
401            if referenced_items.contains(&target_symbol) {
402                referencing_items.insert(referencing_item);
403            }
404        }
405        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
406            if referenced_items.contains(&target_symbol) {
407                referencing_items.insert(referencing_item);
408            }
409        }
410        referencing_items
411    }
412
413    /// Computes the count of references for each unique symbol/member referenced in bodies or signatures.
414    ///
415    /// # Returns
416    ///
417    /// A `HashMap` where the key is the referenced symbol/member `(Symbol, Member)` and the value
418    /// is the total count (`u32`) of references to it.
419    #[inline]
420    pub fn get_referenced_symbols_and_members_with_counts(&self) -> HashMap<SymbolIdentifier, u32> {
421        let mut counts = HashMap::default();
422        for referenced_items in self.symbol_references_to_symbols.values() {
423            for referenced_item in referenced_items {
424                *counts.entry(*referenced_item).or_insert(0) += 1;
425            }
426        }
427        for referenced_items in self.symbol_references_to_symbols_in_signature.values() {
428            for referenced_item in referenced_items {
429                *counts.entry(*referenced_item).or_insert(0) += 1;
430            }
431        }
432        counts
433    }
434
435    /// Computes the inverse of the overridden member reference map.
436    ///
437    /// # Returns
438    ///
439    /// A `HashMap` where the key is the overridden member `(ParentSymbol, Member)` and the value
440    /// is a `HashSet` of referencing symbols/members `(RefSymbol, RefMember)` that call it via `parent::`.
441    #[inline]
442    pub fn get_referenced_overridden_class_members(&self) -> HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> {
443        let mut back_refs: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> = HashMap::default();
444
445        for (referencing_item, referenced_items) in &self.symbol_references_to_overridden_members {
446            for referenced_item in referenced_items {
447                back_refs.entry(*referenced_item).or_default().insert(*referencing_item);
448            }
449        }
450        back_refs
451    }
452
453    /// Calculates sets of invalid symbols and members based on detected code changes (`CodebaseDiff`).
454    /// Propagates invalidation through the dependency graph stored in signature references.
455    /// Limits propagation expense to avoid excessive computation on large changes.
456    ///
457    /// # Arguments
458    ///
459    /// * `codebase_diff`: Information about added, deleted, or modified symbols/signatures.
460    ///
461    /// # Returns
462    ///
463    /// `Some((invalid_signatures, partially_invalid))` on success, where `invalid_signatures` contains
464    /// all symbol/member pairs whose signature is invalid (including propagated ones), and `partially_invalid`
465    /// contains symbols with at least one invalid member.
466    /// Returns `None` if the propagation exceeds an expense limit (currently 5000 steps).
467    #[inline]
468    pub fn get_invalid_symbols(&self, codebase_diff: &CodebaseDiff) -> Option<(HashSet<SymbolIdentifier>, AtomSet)> {
469        let mut invalid_signatures = HashSet::default();
470        let mut partially_invalid_symbols = AtomSet::default();
471
472        for sig_ref_key in self.symbol_references_to_symbols_in_signature.keys() {
473            // Represent the containing symbol (ignore member part for diff check)
474            let containing_symbol = (sig_ref_key.0, empty_atom());
475
476            if codebase_diff.contains_add_or_delete_entry(&containing_symbol) {
477                invalid_signatures.insert(*sig_ref_key);
478                partially_invalid_symbols.insert(sig_ref_key.0);
479            }
480        }
481
482        // Start with symbols directly added/deleted in the diff.
483        let mut symbols_to_process = codebase_diff.get_add_or_delete().iter().copied().collect::<Vec<_>>();
484        let mut processed_symbols = HashSet::default();
485        let mut expense_counter = 0;
486
487        const EXPENSE_LIMIT: usize = 5000;
488        while let Some(invalidated_item) = symbols_to_process.pop() {
489            if processed_symbols.contains(&invalidated_item) {
490                continue;
491            }
492
493            expense_counter += 1;
494            if expense_counter > EXPENSE_LIMIT {
495                return None;
496            }
497
498            // Mark this item as invalid (signature) and processed
499            invalid_signatures.insert(invalidated_item);
500            processed_symbols.insert(invalidated_item);
501            if !invalidated_item.1.is_empty() {
502                // If it's a member...
503                partially_invalid_symbols.insert(invalidated_item.0);
504            }
505
506            // Find all items that reference this now-invalid item *in their signature*
507            for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
508                if referenced_items.contains(&invalidated_item) {
509                    // If referencing item not already processed, add it to the processing queue
510                    if !processed_symbols.contains(referencing_item) {
511                        symbols_to_process.push(*referencing_item);
512                    }
513
514                    // Mark the referencing item itself as invalid (signature)
515                    invalid_signatures.insert(*referencing_item);
516                    if !referencing_item.1.is_empty() {
517                        // If it's a member...
518                        partially_invalid_symbols.insert(referencing_item.0);
519                    }
520                }
521            }
522
523            // Simple check against limit within loop might be slightly faster
524            if expense_counter > EXPENSE_LIMIT {
525                return None;
526            }
527        }
528
529        // An item's body is invalid if it references (anywhere, body or sig) an item with an invalid signature,
530        // OR if its own signature was kept but its body might have changed (keep_signature diff).
531        let mut invalid_bodies = HashSet::default();
532
533        // Check references from body map
534        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols {
535            // Does this item reference *any* item with an invalid signature?
536            if referenced_items.iter().any(|r| invalid_signatures.contains(r)) {
537                invalid_bodies.insert(*referencing_item);
538                if !referencing_item.1.is_empty() {
539                    // If it's a member...
540                    partially_invalid_symbols.insert(referencing_item.0);
541                }
542            }
543        }
544
545        // Check references from signature map (redundant with propagation? Maybe not entirely)
546        // If item A's signature references item B (invalid signature), A's signature becomes invalid (handled above).
547        // But A's *body* might also be considered invalid due to the signature dependency.
548        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
549            if referenced_items.iter().any(|r| invalid_signatures.contains(r)) {
550                invalid_bodies.insert(*referencing_item);
551                if !referencing_item.1.is_empty() {
552                    partially_invalid_symbols.insert(referencing_item.0);
553                }
554            }
555        }
556
557        // Add items whose signatures were kept, but bodies might have changed
558        for keep_sig_item in codebase_diff.get_keep_signature() {
559            invalid_bodies.insert(*keep_sig_item);
560            if !keep_sig_item.1.is_empty() {
561                partially_invalid_symbols.insert(keep_sig_item.0);
562            }
563        }
564
565        // Combine results: invalid_signatures includes items whose definition changed or depend on changed signatures.
566        // partially_invalid_symbols includes symbols containing members from either invalid_signatures or invalid_bodies.
567        Some((invalid_signatures, partially_invalid_symbols))
568    }
569
570    /// Removes all references *originating from* symbols/members that are marked as invalid.
571    ///
572    /// # Arguments
573    ///
574    /// * `invalid_symbols_and_members`: A set containing `(SymbolName, MemberName)` tuples for invalid items.
575    #[inline]
576    pub fn remove_references_from_invalid_symbols(&mut self, invalid_symbols_and_members: &HashSet<SymbolIdentifier>) {
577        // Retain only entries where the key (referencing item) is NOT in the invalid set.
578        self.symbol_references_to_symbols
579            .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
580        self.symbol_references_to_symbols_in_signature
581            .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
582        self.symbol_references_to_overridden_members
583            .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
584    }
585
586    /// Returns a reference to the map tracking references within symbol/member bodies.
587    #[inline]
588    pub fn get_symbol_references_to_symbols(&self) -> &HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> {
589        &self.symbol_references_to_symbols
590    }
591
592    /// Returns a reference to the map tracking references within symbol/member signatures.
593    #[inline]
594    pub fn get_symbol_references_to_symbols_in_signature(
595        &self,
596    ) -> &HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> {
597        &self.symbol_references_to_symbols_in_signature
598    }
599
600    /// Returns a reference to the map tracking references to overridden members.
601    #[inline]
602    pub fn get_symbol_references_to_overridden_members(&self) -> &HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> {
603        &self.symbol_references_to_overridden_members
604    }
605
606    /// Returns a reference to the map tracking references to function-like return values.
607    #[inline]
608    pub fn get_functionlike_references_to_functionlike_returns(
609        &self,
610    ) -> &HashMap<FunctionLikeIdentifier, HashSet<FunctionLikeIdentifier>> {
611        &self.functionlike_references_to_functionlike_returns
612    }
613}