Skip to main content

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