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