Skip to main content

mago_codex/
reference.rs

1use foldhash::HashMap;
2use foldhash::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)]
35#[allow(clippy::struct_field_names)]
36pub struct InvalidSymbols {
37    /// Set of (Symbol, Member) pairs whose *signatures* are considered invalid.
38    /// An empty member name usually indicates the symbol itself.
39    invalid_symbol_and_member_signatures: HashSet<SymbolIdentifier>,
40    /// Set of (Symbol, Member) pairs whose *bodies* are considered invalid.
41    /// An empty member name usually indicates the symbol itself.
42    invalid_symbol_and_member_bodies: HashSet<SymbolIdentifier>,
43    /// Set of top-level symbols (class FQCN, function FQN) that are partially invalid,
44    /// meaning at least one member's signature or body is invalid, but not necessarily the whole symbol.
45    partially_invalid_symbols: AtomSet,
46}
47
48/// Stores various maps tracking references between symbols (classes, functions, etc.)
49/// and class-like members (methods, properties, constants, etc.) within the codebase.
50///
51/// This is primarily used for dependency analysis, understanding code structure,
52/// and potentially for tasks like dead code detection or impact analysis.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
54#[allow(clippy::struct_field_names)]
55pub struct SymbolReferences {
56    /// Maps a referencing symbol/member `(RefSymbol, RefMember)` to a set of referenced symbols/members `(Symbol, Member)`
57    /// found within the *body* of the referencing context.
58    /// `RefMember` or `Member` being empty usually signifies the symbol itself.
59    symbol_references_to_symbols: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
60
61    /// Maps a referencing symbol/member `(RefSymbol, RefMember)` to a set of referenced symbols/members `(Symbol, Member)`
62    /// found within the *signature* (e.g., type hints, attributes) of the referencing context.
63    symbol_references_to_symbols_in_signature: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
64
65    /// Maps a referencing symbol/member `(RefSymbol, RefMember)` to a set of *overridden* members `(ParentSymbol, Member)`
66    /// that it directly references (e.g., via `parent::method()`).
67    symbol_references_to_overridden_members: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
68
69    /// Maps a referencing function/method (`FunctionLikeIdentifier`) to a set of functions/methods (`FunctionLikeIdentifier`)
70    /// whose return values it references/uses. Used for dead code analysis on return values.
71    functionlike_references_to_functionlike_returns: HashMap<FunctionLikeIdentifier, HashSet<FunctionLikeIdentifier>>,
72
73    /// Maps a file (represented by its hash as an Atom) to a set of referenced symbols/members `(Symbol, Member)`
74    /// found within the file's global scope (outside any symbol). This tracks references from top-level code.
75    /// Used for incremental analysis to determine which files need re-analysis when a symbol changes.
76    file_references_to_symbols: HashMap<Atom, HashSet<SymbolIdentifier>>,
77
78    /// Maps a file (represented by its hash as an Atom) to a set of referenced symbols/members `(Symbol, Member)`
79    /// found within the file's global scope signatures (e.g., top-level type declarations).
80    file_references_to_symbols_in_signature: HashMap<Atom, HashSet<SymbolIdentifier>>,
81
82    /// Maps a referencing symbol/member to a set of properties that are *written* (assigned to).
83    /// This is separate from read references to enable detection of write-only properties.
84    /// The key is the referencing symbol/member, the value is the set of properties being written.
85    property_write_references: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
86
87    /// Maps a referencing symbol/member to a set of properties that are *read* (accessed for value).
88    /// This is separate from write references to enable accurate read/write tracking.
89    /// The key is the referencing symbol/member, the value is the set of properties being read.
90    property_read_references: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>>,
91}
92
93impl SymbolReferences {
94    /// Creates a new, empty `SymbolReferences` collection.
95    #[inline]
96    #[must_use]
97    pub fn new() -> Self {
98        Self {
99            symbol_references_to_symbols: HashMap::default(),
100            symbol_references_to_symbols_in_signature: HashMap::default(),
101            symbol_references_to_overridden_members: HashMap::default(),
102            functionlike_references_to_functionlike_returns: HashMap::default(),
103            file_references_to_symbols: HashMap::default(),
104            file_references_to_symbols_in_signature: HashMap::default(),
105            property_write_references: HashMap::default(),
106            property_read_references: HashMap::default(),
107        }
108    }
109
110    /// Counts the total number of symbol-to-symbol body references.
111    #[inline]
112    pub fn count_body_references(&self) -> usize {
113        self.symbol_references_to_symbols.values().map(std::collections::HashSet::len).sum()
114    }
115
116    /// Counts the total number of symbol-to-symbol signature references.
117    #[inline]
118    pub fn count_signature_references(&self) -> usize {
119        self.symbol_references_to_symbols_in_signature.values().map(std::collections::HashSet::len).sum()
120    }
121
122    /// Returns the total number of map entries (keys) across all reference maps.
123    /// Useful for memory auditing — this count should remain stable across cycles
124    /// in a long-running process.
125    #[inline]
126    #[must_use]
127    pub fn total_map_entries(&self) -> usize {
128        self.symbol_references_to_symbols.len()
129            + self.symbol_references_to_symbols_in_signature.len()
130            + self.symbol_references_to_overridden_members.len()
131            + self.functionlike_references_to_functionlike_returns.len()
132            + self.file_references_to_symbols.len()
133            + self.file_references_to_symbols_in_signature.len()
134            + self.property_write_references.len()
135            + self.property_read_references.len()
136    }
137
138    /// Counts how many symbols reference the given symbol.
139    ///
140    /// # Arguments
141    /// * `symbol` - The symbol to check references to
142    /// * `in_signature` - If true, count signature references; if false, count body references
143    ///
144    /// # Returns
145    /// The number of symbols that reference the given symbol
146    #[inline]
147    #[must_use]
148    pub fn count_referencing_symbols(&self, symbol: &SymbolIdentifier, in_signature: bool) -> usize {
149        let map = if in_signature {
150            &self.symbol_references_to_symbols_in_signature
151        } else {
152            &self.symbol_references_to_symbols
153        };
154
155        map.values().filter(|referenced_set| referenced_set.contains(symbol)).count()
156    }
157
158    /// Counts how many symbols have a *read* reference to the given property.
159    ///
160    /// # Arguments
161    ///
162    /// * `property` - The property symbol identifier `(ClassName, PropertyName)` to check
163    ///
164    /// # Returns
165    ///
166    /// The number of symbols that read the given property
167    #[inline]
168    #[must_use]
169    pub fn count_property_reads(&self, property: &SymbolIdentifier) -> usize {
170        self.property_read_references.values().filter(|read_set| read_set.contains(property)).count()
171    }
172
173    /// Counts how many symbols have a *write* reference to the given property.
174    ///
175    /// # Arguments
176    ///
177    /// * `property` - The property symbol identifier `(ClassName, PropertyName)` to check
178    ///
179    /// # Returns
180    ///
181    /// The number of symbols that write to the given property
182    #[inline]
183    #[must_use]
184    pub fn count_property_writes(&self, property: &SymbolIdentifier) -> usize {
185        self.property_write_references.values().filter(|write_set| write_set.contains(property)).count()
186    }
187
188    /// Records that a top-level symbol (e.g., a function) references a class member.
189    ///
190    /// Automatically adds a reference from the referencing symbol to the member's class.
191    ///
192    /// # Arguments
193    ///
194    /// * `referencing_symbol`: The FQN of the function or global const making the reference.
195    /// * `class_member`: A tuple `(ClassName, MemberName)` being referenced.
196    /// * `in_signature`: `true` if the reference occurs in a signature context, `false` if in the body.
197    #[inline]
198    pub fn add_symbol_reference_to_class_member(
199        &mut self,
200        referencing_symbol: Atom,
201        class_member: SymbolIdentifier,
202        in_signature: bool,
203    ) {
204        // Reference the class itself implicitly (in body context)
205        self.add_symbol_reference_to_symbol(referencing_symbol, class_member.0, false);
206
207        // Use empty member for the referencing symbol key
208        let key = (referencing_symbol, empty_atom());
209        if in_signature {
210            self.symbol_references_to_symbols_in_signature.entry(key).or_default().insert(class_member);
211        } else {
212            self.symbol_references_to_symbols.entry(key).or_default().insert(class_member);
213        }
214    }
215
216    /// Records that a top-level symbol references another top-level symbol.
217    ///
218    /// Skips self-references. Skips body references if already referenced in signature.
219    ///
220    /// # Arguments
221    /// * `referencing_symbol`: The FQN of the symbol making the reference.
222    /// * `symbol`: The FQN of the symbol being referenced.
223    /// * `in_signature`: `true` if the reference occurs in a signature context, `false` if in the body.
224    #[inline]
225    pub fn add_symbol_reference_to_symbol(&mut self, referencing_symbol: Atom, symbol: Atom, in_signature: bool) {
226        if referencing_symbol == symbol {
227            return;
228        }
229
230        // Represent top-level symbols with an empty member identifier
231        let referencing_key = (referencing_symbol, empty_atom());
232        let referenced_key = (symbol, empty_atom());
233
234        if in_signature {
235            self.symbol_references_to_symbols_in_signature.entry(referencing_key).or_default().insert(referenced_key);
236        } else {
237            // If it's already referenced in the signature, don't add as a body reference
238            if let Some(sig_refs) = self.symbol_references_to_symbols_in_signature.get(&referencing_key)
239                && sig_refs.contains(&referenced_key)
240            {
241                return;
242            }
243            self.symbol_references_to_symbols.entry(referencing_key).or_default().insert(referenced_key);
244        }
245    }
246
247    /// Records that a class member references another class member.
248    ///
249    /// Automatically adds references from the referencing member's class to the referenced member's class,
250    /// and from the referencing member to the referenced member's class. Skips self-references.
251    ///
252    /// # Arguments
253    /// * `referencing_class_member`: Tuple `(ClassName, MemberName)` making the reference.
254    /// * `class_member`: Tuple `(ClassName, MemberName)` being referenced.
255    /// * `in_signature`: `true` if the reference occurs in a signature context, `false` if in the body.
256    #[inline]
257    pub fn add_class_member_reference_to_class_member(
258        &mut self,
259        referencing_class_member: SymbolIdentifier,
260        class_member: SymbolIdentifier,
261        in_signature: bool,
262    ) {
263        if referencing_class_member == class_member {
264            return;
265        }
266
267        // Add implicit references between the classes/symbols involved
268        self.add_symbol_reference_to_symbol(referencing_class_member.0, class_member.0, false);
269        self.add_class_member_reference_to_symbol(referencing_class_member, class_member.0, false);
270
271        // Add the direct member-to-member reference
272        if in_signature {
273            self.symbol_references_to_symbols_in_signature
274                .entry(referencing_class_member)
275                .or_default()
276                .insert(class_member);
277        } else {
278            // Check signature refs first? (Consistency with add_symbol_reference_to_symbol might be needed)
279            // Current logic adds to body refs regardless of signature refs for member->member.
280            self.symbol_references_to_symbols.entry(referencing_class_member).or_default().insert(class_member);
281        }
282    }
283
284    /// Records that a class member references a top-level symbol.
285    ///
286    /// Automatically adds a reference from the referencing member's class to the referenced symbol.
287    /// Skips references to the member's own class. Skips body references if already referenced in signature.
288    ///
289    /// # Arguments
290    /// * `referencing_class_member`: Tuple `(ClassName, MemberName)` making the reference.
291    /// * `symbol`: The FQN of the symbol being referenced.
292    /// * `in_signature`: `true` if the reference occurs in a signature context, `false` if in the body.
293    #[inline]
294    pub fn add_class_member_reference_to_symbol(
295        &mut self,
296        referencing_class_member: SymbolIdentifier,
297        symbol: Atom,
298        in_signature: bool,
299    ) {
300        if referencing_class_member.0 == symbol {
301            return;
302        }
303
304        // Add implicit reference from the class to the symbol
305        self.add_symbol_reference_to_symbol(referencing_class_member.0, symbol, false);
306
307        // Represent the referenced symbol with an empty member identifier
308        let referenced_key = (symbol, empty_atom());
309
310        if in_signature {
311            self.symbol_references_to_symbols_in_signature
312                .entry(referencing_class_member)
313                .or_default()
314                .insert(referenced_key);
315        } else {
316            // If already referenced in signature, don't add as body reference
317            if let Some(sig_refs) = self.symbol_references_to_symbols_in_signature.get(&referencing_class_member)
318                && sig_refs.contains(&referenced_key)
319            {
320                return;
321            }
322            self.symbol_references_to_symbols.entry(referencing_class_member).or_default().insert(referenced_key);
323        }
324    }
325
326    /// Adds a file-level reference to a class member.
327    /// This is used for references from global/top-level scope that aren't within any symbol.
328    #[inline]
329    pub fn add_file_reference_to_class_member(
330        &mut self,
331        file_hash: Atom,
332        class_member: SymbolIdentifier,
333        in_signature: bool,
334    ) {
335        if in_signature {
336            self.file_references_to_symbols_in_signature.entry(file_hash).or_default().insert(class_member);
337        } else {
338            // Check if already in signature to avoid duplicate tracking
339            if let Some(sig_refs) = self.file_references_to_symbols_in_signature.get(&file_hash)
340                && sig_refs.contains(&class_member)
341            {
342                return;
343            }
344            self.file_references_to_symbols.entry(file_hash).or_default().insert(class_member);
345        }
346    }
347
348    /// Convenience method to add a reference *from* the current function context *to* a class member.
349    /// Delegates to appropriate `add_*` methods based on the function context.
350    #[inline]
351    pub fn add_reference_to_class_member(
352        &mut self,
353        scope: &ScopeContext<'_>,
354        class_member: SymbolIdentifier,
355        in_signature: bool,
356    ) {
357        self.add_reference_to_class_member_with_file(scope, class_member, in_signature, None);
358    }
359
360    /// Convenience method to add a reference *from* the current function context *to* a class member.
361    /// Delegates to appropriate `add_*` methods based on the function context.
362    /// If `file_hash` is provided and the reference is from global scope, uses file-level tracking.
363    ///
364    /// # Note on Normalization
365    ///
366    /// This method assumes that symbol names (`class_member`, `function_name`, `class_name`) are already
367    /// normalized to lowercase, as they come from the codebase which stores all symbols in lowercase form.
368    /// No additional normalization is performed to avoid redundant overhead.
369    #[inline]
370    pub fn add_reference_to_class_member_with_file(
371        &mut self,
372        scope: &ScopeContext<'_>,
373        class_member: SymbolIdentifier,
374        in_signature: bool,
375        file_hash: Option<Atom>,
376    ) {
377        if let Some(referencing_functionlike) = scope.get_function_like_identifier() {
378            match referencing_functionlike {
379                FunctionLikeIdentifier::Function(function_name) => {
380                    self.add_symbol_reference_to_class_member(function_name, class_member, in_signature);
381                }
382                FunctionLikeIdentifier::Method(class_name, function_name) => self
383                    .add_class_member_reference_to_class_member(
384                        (class_name, function_name),
385                        class_member,
386                        in_signature,
387                    ),
388                _ => {
389                    // A reference from a closure or arrow function
390                    // If we have a file hash, track it at file level; otherwise use empty_atom()
391                    if let Some(hash) = file_hash {
392                        self.add_file_reference_to_class_member(hash, class_member, in_signature);
393                    } else {
394                        self.add_symbol_reference_to_class_member(empty_atom(), class_member, in_signature);
395                    }
396                }
397            }
398        } else if let Some(calling_class) = scope.get_class_like_name() {
399            // Reference from the class scope itself (e.g., property default)
400            self.add_symbol_reference_to_class_member(calling_class, class_member, in_signature);
401        } else {
402            // No function or class scope - this is a top-level/global reference
403            // Track it at file level if we have a file hash
404            if let Some(hash) = file_hash {
405                self.add_file_reference_to_class_member(hash, class_member, in_signature);
406            } else {
407                self.add_symbol_reference_to_class_member(empty_atom(), class_member, in_signature);
408            }
409        }
410    }
411
412    #[inline]
413    pub fn add_reference_for_method_call(&mut self, scope: &ScopeContext<'_>, method: &MethodIdentifier) {
414        self.add_reference_to_class_member(
415            scope,
416            (ascii_lowercase_atom(&method.get_class_name()), method.get_method_name()),
417            false,
418        );
419    }
420
421    /// Records a read reference to a property (e.g., `$this->prop` used as a value).
422    #[inline]
423    pub fn add_reference_for_property_read(&mut self, scope: &ScopeContext<'_>, class_name: Atom, property_name: Atom) {
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_read_references.entry(referencing_key).or_default().insert(class_member);
431    }
432
433    /// Records a write reference to a property (e.g., `$this->prop = value`).
434    /// This is tracked separately from read references to enable write-only property detection.
435    #[inline]
436    pub fn add_reference_for_property_write(
437        &mut self,
438        scope: &ScopeContext<'_>,
439        class_name: Atom,
440        property_name: Atom,
441    ) {
442        let normalized_class_name = ascii_lowercase_atom(&class_name);
443        let class_member = (normalized_class_name, property_name);
444
445        self.add_reference_to_class_member(scope, class_member, false);
446
447        let referencing_key = self.get_referencing_key_from_scope(scope);
448        self.property_write_references.entry(referencing_key).or_default().insert(class_member);
449    }
450
451    /// Helper to get the referencing key from the current scope context.
452    #[inline]
453    fn get_referencing_key_from_scope(&self, scope: &ScopeContext<'_>) -> SymbolIdentifier {
454        if let Some(referencing_functionlike) = scope.get_function_like_identifier() {
455            match referencing_functionlike {
456                FunctionLikeIdentifier::Function(function_name) => (function_name, empty_atom()),
457                FunctionLikeIdentifier::Method(class_name, function_name) => (class_name, function_name),
458                _ => (empty_atom(), empty_atom()),
459            }
460        } else if let Some(calling_class) = scope.get_class_like_name() {
461            (ascii_lowercase_atom(&calling_class), empty_atom())
462        } else {
463            (empty_atom(), empty_atom())
464        }
465    }
466
467    /// Convenience method to add a reference *from* the current function context *to* an overridden class member (e.g., `parent::foo`).
468    /// Delegates based on the function context.
469    #[inline]
470    pub fn add_reference_to_overridden_class_member(&mut self, scope: &ScopeContext, class_member: SymbolIdentifier) {
471        let referencing_key = if let Some(referencing_functionlike) = scope.get_function_like_identifier() {
472            match referencing_functionlike {
473                FunctionLikeIdentifier::Function(function_name) => (empty_atom(), function_name),
474                FunctionLikeIdentifier::Method(class_name, function_name) => (class_name, function_name),
475                _ => {
476                    // A reference from a closure can be ignored for now.
477                    return;
478                }
479            }
480        } else if let Some(calling_class) = scope.get_class_like_name() {
481            (ascii_lowercase_atom(&calling_class), empty_atom())
482        } else {
483            return; // Cannot record reference without a source context
484        };
485
486        self.symbol_references_to_overridden_members.entry(referencing_key).or_default().insert(class_member);
487    }
488
489    /// Convenience method to add a reference *from* the current function context *to* a top-level symbol.
490    /// Delegates to appropriate `add_*` methods based on the function context.
491    #[inline]
492    pub fn add_reference_to_symbol(&mut self, scope: &ScopeContext, symbol: Atom, in_signature: bool) {
493        if let Some(referencing_functionlike) = scope.get_function_like_identifier() {
494            match referencing_functionlike {
495                FunctionLikeIdentifier::Function(function_name) => {
496                    self.add_symbol_reference_to_symbol(function_name, symbol, in_signature);
497                }
498                FunctionLikeIdentifier::Method(class_name, function_name) => {
499                    self.add_class_member_reference_to_symbol((class_name, function_name), symbol, in_signature);
500                }
501                _ => {
502                    // Ignore references from closures.
503                }
504            }
505        } else if let Some(calling_class) = scope.get_class_like_name() {
506            self.add_symbol_reference_to_symbol(ascii_lowercase_atom(&calling_class), symbol, in_signature);
507        }
508    }
509
510    /// Records that one function/method references the return value of another. Used for dead code analysis.
511    #[inline]
512    pub fn add_reference_to_functionlike_return(
513        &mut self,
514        referencing_functionlike: FunctionLikeIdentifier,
515        referenced_functionlike: FunctionLikeIdentifier,
516    ) {
517        if referencing_functionlike == referenced_functionlike {
518            return;
519        }
520
521        self.functionlike_references_to_functionlike_returns
522            .entry(referencing_functionlike)
523            .or_default()
524            .insert(referenced_functionlike);
525    }
526
527    /// Merges references from another `SymbolReferences` instance into this one.
528    /// Existing references are extended, not replaced.
529    #[inline]
530    pub fn extend(&mut self, other: Self) {
531        for (k, v) in other.symbol_references_to_symbols {
532            self.symbol_references_to_symbols.entry(k).or_default().extend(v);
533        }
534        for (k, v) in other.symbol_references_to_symbols_in_signature {
535            self.symbol_references_to_symbols_in_signature.entry(k).or_default().extend(v);
536        }
537        for (k, v) in other.symbol_references_to_overridden_members {
538            self.symbol_references_to_overridden_members.entry(k).or_default().extend(v);
539        }
540        for (k, v) in other.functionlike_references_to_functionlike_returns {
541            self.functionlike_references_to_functionlike_returns.entry(k).or_default().extend(v);
542        }
543
544        for (k, v) in other.file_references_to_symbols {
545            self.file_references_to_symbols.entry(k).or_default().extend(v);
546        }
547
548        for (k, v) in other.file_references_to_symbols_in_signature {
549            self.file_references_to_symbols_in_signature.entry(k).or_default().extend(v);
550        }
551
552        for (k, v) in other.property_write_references {
553            self.property_write_references.entry(k).or_default().extend(v);
554        }
555
556        for (k, v) in other.property_read_references {
557            self.property_read_references.entry(k).or_default().extend(v);
558        }
559    }
560
561    /// Computes the set of all unique symbols and members that are referenced *by* any symbol/member
562    /// tracked in the body or signature reference maps.
563    ///
564    /// # Returns
565    ///
566    /// A `HashSet` containing `&(SymbolName, MemberName)` tuples of all referenced items.
567    #[inline]
568    #[must_use]
569    pub fn get_referenced_symbols_and_members(&self) -> HashSet<&SymbolIdentifier> {
570        let mut referenced_items = HashSet::default();
571        for refs in self.symbol_references_to_symbols.values() {
572            referenced_items.extend(refs.iter());
573        }
574        for refs in self.symbol_references_to_symbols_in_signature.values() {
575            referenced_items.extend(refs.iter());
576        }
577
578        referenced_items
579    }
580
581    /// Computes the inverse of the body and signature reference maps.
582    ///
583    /// # Returns
584    ///
585    /// A `HashMap` where the key is the referenced symbol/member `(Symbol, Member)` and the value
586    /// is a `HashSet` of referencing symbols/members `(RefSymbol, RefMember)`.
587    #[inline]
588    #[must_use]
589    pub fn get_back_references(&self) -> HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> {
590        let mut back_refs: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> = HashMap::default();
591
592        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols {
593            for referenced_item in referenced_items {
594                back_refs.entry(*referenced_item).or_default().insert(*referencing_item);
595            }
596        }
597        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
598            for referenced_item in referenced_items {
599                back_refs.entry(*referenced_item).or_default().insert(*referencing_item);
600            }
601        }
602        back_refs
603    }
604
605    /// Finds all symbols/members that reference a specific target symbol/member.
606    /// Checks both body and signature references.
607    ///
608    /// # Arguments
609    ///
610    /// * `target_symbol`: The `(SymbolName, MemberName)` tuple being referenced.
611    ///
612    /// # Returns
613    ///
614    /// A `HashSet` containing `&(RefSymbol, RefMember)` tuples of all items referencing the target.
615    #[inline]
616    #[must_use]
617    pub fn get_references_to_symbol(&self, target_symbol: SymbolIdentifier) -> HashSet<&SymbolIdentifier> {
618        let mut referencing_items = HashSet::default();
619        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols {
620            if referenced_items.contains(&target_symbol) {
621                referencing_items.insert(referencing_item);
622            }
623        }
624        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
625            if referenced_items.contains(&target_symbol) {
626                referencing_items.insert(referencing_item);
627            }
628        }
629        referencing_items
630    }
631
632    /// Computes the count of references for each unique symbol/member referenced in bodies or signatures.
633    ///
634    /// # Returns
635    ///
636    /// A `HashMap` where the key is the referenced symbol/member `(Symbol, Member)` and the value
637    /// is the total count (`u32`) of references to it.
638    #[inline]
639    #[must_use]
640    pub fn get_referenced_symbols_and_members_with_counts(&self) -> HashMap<SymbolIdentifier, u32> {
641        let mut counts = HashMap::default();
642        for referenced_items in self.symbol_references_to_symbols.values() {
643            for referenced_item in referenced_items {
644                *counts.entry(*referenced_item).or_insert(0) += 1;
645            }
646        }
647        for referenced_items in self.symbol_references_to_symbols_in_signature.values() {
648            for referenced_item in referenced_items {
649                *counts.entry(*referenced_item).or_insert(0) += 1;
650            }
651        }
652        counts
653    }
654
655    /// Computes the inverse of the overridden member reference map.
656    ///
657    /// # Returns
658    ///
659    /// A `HashMap` where the key is the overridden member `(ParentSymbol, Member)` and the value
660    /// is a `HashSet` of referencing symbols/members `(RefSymbol, RefMember)` that call it via `parent::`.
661    #[inline]
662    #[must_use]
663    pub fn get_referenced_overridden_class_members(&self) -> HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> {
664        let mut back_refs: HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> = HashMap::default();
665
666        for (referencing_item, referenced_items) in &self.symbol_references_to_overridden_members {
667            for referenced_item in referenced_items {
668                back_refs.entry(*referenced_item).or_default().insert(*referencing_item);
669            }
670        }
671        back_refs
672    }
673
674    /// Calculates sets of invalid symbols and members based on detected code changes (`CodebaseDiff`).
675    /// Propagates invalidation through the dependency graph stored in signature references.
676    /// Limits propagation expense to avoid excessive computation on large changes.
677    ///
678    /// # Arguments
679    ///
680    /// * `codebase_diff`: Information about added, deleted, or modified symbols/signatures.
681    ///
682    /// # Returns
683    ///
684    /// `Some((invalid_signatures, partially_invalid))` on success, where `invalid_signatures` contains
685    /// all symbol/member pairs whose signature is invalid (including propagated ones), and `partially_invalid`
686    /// contains symbols with at least one invalid member.
687    /// Returns `None` if the propagation exceeds an expense limit (currently 5000 steps).
688    #[inline]
689    #[must_use]
690    pub fn get_invalid_symbols(&self, codebase_diff: &CodebaseDiff) -> Option<(HashSet<SymbolIdentifier>, AtomSet)> {
691        let mut invalid_signatures = HashSet::default();
692        let mut partially_invalid_symbols = AtomSet::default();
693
694        let mut sig_reverse_index: HashMap<SymbolIdentifier, Vec<SymbolIdentifier>> = HashMap::default();
695        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
696            let containing_symbol = (referencing_item.0, empty_atom());
697            if codebase_diff.contains_changed_entry(&containing_symbol) {
698                invalid_signatures.insert(*referencing_item);
699                partially_invalid_symbols.insert(referencing_item.0);
700            }
701
702            for referenced in referenced_items {
703                sig_reverse_index.entry(*referenced).or_default().push(*referencing_item);
704            }
705        }
706
707        // Start with symbols directly added/deleted in the diff.
708        let mut symbols_to_process = codebase_diff.get_changed().iter().copied().collect::<Vec<_>>();
709        let mut processed_symbols = HashSet::default();
710        let mut expense_counter = 0;
711
712        const EXPENSE_LIMIT: usize = 5000;
713        while let Some(invalidated_item) = symbols_to_process.pop() {
714            if processed_symbols.contains(&invalidated_item) {
715                continue;
716            }
717
718            expense_counter += 1;
719            if expense_counter > EXPENSE_LIMIT {
720                return None;
721            }
722
723            // Mark this item as invalid (signature) and processed
724            invalid_signatures.insert(invalidated_item);
725            processed_symbols.insert(invalidated_item);
726            if !invalidated_item.1.is_empty() {
727                // If it's a member, also mark its containing symbol for processing.
728                partially_invalid_symbols.insert(invalidated_item.0);
729                let containing_symbol = (invalidated_item.0, empty_atom());
730                if !processed_symbols.contains(&containing_symbol) {
731                    symbols_to_process.push(containing_symbol);
732                }
733            }
734
735            // Find all items that reference this now-invalid item *in their signature*
736            if let Some(referencing_items) = sig_reverse_index.get(&invalidated_item) {
737                for referencing_item in referencing_items {
738                    if !processed_symbols.contains(referencing_item) {
739                        symbols_to_process.push(*referencing_item);
740                    }
741
742                    invalid_signatures.insert(*referencing_item);
743                    if !referencing_item.1.is_empty() {
744                        partially_invalid_symbols.insert(referencing_item.0);
745                    }
746                }
747            }
748        }
749
750        // An item's body is invalid if it references (anywhere, body or sig) an item with an invalid signature.
751        // Check both body and signature reference maps in a single pass where possible.
752        let mut invalid_bodies = HashSet::default();
753
754        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols {
755            if referenced_items.iter().any(|r| invalid_signatures.contains(r)) {
756                invalid_bodies.insert(*referencing_item);
757                if !referencing_item.1.is_empty() {
758                    partially_invalid_symbols.insert(referencing_item.0);
759                }
760            }
761        }
762
763        for (referencing_item, referenced_items) in &self.symbol_references_to_symbols_in_signature {
764            if referenced_items.iter().any(|r| invalid_signatures.contains(r)) {
765                invalid_bodies.insert(*referencing_item);
766                if !referencing_item.1.is_empty() {
767                    partially_invalid_symbols.insert(referencing_item.0);
768                }
769            }
770        }
771
772        let mut all_invalid_symbols = invalid_signatures;
773        all_invalid_symbols.extend(invalid_bodies);
774        Some((all_invalid_symbols, partially_invalid_symbols))
775    }
776
777    /// Extracts references originating from safe (skipped) symbols and merges them into this instance.
778    ///
779    /// When incremental analysis runs with `diff = true`, the analyzer skips safe symbols,
780    /// which means their body references are not collected. This method copies those missing
781    /// references from the previous run's reference graph.
782    ///
783    /// Only references from symbols that are in `safe_symbols` or `safe_symbol_members`
784    /// (and not already present in this instance) are copied.
785    ///
786    /// # Arguments
787    ///
788    /// * `previous` - The previous run's complete symbol references
789    /// * `safe_symbols` - Set of safe top-level symbol names
790    /// * `safe_symbol_members` - Set of safe (symbol, member) pairs
791    #[inline]
792    pub fn restore_references_for_safe_symbols(
793        &mut self,
794        previous: &SymbolReferences,
795        safe_symbols: &AtomSet,
796        safe_symbol_members: &HashSet<SymbolIdentifier>,
797    ) {
798        let is_safe = |key: &SymbolIdentifier| -> bool {
799            if key.1.is_empty() { safe_symbols.contains(&key.0) } else { safe_symbol_members.contains(key) }
800        };
801
802        // Restore body references for safe symbols
803        for (key, refs) in &previous.symbol_references_to_symbols {
804            if is_safe(key) && !self.symbol_references_to_symbols.contains_key(key) {
805                self.symbol_references_to_symbols.insert(*key, refs.clone());
806            }
807        }
808
809        // Restore overridden member references for safe symbols
810        for (key, refs) in &previous.symbol_references_to_overridden_members {
811            if is_safe(key) && !self.symbol_references_to_overridden_members.contains_key(key) {
812                self.symbol_references_to_overridden_members.insert(*key, refs.clone());
813            }
814        }
815
816        // Restore function-like return references for safe symbols
817        for (key, refs) in &previous.functionlike_references_to_functionlike_returns {
818            let sym_key = match key {
819                FunctionLikeIdentifier::Function(name) => (*name, mago_atom::empty_atom()),
820                FunctionLikeIdentifier::Method(class, method) => (*class, *method),
821                _ => continue,
822            };
823
824            if is_safe(&sym_key) && !self.functionlike_references_to_functionlike_returns.contains_key(key) {
825                self.functionlike_references_to_functionlike_returns.insert(*key, refs.clone());
826            }
827        }
828
829        // Restore property write references for safe symbols
830        for (key, refs) in &previous.property_write_references {
831            if is_safe(key) && !self.property_write_references.contains_key(key) {
832                self.property_write_references.insert(*key, refs.clone());
833            }
834        }
835
836        // Restore property read references for safe symbols
837        for (key, refs) in &previous.property_read_references {
838            if is_safe(key) && !self.property_read_references.contains_key(key) {
839                self.property_read_references.insert(*key, refs.clone());
840            }
841        }
842    }
843
844    /// Removes **body** references originating from the given symbols/members.
845    ///
846    /// Used by the body-only fast path: when only function/method bodies changed (no signature
847    /// changes), we remove old body references and let the analyzer rebuild them fresh.
848    /// Signature references are kept because signatures didn't change.
849    ///
850    /// Also removes function-like return references and property read/write references from
851    /// the given symbols, as those originate from body code.
852    ///
853    /// File-level references keyed by the given file names are also removed.
854    #[inline]
855    pub fn remove_body_references_for_symbols(
856        &mut self,
857        symbols_and_members: &HashSet<SymbolIdentifier>,
858        file_names: &[Atom],
859    ) {
860        // Remove body (not signature) references
861        for key in symbols_and_members {
862            self.symbol_references_to_symbols.remove(key);
863            self.symbol_references_to_overridden_members.remove(key);
864            self.property_write_references.remove(key);
865            self.property_read_references.remove(key);
866        }
867
868        // Remove function-like return references for matching keys
869        self.functionlike_references_to_functionlike_returns.retain(|key, _| {
870            let sym_key = match key {
871                FunctionLikeIdentifier::Function(name) => (*name, mago_atom::empty_atom()),
872                FunctionLikeIdentifier::Method(class, method) => (*class, *method),
873                _ => return true,
874            };
875
876            !symbols_and_members.contains(&sym_key)
877        });
878
879        // Remove file-level body references (signature refs kept)
880        for name in file_names {
881            self.file_references_to_symbols.remove(name);
882        }
883    }
884
885    /// Removes all references *originating from* symbols/members that are marked as invalid.
886    ///
887    /// # Arguments
888    ///
889    /// * `invalid_symbols_and_members`: A set containing `(SymbolName, MemberName)` tuples for invalid items.
890    #[inline]
891    pub fn remove_references_from_invalid_symbols(&mut self, invalid_symbols_and_members: &HashSet<SymbolIdentifier>) {
892        // Retain only entries where the key (referencing item) is NOT in the invalid set.
893        self.symbol_references_to_symbols
894            .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
895        self.symbol_references_to_symbols_in_signature
896            .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
897        self.symbol_references_to_overridden_members
898            .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
899        self.property_write_references
900            .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
901        self.property_read_references
902            .retain(|referencing_item, _| !invalid_symbols_and_members.contains(referencing_item));
903    }
904
905    /// Retains only references originating from safe (unchanged) symbols, removing all others.
906    ///
907    /// This is the inverse of [`remove_references_from_invalid_symbols`]: instead of
908    /// specifying what to remove, you specify what to keep. References from non-safe symbols
909    /// will be rebuilt by `populate_codebase` and the analyzer.
910    ///
911    /// This method also retains all builtin/prelude references (those where the key symbol
912    /// is not user-defined, i.e., is in the base references).
913    #[inline]
914    pub fn retain_safe_symbol_references(
915        &mut self,
916        safe_symbols: &AtomSet,
917        safe_symbol_members: &HashSet<SymbolIdentifier>,
918    ) {
919        let is_safe = |key: &SymbolIdentifier| -> bool {
920            if key.1.is_empty() { safe_symbols.contains(&key.0) } else { safe_symbol_members.contains(key) }
921        };
922
923        self.symbol_references_to_symbols.retain(|k, _| is_safe(k));
924        self.symbol_references_to_symbols_in_signature.retain(|k, _| is_safe(k));
925        self.symbol_references_to_overridden_members.retain(|k, _| is_safe(k));
926        self.property_write_references.retain(|k, _| is_safe(k));
927        self.property_read_references.retain(|k, _| is_safe(k));
928
929        self.functionlike_references_to_functionlike_returns.retain(|key, _| {
930            let sym_key = match key {
931                FunctionLikeIdentifier::Function(name) => (*name, mago_atom::empty_atom()),
932                FunctionLikeIdentifier::Method(class, method) => (*class, *method),
933                _ => return true, // Keep closures and other non-symbol function-likes
934            };
935
936            is_safe(&sym_key)
937        });
938    }
939
940    /// Removes references for dirty (non-safe) symbols — O(dirty) instead of O(all).
941    ///
942    /// This is the inverse of [`retain_safe_symbol_references`]: instead of iterating all
943    /// entries and keeping safe ones, it directly removes entries for the given dirty set.
944    /// Much faster when the dirty set is small relative to the total number of references.
945    pub fn remove_dirty_symbol_references(&mut self, dirty_symbols: &HashSet<SymbolIdentifier>) {
946        for key in dirty_symbols {
947            self.symbol_references_to_symbols.remove(key);
948            self.symbol_references_to_symbols_in_signature.remove(key);
949            self.symbol_references_to_overridden_members.remove(key);
950            self.property_write_references.remove(key);
951            self.property_read_references.remove(key);
952
953            let fl_key = if key.1.is_empty() {
954                FunctionLikeIdentifier::Function(key.0)
955            } else {
956                FunctionLikeIdentifier::Method(key.0, key.1)
957            };
958
959            self.functionlike_references_to_functionlike_returns.remove(&fl_key);
960        }
961    }
962
963    /// Returns a reference to the map tracking references within symbol/member bodies.
964    #[inline]
965    #[must_use]
966    pub fn get_symbol_references_to_symbols(&self) -> &HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> {
967        &self.symbol_references_to_symbols
968    }
969
970    /// Returns a reference to the map tracking references within symbol/member signatures.
971    #[inline]
972    #[must_use]
973    pub fn get_symbol_references_to_symbols_in_signature(
974        &self,
975    ) -> &HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> {
976        &self.symbol_references_to_symbols_in_signature
977    }
978
979    /// Returns a reference to the map tracking references to overridden members.
980    #[inline]
981    #[must_use]
982    pub fn get_symbol_references_to_overridden_members(&self) -> &HashMap<SymbolIdentifier, HashSet<SymbolIdentifier>> {
983        &self.symbol_references_to_overridden_members
984    }
985
986    /// Returns a reference to the map tracking references to function-like return values.
987    #[inline]
988    #[must_use]
989    pub fn get_functionlike_references_to_functionlike_returns(
990        &self,
991    ) -> &HashMap<FunctionLikeIdentifier, HashSet<FunctionLikeIdentifier>> {
992        &self.functionlike_references_to_functionlike_returns
993    }
994
995    /// Returns a reference to the map tracking file-level references to symbols (body).
996    #[inline]
997    #[must_use]
998    pub fn get_file_references_to_symbols(&self) -> &HashMap<Atom, HashSet<SymbolIdentifier>> {
999        &self.file_references_to_symbols
1000    }
1001
1002    /// Returns a reference to the map tracking file-level references to symbols (signature).
1003    #[inline]
1004    #[must_use]
1005    pub fn get_file_references_to_symbols_in_signature(&self) -> &HashMap<Atom, HashSet<SymbolIdentifier>> {
1006        &self.file_references_to_symbols_in_signature
1007    }
1008}
1009
1010#[cfg(test)]
1011#[allow(clippy::unwrap_used, clippy::expect_used)]
1012mod tests {
1013    use super::*;
1014    use mago_atom::atom;
1015    use mago_atom::empty_atom;
1016
1017    fn make_refs_with_body(entries: Vec<(SymbolIdentifier, Vec<SymbolIdentifier>)>) -> SymbolReferences {
1018        let mut refs = SymbolReferences::new();
1019        for (key, values) in entries {
1020            let set: HashSet<SymbolIdentifier> = values.into_iter().collect();
1021            refs.symbol_references_to_symbols.insert(key, set);
1022        }
1023        refs
1024    }
1025
1026    #[test]
1027    fn test_restore_references_for_safe_symbols_restores_missing_body_refs() {
1028        let class_a = atom("class_a");
1029        let class_b = atom("class_b");
1030        let method_foo = atom("foo");
1031        let method_bar = atom("bar");
1032
1033        let previous = make_refs_with_body(vec![
1034            ((class_a, method_foo), vec![(class_b, empty_atom())]),
1035            ((class_b, method_bar), vec![(class_a, empty_atom())]),
1036        ]);
1037
1038        let mut current = make_refs_with_body(vec![((class_b, method_bar), vec![(class_a, empty_atom())])]);
1039
1040        let safe_symbols = AtomSet::default();
1041        let mut safe_members = HashSet::default();
1042        safe_members.insert((class_a, method_foo));
1043
1044        current.restore_references_for_safe_symbols(&previous, &safe_symbols, &safe_members);
1045
1046        assert!(current.symbol_references_to_symbols.contains_key(&(class_a, method_foo)));
1047        let restored = &current.symbol_references_to_symbols[&(class_a, method_foo)];
1048        assert!(restored.contains(&(class_b, empty_atom())));
1049
1050        assert!(current.symbol_references_to_symbols.contains_key(&(class_b, method_bar)));
1051    }
1052
1053    #[test]
1054    fn test_restore_references_does_not_overwrite_existing() {
1055        let class_a = atom("class_a");
1056        let class_b = atom("class_b");
1057        let class_c = atom("class_c");
1058        let method_foo = atom("foo");
1059
1060        let previous = make_refs_with_body(vec![((class_a, method_foo), vec![(class_b, empty_atom())])]);
1061
1062        let mut current = make_refs_with_body(vec![((class_a, method_foo), vec![(class_c, empty_atom())])]);
1063
1064        let safe_symbols = AtomSet::default();
1065        let mut safe_members = HashSet::default();
1066        safe_members.insert((class_a, method_foo));
1067
1068        current.restore_references_for_safe_symbols(&previous, &safe_symbols, &safe_members);
1069
1070        let refs = &current.symbol_references_to_symbols[&(class_a, method_foo)];
1071        assert!(refs.contains(&(class_c, empty_atom())));
1072        assert!(!refs.contains(&(class_b, empty_atom())));
1073    }
1074
1075    #[test]
1076    fn test_restore_references_for_safe_top_level_symbols() {
1077        let func_a = atom("func_a");
1078        let class_b = atom("class_b");
1079
1080        let previous = make_refs_with_body(vec![((func_a, empty_atom()), vec![(class_b, empty_atom())])]);
1081
1082        let mut current = SymbolReferences::new();
1083
1084        let mut safe_symbols = AtomSet::default();
1085        safe_symbols.insert(func_a);
1086        let safe_members = HashSet::default();
1087
1088        current.restore_references_for_safe_symbols(&previous, &safe_symbols, &safe_members);
1089
1090        assert!(current.symbol_references_to_symbols.contains_key(&(func_a, empty_atom())));
1091        let restored = &current.symbol_references_to_symbols[&(func_a, empty_atom())];
1092        assert!(restored.contains(&(class_b, empty_atom())));
1093    }
1094
1095    #[test]
1096    fn test_restore_skips_non_safe_symbols() {
1097        let func_a = atom("func_a");
1098        let class_b = atom("class_b");
1099        let previous = make_refs_with_body(vec![((func_a, empty_atom()), vec![(class_b, empty_atom())])]);
1100
1101        let mut current = SymbolReferences::new();
1102
1103        let safe_symbols = AtomSet::default();
1104        let safe_members = HashSet::default();
1105
1106        current.restore_references_for_safe_symbols(&previous, &safe_symbols, &safe_members);
1107
1108        assert!(!current.symbol_references_to_symbols.contains_key(&(func_a, empty_atom())));
1109    }
1110
1111    #[test]
1112    fn test_get_invalid_symbols_basic_cascade() {
1113        let class_a = atom("class_a");
1114        let class_b = atom("class_b");
1115        let method_foo = atom("foo");
1116
1117        let mut refs = SymbolReferences::new();
1118        refs.symbol_references_to_symbols_in_signature.insert((class_b, method_foo), {
1119            let mut set = HashSet::default();
1120            set.insert((class_a, empty_atom()));
1121            set
1122        });
1123
1124        let mut diff = crate::diff::CodebaseDiff::new();
1125        let mut changed = HashSet::default();
1126        changed.insert((class_a, empty_atom()));
1127        diff = diff.with_changed(changed);
1128
1129        let result = refs.get_invalid_symbols(&diff);
1130        assert!(result.is_some());
1131        let (invalid, partially_invalid) = result.unwrap();
1132
1133        assert!(invalid.contains(&(class_a, empty_atom())));
1134        assert!(invalid.contains(&(class_b, method_foo)));
1135        assert!(partially_invalid.contains(&class_b));
1136    }
1137}