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    /// Maps a file (represented by its hash as an Atom) to a set of referenced symbols/members `(Symbol, Member)`
72    /// found within the file's global scope (outside any symbol). This tracks references from top-level code.
73    /// Used for incremental analysis to determine which files need re-analysis when a symbol changes.
74    file_references_to_symbols: HashMap<Atom, HashSet<SymbolIdentifier>>,
75
76    /// Maps a file (represented by its hash as an Atom) to a set of referenced symbols/members `(Symbol, Member)`
77    /// found within the file's global scope signatures (e.g., top-level type declarations).
78    file_references_to_symbols_in_signature: HashMap<Atom, HashSet<SymbolIdentifier>>,
79}
80
81impl SymbolReferences {
82    /// Creates a new, empty `SymbolReferences` collection.
83    #[inline]
84    #[must_use]
85    pub fn new() -> Self {
86        Self {
87            symbol_references_to_symbols: HashMap::default(),
88            symbol_references_to_symbols_in_signature: HashMap::default(),
89            symbol_references_to_overridden_members: HashMap::default(),
90            functionlike_references_to_functionlike_returns: HashMap::default(),
91            file_references_to_symbols: HashMap::default(),
92            file_references_to_symbols_in_signature: HashMap::default(),
93        }
94    }
95
96    /// Counts the total number of symbol-to-symbol body references.
97    #[inline]
98    pub fn count_body_references(&self) -> usize {
99        self.symbol_references_to_symbols.values().map(std::collections::HashSet::len).sum()
100    }
101
102    /// Counts the total number of symbol-to-symbol signature references.
103    #[inline]
104    pub fn count_signature_references(&self) -> usize {
105        self.symbol_references_to_symbols_in_signature.values().map(std::collections::HashSet::len).sum()
106    }
107
108    /// Counts how many symbols reference the given symbol.
109    ///
110    /// # Arguments
111    /// * `symbol` - The symbol to check references to
112    /// * `in_signature` - If true, count signature references; if false, count body references
113    ///
114    /// # Returns
115    /// The number of symbols that reference the given symbol
116    #[inline]
117    #[must_use]
118    pub fn count_referencing_symbols(&self, symbol: &SymbolIdentifier, in_signature: bool) -> usize {
119        let map = if in_signature {
120            &self.symbol_references_to_symbols_in_signature
121        } else {
122            &self.symbol_references_to_symbols
123        };
124
125        map.values().filter(|referenced_set| referenced_set.contains(symbol)).count()
126    }
127
128    /// Records that a top-level symbol (e.g., a function) references a class member.
129    ///
130    /// Automatically adds a reference from the referencing symbol to the member's class.
131    ///
132    /// # Arguments
133    /// * `referencing_symbol`: The FQN of the function or global const making the reference.
134    /// * `class_member`: A tuple `(ClassName, MemberName)` being referenced.
135    /// * `in_signature`: `true` if the reference occurs in a signature context, `false` if in the body.
136    #[inline]
137    pub fn add_symbol_reference_to_class_member(
138        &mut self,
139        referencing_symbol: Atom,
140        class_member: SymbolIdentifier,
141        in_signature: bool,
142    ) {
143        // Reference the class itself implicitly (in body context)
144        self.add_symbol_reference_to_symbol(referencing_symbol, class_member.0, false);
145
146        // Use empty member for the referencing symbol key
147        let key = (referencing_symbol, empty_atom());
148        if in_signature {
149            self.symbol_references_to_symbols_in_signature.entry(key).or_default().insert(class_member);
150        } else {
151            self.symbol_references_to_symbols.entry(key).or_default().insert(class_member);
152        }
153    }
154
155    /// Records that a top-level symbol references another top-level symbol.
156    ///
157    /// Skips self-references. Skips body references if already referenced in signature.
158    ///
159    /// # Arguments
160    /// * `referencing_symbol`: The FQN of the symbol making the reference.
161    /// * `symbol`: The FQN of the symbol being referenced.
162    /// * `in_signature`: `true` if the reference occurs in a signature context, `false` if in the body.
163    #[inline]
164    pub fn add_symbol_reference_to_symbol(&mut self, referencing_symbol: Atom, symbol: Atom, in_signature: bool) {
165        if referencing_symbol == symbol {
166            return;
167        }
168
169        // Represent top-level symbols with an empty member identifier
170        let referencing_key = (referencing_symbol, empty_atom());
171        let referenced_key = (symbol, empty_atom());
172
173        if in_signature {
174            self.symbol_references_to_symbols_in_signature.entry(referencing_key).or_default().insert(referenced_key);
175        } else {
176            // If it's already referenced in the signature, don't add as a body reference
177            if let Some(sig_refs) = self.symbol_references_to_symbols_in_signature.get(&referencing_key)
178                && sig_refs.contains(&referenced_key)
179            {
180                return;
181            }
182            self.symbol_references_to_symbols.entry(referencing_key).or_default().insert(referenced_key);
183        }
184    }
185
186    /// Records that a class member references another class member.
187    ///
188    /// Automatically adds references from the referencing member's class to the referenced member's class,
189    /// and from the referencing member to the referenced member's class. Skips self-references.
190    ///
191    /// # Arguments
192    /// * `referencing_class_member`: Tuple `(ClassName, MemberName)` making the reference.
193    /// * `class_member`: Tuple `(ClassName, MemberName)` being referenced.
194    /// * `in_signature`: `true` if the reference occurs in a signature context, `false` if in the body.
195    #[inline]
196    pub fn add_class_member_reference_to_class_member(
197        &mut self,
198        referencing_class_member: SymbolIdentifier,
199        class_member: SymbolIdentifier,
200        in_signature: bool,
201    ) {
202        if referencing_class_member == class_member {
203            return;
204        }
205
206        // Add implicit references between the classes/symbols involved
207        self.add_symbol_reference_to_symbol(referencing_class_member.0, class_member.0, false);
208        self.add_class_member_reference_to_symbol(referencing_class_member, class_member.0, false);
209
210        // Add the direct member-to-member reference
211        if in_signature {
212            self.symbol_references_to_symbols_in_signature
213                .entry(referencing_class_member)
214                .or_default()
215                .insert(class_member);
216        } else {
217            // Check signature refs first? (Consistency with add_symbol_reference_to_symbol might be needed)
218            // Current logic adds to body refs regardless of signature refs for member->member.
219            self.symbol_references_to_symbols.entry(referencing_class_member).or_default().insert(class_member);
220        }
221    }
222
223    /// Records that a class member references a top-level symbol.
224    ///
225    /// Automatically adds a reference from the referencing member's class to the referenced symbol.
226    /// Skips references to the member's own class. Skips body references if already referenced in signature.
227    ///
228    /// # Arguments
229    /// * `referencing_class_member`: Tuple `(ClassName, MemberName)` making the reference.
230    /// * `symbol`: The FQN of the symbol being referenced.
231    /// * `in_signature`: `true` if the reference occurs in a signature context, `false` if in the body.
232    #[inline]
233    pub fn add_class_member_reference_to_symbol(
234        &mut self,
235        referencing_class_member: SymbolIdentifier,
236        symbol: Atom,
237        in_signature: bool,
238    ) {
239        if referencing_class_member.0 == symbol {
240            return;
241        }
242
243        // Add implicit reference from the class to the symbol
244        self.add_symbol_reference_to_symbol(referencing_class_member.0, symbol, false);
245
246        // Represent the referenced symbol with an empty member identifier
247        let referenced_key = (symbol, empty_atom());
248
249        if in_signature {
250            self.symbol_references_to_symbols_in_signature
251                .entry(referencing_class_member)
252                .or_default()
253                .insert(referenced_key);
254        } else {
255            // If already referenced in signature, don't add as body reference
256            if let Some(sig_refs) = self.symbol_references_to_symbols_in_signature.get(&referencing_class_member)
257                && sig_refs.contains(&referenced_key)
258            {
259                return;
260            }
261            self.symbol_references_to_symbols.entry(referencing_class_member).or_default().insert(referenced_key);
262        }
263    }
264
265    /// Adds a file-level reference to a class member.
266    /// This is used for references from global/top-level scope that aren't within any symbol.
267    #[inline]
268    pub fn add_file_reference_to_class_member(
269        &mut self,
270        file_hash: Atom,
271        class_member: SymbolIdentifier,
272        in_signature: bool,
273    ) {
274        if in_signature {
275            self.file_references_to_symbols_in_signature.entry(file_hash).or_default().insert(class_member);
276        } else {
277            // Check if already in signature to avoid duplicate tracking
278            if let Some(sig_refs) = self.file_references_to_symbols_in_signature.get(&file_hash)
279                && sig_refs.contains(&class_member)
280            {
281                return;
282            }
283            self.file_references_to_symbols.entry(file_hash).or_default().insert(class_member);
284        }
285    }
286
287    /// Convenience method to add a reference *from* the current function context *to* a class member.
288    /// Delegates to appropriate `add_*` methods based on the function context.
289    #[inline]
290    pub fn add_reference_to_class_member(
291        &mut self,
292        scope: &ScopeContext<'_>,
293        class_member: SymbolIdentifier,
294        in_signature: bool,
295    ) {
296        self.add_reference_to_class_member_with_file(scope, class_member, in_signature, None);
297    }
298
299    /// Convenience method to add a reference *from* the current function context *to* a class member.
300    /// Delegates to appropriate `add_*` methods based on the function context.
301    /// If `file_hash` is provided and the reference is from global scope, uses file-level tracking.
302    ///
303    /// # Note on Normalization
304    ///
305    /// This method assumes that symbol names (`class_member`, `function_name`, `class_name`) are already
306    /// normalized to lowercase, as they come from the codebase which stores all symbols in lowercase form.
307    /// No additional normalization is performed to avoid redundant overhead.
308    #[inline]
309    pub fn add_reference_to_class_member_with_file(
310        &mut self,
311        scope: &ScopeContext<'_>,
312        class_member: SymbolIdentifier,
313        in_signature: bool,
314        file_hash: Option<Atom>,
315    ) {
316        if let Some(referencing_functionlike) = scope.get_function_like_identifier() {
317            match referencing_functionlike {
318                FunctionLikeIdentifier::Function(function_name) => {
319                    self.add_symbol_reference_to_class_member(function_name, class_member, in_signature);
320                }
321                FunctionLikeIdentifier::Method(class_name, function_name) => self
322                    .add_class_member_reference_to_class_member(
323                        (class_name, function_name),
324                        class_member,
325                        in_signature,
326                    ),
327                _ => {
328                    // A reference from a closure or arrow function
329                    // If we have a file hash, track it at file level; otherwise use empty_atom()
330                    if let Some(hash) = file_hash {
331                        self.add_file_reference_to_class_member(hash, class_member, in_signature);
332                    } else {
333                        self.add_symbol_reference_to_class_member(empty_atom(), class_member, in_signature);
334                    }
335                }
336            }
337        } else if let Some(calling_class) = scope.get_class_like_name() {
338            // Reference from the class scope itself (e.g., property default)
339            self.add_symbol_reference_to_class_member(calling_class, class_member, in_signature);
340        } else {
341            // No function or class scope - this is a top-level/global reference
342            // Track it at file level if we have a file hash
343            if let Some(hash) = file_hash {
344                self.add_file_reference_to_class_member(hash, class_member, in_signature);
345            } else {
346                self.add_symbol_reference_to_class_member(empty_atom(), class_member, in_signature);
347            }
348        }
349    }
350
351    #[inline]
352    pub fn add_reference_for_method_call(&mut self, scope: &ScopeContext<'_>, method: &MethodIdentifier) {
353        self.add_reference_to_class_member(scope, (*method.get_class_name(), *method.get_method_name()), false);
354    }
355
356    #[inline]
357    pub fn add_reference_for_property_access(
358        &mut self,
359        scope: &ScopeContext<'_>,
360        class_name: Atom,
361        property_name: Atom,
362    ) {
363        self.add_reference_to_class_member(scope, (class_name, property_name), false);
364    }
365
366    /// Convenience method to add a reference *from* the current function context *to* an overridden class member (e.g., `parent::foo`).
367    /// Delegates based on the function context.
368    #[inline]
369    pub fn add_reference_to_overridden_class_member(&mut self, scope: &ScopeContext, class_member: SymbolIdentifier) {
370        let referencing_key = if let Some(referencing_functionlike) = scope.get_function_like_identifier() {
371            match referencing_functionlike {
372                FunctionLikeIdentifier::Function(function_name) => (empty_atom(), function_name),
373                FunctionLikeIdentifier::Method(class_name, function_name) => (class_name, function_name),
374                _ => {
375                    // A reference from a closure can be ignored for now.
376                    return;
377                }
378            }
379        } else if let Some(calling_class) = scope.get_class_like_name() {
380            (ascii_lowercase_atom(&calling_class), empty_atom())
381        } else {
382            return; // Cannot record reference without a source context
383        };
384
385        self.symbol_references_to_overridden_members.entry(referencing_key).or_default().insert(class_member);
386    }
387
388    /// Convenience method to add a reference *from* the current function context *to* a top-level symbol.
389    /// Delegates to appropriate `add_*` methods based on the function context.
390    #[inline]
391    pub fn add_reference_to_symbol(&mut self, scope: &ScopeContext, symbol: Atom, in_signature: bool) {
392        if let Some(referencing_functionlike) = scope.get_function_like_identifier() {
393            match referencing_functionlike {
394                FunctionLikeIdentifier::Function(function_name) => {
395                    self.add_symbol_reference_to_symbol(function_name, symbol, in_signature);
396                }
397                FunctionLikeIdentifier::Method(class_name, function_name) => {
398                    self.add_class_member_reference_to_symbol((class_name, function_name), symbol, in_signature);
399                }
400                _ => {
401                    // Ignore references from closures.
402                }
403            }
404        } else if let Some(calling_class) = scope.get_class_like_name() {
405            self.add_symbol_reference_to_symbol(ascii_lowercase_atom(&calling_class), symbol, in_signature);
406        }
407    }
408
409    /// Records that one function/method references the return value of another. Used for dead code analysis.
410    #[inline]
411    pub fn add_reference_to_functionlike_return(
412        &mut self,
413        referencing_functionlike: FunctionLikeIdentifier,
414        referenced_functionlike: FunctionLikeIdentifier,
415    ) {
416        if referencing_functionlike == referenced_functionlike {
417            return;
418        }
419
420        self.functionlike_references_to_functionlike_returns
421            .entry(referencing_functionlike)
422            .or_default()
423            .insert(referenced_functionlike);
424    }
425
426    /// Merges references from another `SymbolReferences` instance into this one.
427    /// Existing references are extended, not replaced.
428    #[inline]
429    pub fn extend(&mut self, other: Self) {
430        for (k, v) in other.symbol_references_to_symbols {
431            self.symbol_references_to_symbols.entry(k).or_default().extend(v);
432        }
433        for (k, v) in other.symbol_references_to_symbols_in_signature {
434            self.symbol_references_to_symbols_in_signature.entry(k).or_default().extend(v);
435        }
436        for (k, v) in other.symbol_references_to_overridden_members {
437            self.symbol_references_to_overridden_members.entry(k).or_default().extend(v);
438        }
439        for (k, v) in other.functionlike_references_to_functionlike_returns {
440            self.functionlike_references_to_functionlike_returns.entry(k).or_default().extend(v);
441        }
442
443        for (k, v) in other.file_references_to_symbols {
444            self.file_references_to_symbols.entry(k).or_default().extend(v);
445        }
446
447        for (k, v) in other.file_references_to_symbols_in_signature {
448            self.file_references_to_symbols_in_signature.entry(k).or_default().extend(v);
449        }
450    }
451
452    /// Computes the set of all unique symbols and members that are referenced *by* any symbol/member
453    /// tracked in the body or signature reference maps.
454    ///
455    /// # Returns
456    ///
457    /// A `HashSet` containing `&(SymbolName, MemberName)` tuples of all referenced items.
458    #[inline]
459    #[must_use]
460    pub fn get_referenced_symbols_and_members(&self) -> HashSet<&SymbolIdentifier> {
461        let mut referenced_items = HashSet::default();
462        for refs in self.symbol_references_to_symbols.values() {
463            referenced_items.extend(refs.iter());
464        }
465        for refs in self.symbol_references_to_symbols_in_signature.values() {
466            referenced_items.extend(refs.iter());
467        }
468
469        referenced_items
470    }
471
472    /// Computes the inverse of the body and signature reference maps.
473    ///
474    /// # Returns
475    ///
476    /// A `HashMap` where the key is the referenced symbol/member `(Symbol, Member)` and the value
477    /// is a `HashSet` of referencing symbols/members `(RefSymbol, RefMember)`.
478    #[inline]
479    #[must_use]
480    pub fn get_back_references(&self) -> HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> {
481        let mut back_refs: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> = HashMap::default();
482
483        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols {
484            for referenced_item in referenced_items {
485                back_refs.entry(*referenced_item).or_default().insert(*referencing_item);
486            }
487        }
488        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
489            for referenced_item in referenced_items {
490                back_refs.entry(*referenced_item).or_default().insert(*referencing_item);
491            }
492        }
493        back_refs
494    }
495
496    /// Finds all symbols/members that reference a specific target symbol/member.
497    /// Checks both body and signature references.
498    ///
499    /// # Arguments
500    ///
501    /// * `target_symbol`: The `(SymbolName, MemberName)` tuple being referenced.
502    ///
503    /// # Returns
504    ///
505    /// A `HashSet` containing `&(RefSymbol, RefMember)` tuples of all items referencing the target.
506    #[inline]
507    #[must_use]
508    pub fn get_references_to_symbol(&self, target_symbol: SymbolIdentifier) -> HashSet<&SymbolIdentifier> {
509        let mut referencing_items = HashSet::default();
510        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols {
511            if referenced_items.contains(&target_symbol) {
512                referencing_items.insert(referencing_item);
513            }
514        }
515        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
516            if referenced_items.contains(&target_symbol) {
517                referencing_items.insert(referencing_item);
518            }
519        }
520        referencing_items
521    }
522
523    /// Computes the count of references for each unique symbol/member referenced in bodies or signatures.
524    ///
525    /// # Returns
526    ///
527    /// A `HashMap` where the key is the referenced symbol/member `(Symbol, Member)` and the value
528    /// is the total count (`u32`) of references to it.
529    #[inline]
530    #[must_use]
531    pub fn get_referenced_symbols_and_members_with_counts(&self) -> HashMap<SymbolIdentifier, u32> {
532        let mut counts = HashMap::default();
533        for referenced_items in self.symbol_references_to_symbols.values() {
534            for referenced_item in referenced_items {
535                *counts.entry(*referenced_item).or_insert(0) += 1;
536            }
537        }
538        for referenced_items in self.symbol_references_to_symbols_in_signature.values() {
539            for referenced_item in referenced_items {
540                *counts.entry(*referenced_item).or_insert(0) += 1;
541            }
542        }
543        counts
544    }
545
546    /// Computes the inverse of the overridden member reference map.
547    ///
548    /// # Returns
549    ///
550    /// A `HashMap` where the key is the overridden member `(ParentSymbol, Member)` and the value
551    /// is a `HashSet` of referencing symbols/members `(RefSymbol, RefMember)` that call it via `parent::`.
552    #[inline]
553    #[must_use]
554    pub fn get_referenced_overridden_class_members(&self) -> HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> {
555        let mut back_refs: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> = HashMap::default();
556
557        for (referencing_item, referenced_items) in &self.symbol_references_to_overridden_members {
558            for referenced_item in referenced_items {
559                back_refs.entry(*referenced_item).or_default().insert(*referencing_item);
560            }
561        }
562        back_refs
563    }
564
565    /// Calculates sets of invalid symbols and members based on detected code changes (`CodebaseDiff`).
566    /// Propagates invalidation through the dependency graph stored in signature references.
567    /// Limits propagation expense to avoid excessive computation on large changes.
568    ///
569    /// # Arguments
570    ///
571    /// * `codebase_diff`: Information about added, deleted, or modified symbols/signatures.
572    ///
573    /// # Returns
574    ///
575    /// `Some((invalid_signatures, partially_invalid))` on success, where `invalid_signatures` contains
576    /// all symbol/member pairs whose signature is invalid (including propagated ones), and `partially_invalid`
577    /// contains symbols with at least one invalid member.
578    /// Returns `None` if the propagation exceeds an expense limit (currently 5000 steps).
579    #[inline]
580    #[must_use]
581    pub fn get_invalid_symbols(&self, codebase_diff: &CodebaseDiff) -> Option<(HashSet<SymbolIdentifier>, AtomSet)> {
582        let mut invalid_signatures = HashSet::default();
583        let mut partially_invalid_symbols = AtomSet::default();
584
585        for sig_ref_key in self.symbol_references_to_symbols_in_signature.keys() {
586            // Represent the containing symbol (ignore member part for diff check)
587            let containing_symbol = (sig_ref_key.0, empty_atom());
588
589            if codebase_diff.contains_changed_entry(&containing_symbol) {
590                invalid_signatures.insert(*sig_ref_key);
591                partially_invalid_symbols.insert(sig_ref_key.0);
592            }
593        }
594
595        // Start with symbols directly added/deleted in the diff.
596        let mut symbols_to_process = codebase_diff.get_changed().iter().copied().collect::<Vec<_>>();
597        let mut processed_symbols = HashSet::default();
598        let mut expense_counter = 0;
599
600        const EXPENSE_LIMIT: usize = 5000;
601        while let Some(invalidated_item) = symbols_to_process.pop() {
602            if processed_symbols.contains(&invalidated_item) {
603                continue;
604            }
605
606            expense_counter += 1;
607            if expense_counter > EXPENSE_LIMIT {
608                return None;
609            }
610
611            // Mark this item as invalid (signature) and processed
612            invalid_signatures.insert(invalidated_item);
613            processed_symbols.insert(invalidated_item);
614            if !invalidated_item.1.is_empty() {
615                // If it's a member...
616                partially_invalid_symbols.insert(invalidated_item.0);
617            }
618
619            // Find all items that reference this now-invalid item *in their signature*
620            for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
621                if referenced_items.contains(&invalidated_item) {
622                    // If referencing item not already processed, add it to the processing queue
623                    if !processed_symbols.contains(referencing_item) {
624                        symbols_to_process.push(*referencing_item);
625                    }
626
627                    // Mark the referencing item itself as invalid (signature)
628                    invalid_signatures.insert(*referencing_item);
629                    if !referencing_item.1.is_empty() {
630                        // If it's a member...
631                        partially_invalid_symbols.insert(referencing_item.0);
632                    }
633                }
634            }
635
636            // Simple check against limit within loop might be slightly faster
637            if expense_counter > EXPENSE_LIMIT {
638                return None;
639            }
640        }
641
642        // An item's body is invalid if it references (anywhere, body or sig) an item with an invalid signature,
643        // OR if its own signature was kept but its body might have changed (keep_signature diff).
644        let mut invalid_bodies = HashSet::default();
645
646        // Check references from body map
647        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols {
648            // Does this item reference *any* item with an invalid signature?
649            if referenced_items.iter().any(|r| invalid_signatures.contains(r)) {
650                invalid_bodies.insert(*referencing_item);
651                if !referencing_item.1.is_empty() {
652                    // If it's a member...
653                    partially_invalid_symbols.insert(referencing_item.0);
654                }
655            }
656        }
657
658        // Check references from signature map (redundant with propagation? Maybe not entirely)
659        // If item A's signature references item B (invalid signature), A's signature becomes invalid (handled above).
660        // But A's *body* might also be considered invalid due to the signature dependency.
661        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
662            if referenced_items.iter().any(|r| invalid_signatures.contains(r)) {
663                invalid_bodies.insert(*referencing_item);
664                if !referencing_item.1.is_empty() {
665                    partially_invalid_symbols.insert(referencing_item.0);
666                }
667            }
668        }
669
670        // Note: With single-hash fingerprinting, we don't distinguish between signature and body changes.
671        // Any change to a symbol (signature or body) marks it as 'changed' in the diff.
672
673        // Combine results: invalid_symbols includes items whose definition changed or depend on changed signatures,
674        // PLUS items whose bodies reference invalid signatures.
675        // partially_invalid_symbols includes symbols containing members from either invalid_signatures or invalid_bodies.
676        let mut all_invalid_symbols = invalid_signatures;
677        all_invalid_symbols.extend(invalid_bodies);
678        Some((all_invalid_symbols, partially_invalid_symbols))
679    }
680
681    /// Removes all references *originating from* symbols/members that are marked as invalid.
682    ///
683    /// # Arguments
684    ///
685    /// * `invalid_symbols_and_members`: A set containing `(SymbolName, MemberName)` tuples for invalid items.
686    #[inline]
687    pub fn remove_references_from_invalid_symbols(&mut self, invalid_symbols_and_members: &HashSet<SymbolIdentifier>) {
688        // Retain only entries where the key (referencing item) is NOT in the invalid set.
689        self.symbol_references_to_symbols
690            .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
691        self.symbol_references_to_symbols_in_signature
692            .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
693        self.symbol_references_to_overridden_members
694            .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
695    }
696
697    /// Returns a reference to the map tracking references within symbol/member bodies.
698    #[inline]
699    #[must_use]
700    pub fn get_symbol_references_to_symbols(&self) -> &HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> {
701        &self.symbol_references_to_symbols
702    }
703
704    /// Returns a reference to the map tracking references within symbol/member signatures.
705    #[inline]
706    #[must_use]
707    pub fn get_symbol_references_to_symbols_in_signature(
708        &self,
709    ) -> &HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> {
710        &self.symbol_references_to_symbols_in_signature
711    }
712
713    /// Returns a reference to the map tracking references to overridden members.
714    #[inline]
715    #[must_use]
716    pub fn get_symbol_references_to_overridden_members(&self) -> &HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> {
717        &self.symbol_references_to_overridden_members
718    }
719
720    /// Returns a reference to the map tracking references to function-like return values.
721    #[inline]
722    #[must_use]
723    pub fn get_functionlike_references_to_functionlike_returns(
724        &self,
725    ) -> &HashMap<FunctionLikeIdentifier, HashSet<FunctionLikeIdentifier>> {
726        &self.functionlike_references_to_functionlike_returns
727    }
728
729    /// Returns a reference to the map tracking file-level references to symbols (body).
730    #[inline]
731    #[must_use]
732    pub fn get_file_references_to_symbols(&self) -> &HashMap<Atom, HashSet<SymbolIdentifier>> {
733        &self.file_references_to_symbols
734    }
735
736    /// Returns a reference to the map tracking file-level references to symbols (signature).
737    #[inline]
738    #[must_use]
739    pub fn get_file_references_to_symbols_in_signature(&self) -> &HashMap<Atom, HashSet<SymbolIdentifier>> {
740        &self.file_references_to_symbols_in_signature
741    }
742}