Skip to main content

mago_codex/
reference.rs

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