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