mago_codex/
lib.rs

1use std::borrow::Cow;
2
3use mago_atom::Atom;
4use mago_atom::AtomSet;
5use mago_atom::ascii_lowercase_atom;
6use mago_atom::ascii_lowercase_constant_name_atom;
7use mago_atom::atom;
8use mago_atom::empty_atom;
9use mago_atom::u32_atom;
10use mago_atom::u64_atom;
11use mago_database::file::FileId;
12use mago_span::Position;
13use mago_span::Span;
14
15use crate::identifier::method::MethodIdentifier;
16use crate::metadata::CodebaseMetadata;
17use crate::metadata::class_like::ClassLikeMetadata;
18use crate::metadata::class_like_constant::ClassLikeConstantMetadata;
19use crate::metadata::constant::ConstantMetadata;
20use crate::metadata::enum_case::EnumCaseMetadata;
21use crate::metadata::function_like::FunctionLikeMetadata;
22use crate::metadata::property::PropertyMetadata;
23use crate::metadata::ttype::TypeMetadata;
24use crate::symbol::SymbolKind;
25use crate::ttype::atomic::TAtomic;
26use crate::ttype::atomic::object::TObject;
27use crate::ttype::union::TUnion;
28
29pub mod assertion;
30pub mod consts;
31pub mod context;
32pub mod diff;
33pub mod flags;
34pub mod identifier;
35pub mod issue;
36pub mod metadata;
37pub mod misc;
38pub mod populator;
39pub mod reference;
40pub mod scanner;
41pub mod symbol;
42pub mod ttype;
43pub mod visibility;
44
45mod utils;
46
47/// Checks if a global function exists in the codebase.
48///
49/// This lookup is case-insensitive, in line with PHP's behavior for function names.
50pub fn function_exists(codebase: &CodebaseMetadata, function_name: &str) -> bool {
51    let lowercase_function_name = ascii_lowercase_atom(function_name);
52    let function_identifier = (empty_atom(), lowercase_function_name);
53
54    codebase.function_likes.contains_key(&function_identifier)
55}
56
57/// Checks if a global constant exists in the codebase.
58///
59/// The lookup for the namespace part of the constant name is case-insensitive,
60/// but the constant name itself is case-sensitive, matching PHP's behavior.
61pub fn constant_exists(codebase: &CodebaseMetadata, constant_name: &str) -> bool {
62    let lowercase_constant_name = ascii_lowercase_constant_name_atom(constant_name);
63
64    codebase.constants.contains_key(&lowercase_constant_name)
65}
66
67/// Checks if a class exists in the codebase.
68///
69/// This lookup is case-insensitive, in line with PHP's behavior for class names.
70pub fn class_exists(codebase: &CodebaseMetadata, name: &str) -> bool {
71    let lowercase_name = ascii_lowercase_atom(name);
72
73    matches!(codebase.symbols.get_kind(&lowercase_name), Some(SymbolKind::Class))
74}
75
76/// Checks if a class or trait exists in the codebase.
77///
78/// This lookup is case-insensitive, in line with PHP's behavior for class names.
79pub fn class_or_trait_exists(codebase: &CodebaseMetadata, name: &str) -> bool {
80    let lowercase_name = ascii_lowercase_atom(name);
81
82    matches!(codebase.symbols.get_kind(&lowercase_name), Some(SymbolKind::Class | SymbolKind::Trait))
83}
84
85/// Checks if an interface exists in the codebase.
86///
87/// This lookup is case-insensitive, in line with PHP's behavior for interface names.
88pub fn interface_exists(codebase: &CodebaseMetadata, name: &str) -> bool {
89    let lowercase_name = ascii_lowercase_atom(name);
90
91    matches!(codebase.symbols.get_kind(&lowercase_name), Some(SymbolKind::Interface))
92}
93
94/// Checks if a class or interface exists in the codebase.
95///
96/// This lookup is case-insensitive, in line with PHP's behavior for class names.
97pub fn class_or_interface_exists(codebase: &CodebaseMetadata, name: &str) -> bool {
98    let lowercase_name = ascii_lowercase_atom(name);
99
100    matches!(codebase.symbols.get_kind(&lowercase_name), Some(SymbolKind::Class | SymbolKind::Interface))
101}
102
103/// Checks if an enum exists in the codebase.
104///
105/// This lookup is case-insensitive, in line with PHP's behavior for enum names.
106pub fn enum_exists(codebase: &CodebaseMetadata, name: &str) -> bool {
107    let lowercase_name = ascii_lowercase_atom(name);
108
109    matches!(codebase.symbols.get_kind(&lowercase_name), Some(SymbolKind::Enum))
110}
111
112/// Checks if a trait exists in the codebase.
113///
114/// This lookup is case-insensitive, in line with PHP's behavior for trait names.
115pub fn trait_exists(codebase: &CodebaseMetadata, name: &str) -> bool {
116    let lowercase_name = ascii_lowercase_atom(name);
117
118    matches!(codebase.symbols.get_kind(&lowercase_name), Some(SymbolKind::Trait))
119}
120
121/// Checks if a class-like (class, interface, enum, or trait) exists in the codebase.
122///
123/// This lookup is case-insensitive.
124pub fn class_like_exists(codebase: &CodebaseMetadata, name: &str) -> bool {
125    let lowercase_name = ascii_lowercase_atom(name);
126
127    codebase.symbols.contains(&lowercase_name)
128}
129
130/// Checks if a method exists on a given class-like (including inherited methods).
131///
132/// The lookup for both the class-like name and the method name is case-insensitive.
133pub fn method_exists(codebase: &CodebaseMetadata, fqcn: &str, method_name: &str) -> bool {
134    let lowercase_fqcn = ascii_lowercase_atom(fqcn);
135    let lowercase_method_name = ascii_lowercase_atom(method_name);
136
137    codebase
138        .class_likes
139        .get(&lowercase_fqcn)
140        .is_some_and(|meta| meta.appearing_method_ids.contains_key(&lowercase_method_name))
141}
142
143pub fn method_identifier_exists(codebase: &CodebaseMetadata, method_identifier: &MethodIdentifier) -> bool {
144    let lowercase_fqcn = ascii_lowercase_atom(method_identifier.get_class_name());
145    let lowercase_method_name = ascii_lowercase_atom(method_identifier.get_method_name());
146
147    let method_identifier = (lowercase_fqcn, lowercase_method_name);
148
149    codebase.function_likes.contains_key(&method_identifier)
150}
151
152pub fn is_method_abstract(codebase: &CodebaseMetadata, fqcn: &str, method_name: &str) -> bool {
153    let lowercase_fqcn = ascii_lowercase_atom(fqcn);
154    let lowercase_method_name = ascii_lowercase_atom(method_name);
155
156    let method_identifier = (lowercase_fqcn, lowercase_method_name);
157
158    codebase
159        .function_likes
160        .get(&method_identifier)
161        .and_then(|meta| meta.method_metadata.as_ref())
162        .is_some_and(|method| method.is_abstract)
163}
164
165pub fn is_method_static(codebase: &CodebaseMetadata, fqcn: &str, method_name: &str) -> bool {
166    let lowercase_fqcn = ascii_lowercase_atom(fqcn);
167    let lowercase_method_name = ascii_lowercase_atom(method_name);
168
169    let method_identifier = (lowercase_fqcn, lowercase_method_name);
170
171    codebase
172        .function_likes
173        .get(&method_identifier)
174        .and_then(|meta| meta.method_metadata.as_ref())
175        .is_some_and(|method| method.is_static)
176}
177
178pub fn is_method_final(codebase: &CodebaseMetadata, fqcn: &str, method_name: &str) -> bool {
179    let lowercase_fqcn = ascii_lowercase_atom(fqcn);
180    let lowercase_method_name = ascii_lowercase_atom(method_name);
181
182    let method_identifier = (lowercase_fqcn, lowercase_method_name);
183
184    codebase
185        .function_likes
186        .get(&method_identifier)
187        .and_then(|meta| meta.method_metadata.as_ref())
188        .is_some_and(|method| method.is_final)
189}
190
191/// Checks if a property exists on a given class-like (including inherited properties).
192///
193/// The lookup for the class-like name is case-insensitive, but the property name is case-sensitive.
194pub fn property_exists(codebase: &CodebaseMetadata, fqcn: &str, property_name: &str) -> bool {
195    let lowercase_fqcn = ascii_lowercase_atom(fqcn);
196
197    codebase
198        .class_likes
199        .get(&lowercase_fqcn)
200        .is_some_and(|meta| meta.appearing_property_ids.contains_key(&atom(property_name)))
201}
202
203/// Checks if a method is declared directly on a given class-like (not inherited).
204///
205/// The lookup for both the class-like name and the method name is case-insensitive.
206pub fn declaring_method_exists(codebase: &CodebaseMetadata, fqcn: &str, method_name: &str) -> bool {
207    let lowercase_fqcn = ascii_lowercase_atom(fqcn);
208    let lowercase_method_name = ascii_lowercase_atom(method_name);
209
210    codebase
211        .class_likes
212        .get(&lowercase_fqcn)
213        .is_some_and(|meta| meta.declaring_method_ids.contains_key(&lowercase_method_name))
214}
215
216/// Checks if a property is declared directly on a given class-like (not inherited).
217///
218/// The lookup for the class-like name is case-insensitive, but the property name is case-sensitive.
219pub fn declaring_property_exists(codebase: &CodebaseMetadata, fqcn: &str, property_name: &str) -> bool {
220    let lowercase_fqcn = ascii_lowercase_atom(fqcn);
221    let property_name = atom(property_name);
222
223    codebase.class_likes.get(&lowercase_fqcn).is_some_and(|meta| meta.properties.contains_key(&property_name))
224}
225
226/// Checks if a constant or enum case exists on a given class-like.
227///
228/// The lookup for the class-like name is case-insensitive, but the constant/case name is case-sensitive.
229pub fn class_like_constant_or_enum_case_exists(codebase: &CodebaseMetadata, fqcn: &str, constant_name: &str) -> bool {
230    let lowercase_fqcn = ascii_lowercase_atom(fqcn);
231    let constant_name = atom(constant_name);
232
233    if let Some(meta) = codebase.class_likes.get(&lowercase_fqcn) {
234        return meta.constants.contains_key(&constant_name) || meta.enum_cases.contains_key(&constant_name);
235    }
236
237    false
238}
239
240/// Retrieves the metadata for a global function.
241///
242/// This lookup is case-insensitive.
243pub fn get_function<'a>(codebase: &'a CodebaseMetadata, function_name: &str) -> Option<&'a FunctionLikeMetadata> {
244    let lowercase_function_name = ascii_lowercase_atom(function_name);
245    let function_identifier = (empty_atom(), lowercase_function_name);
246
247    codebase.function_likes.get(&function_identifier)
248}
249
250/// Retrieves the metadata for a closure based on its position in the source code.
251///
252/// This function uses the source ID and the closure's position to uniquely identify it.
253pub fn get_closure<'a>(
254    codebase: &'a CodebaseMetadata,
255    file_id: &FileId,
256    position: &Position,
257) -> Option<&'a FunctionLikeMetadata> {
258    let file_ref = u64_atom(file_id.as_u64());
259    let closure_ref = u32_atom(position.offset);
260    let identifier = (file_ref, closure_ref);
261
262    codebase.function_likes.get(&identifier)
263}
264
265/// Retrieves the metadata for a global constant.
266///
267/// The namespace lookup is case-insensitive, but the constant name itself is case-sensitive.
268pub fn get_constant<'a>(codebase: &'a CodebaseMetadata, constant_name: &str) -> Option<&'a ConstantMetadata> {
269    let lowercase_constant_name = ascii_lowercase_constant_name_atom(constant_name);
270
271    codebase.constants.get(&lowercase_constant_name)
272}
273
274/// Retrieves the metadata for a class.
275///
276/// This lookup is case-insensitive.
277pub fn get_class<'a>(codebase: &'a CodebaseMetadata, name: &str) -> Option<&'a ClassLikeMetadata> {
278    let lowercase_name = ascii_lowercase_atom(name);
279
280    if codebase.symbols.contains_class(&lowercase_name) { codebase.class_likes.get(&lowercase_name) } else { None }
281}
282
283/// Retrieves the metadata for an interface.
284///
285/// This lookup is case-insensitive.
286pub fn get_interface<'a>(codebase: &'a CodebaseMetadata, name: &str) -> Option<&'a ClassLikeMetadata> {
287    let lowercase_name = ascii_lowercase_atom(name);
288
289    if codebase.symbols.contains_interface(&lowercase_name) { codebase.class_likes.get(&lowercase_name) } else { None }
290}
291
292/// Retrieves the metadata for an enum.
293///
294/// This lookup is case-insensitive.
295pub fn get_enum<'a>(codebase: &'a CodebaseMetadata, name: &str) -> Option<&'a ClassLikeMetadata> {
296    let lowercase_name = ascii_lowercase_atom(name);
297
298    if codebase.symbols.contains_enum(&lowercase_name) { codebase.class_likes.get(&lowercase_name) } else { None }
299}
300
301/// Retrieves the metadata for a trait.
302///
303/// This lookup is case-insensitive.
304pub fn get_trait<'a>(codebase: &'a CodebaseMetadata, name: &str) -> Option<&'a ClassLikeMetadata> {
305    let lowercase_name = ascii_lowercase_atom(name);
306
307    if codebase.symbols.contains_trait(&lowercase_name) { codebase.class_likes.get(&lowercase_name) } else { None }
308}
309
310pub fn get_anonymous_class_name(span: Span) -> Atom {
311    use std::io::Write;
312
313    // A 64-byte buffer on the stack. This is ample space for the prefix,
314    // u64 file id, and 2 u32 integers, preventing any chance of a heap allocation.
315    let mut buffer = [0u8; 64];
316
317    // Use a block to limit the scope of the mutable writer
318    // `writer` is a mutable slice that implements `std::io::Write`.
319    let mut writer = &mut buffer[..];
320
321    // SAFETY: We use `unwrap_unchecked` here because we are writing to a fixed-size buffer
322    unsafe {
323        write!(writer, "class@anonymous:{}-{}:{}", span.file_id, span.start.offset, span.end.offset).unwrap_unchecked()
324    };
325
326    // Determine how many bytes were written by checking the length of the original buffer
327    // against what the `writer` had left. This is a common pattern for `io::Write` on slices.
328    let written_len = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
329
330    atom(
331        // SAFETY: We use `unwrap_unchecked` here because we are certain the bytes
332        // up to `written_len` are valid UTF-8.
333        unsafe { std::str::from_utf8(&buffer[..written_len]).unwrap_unchecked() },
334    )
335}
336
337/// Retrieves the metadata for an anonymous class based on its span.
338///
339/// This function generates a unique name for the anonymous class based on its span,
340/// which includes the source file and the start and end offsets.
341pub fn get_anonymous_class(codebase: &CodebaseMetadata, span: Span) -> Option<&ClassLikeMetadata> {
342    let name = get_anonymous_class_name(span);
343
344    if class_exists(codebase, &name) { codebase.class_likes.get(&name) } else { None }
345}
346
347/// Retrieves the metadata for any class-like (class, interface, enum, or trait).
348///
349/// This lookup is case-insensitive.
350pub fn get_class_like<'a>(codebase: &'a CodebaseMetadata, name: &str) -> Option<&'a ClassLikeMetadata> {
351    let lowercase_name = ascii_lowercase_atom(name);
352
353    codebase.class_likes.get(&lowercase_name)
354}
355
356pub fn get_declaring_class_for_property(codebase: &CodebaseMetadata, fqcn: &str, property_name: &str) -> Option<Atom> {
357    let lowercase_fqcn = ascii_lowercase_atom(fqcn);
358    let property_name = atom(property_name);
359
360    codebase.class_likes.get(&lowercase_fqcn)?.declaring_property_ids.get(&property_name).copied()
361}
362
363/// Retrieves the metadata for a property, searching the inheritance hierarchy.
364///
365/// This function finds where the property was originally declared and returns its metadata.
366/// The lookup for the class-like name is case-insensitive, but the property name is case-sensitive.
367pub fn get_declaring_property<'a>(
368    codebase: &'a CodebaseMetadata,
369    fqcn: &str,
370    property_name: &str,
371) -> Option<&'a PropertyMetadata> {
372    let lowercase_fqcn = ascii_lowercase_atom(fqcn);
373    let property_name = atom(property_name);
374
375    let declaring_fqcn = codebase.class_likes.get(&lowercase_fqcn)?.declaring_property_ids.get(&property_name)?;
376
377    codebase.class_likes.get(declaring_fqcn)?.properties.get(&property_name)
378}
379
380pub fn get_method_identifier(fqcn: &str, method_name: &str) -> MethodIdentifier {
381    MethodIdentifier::new(atom(fqcn), atom(method_name))
382}
383
384pub fn get_declaring_method_identifier(
385    codebase: &CodebaseMetadata,
386    method_identifier: &MethodIdentifier,
387) -> MethodIdentifier {
388    let lowercase_fqcn = ascii_lowercase_atom(method_identifier.get_class_name());
389    let lowercase_method_name = ascii_lowercase_atom(method_identifier.get_method_name());
390
391    let Some(class_like_metadata) = codebase.class_likes.get(&lowercase_fqcn) else {
392        // If the class-like doesn't exist, return the method ID as is
393        return *method_identifier;
394    };
395
396    if let Some(declaring_fqcn) = class_like_metadata.declaring_method_ids.get(&lowercase_method_name)
397        && let Some(declaring_class_metadata) = codebase.class_likes.get(declaring_fqcn)
398    {
399        return MethodIdentifier::new(declaring_class_metadata.original_name, *method_identifier.get_method_name());
400    };
401
402    if class_like_metadata.flags.is_abstract()
403        && let Some(overridden_classes) = class_like_metadata.overridden_method_ids.get(&lowercase_method_name)
404        && let Some(first_class) = overridden_classes.iter().next()
405        && let Some(first_class_metadata) = codebase.class_likes.get(first_class)
406    {
407        return MethodIdentifier::new(first_class_metadata.original_name, *method_identifier.get_method_name());
408    }
409
410    // If the method isn't declared in this class, return the method ID as is
411    *method_identifier
412}
413
414/// Retrieves the metadata for a method, searching the inheritance hierarchy.
415///
416/// This function finds where the method is declared (which could be an ancestor class/trait)
417/// and returns the metadata from there.
418///
419/// The lookup for both the class-like name and the method name is case-insensitive.
420pub fn get_declaring_method<'a>(
421    codebase: &'a CodebaseMetadata,
422    fqcn: &str,
423    method_name: &str,
424) -> Option<&'a FunctionLikeMetadata> {
425    let method_id = MethodIdentifier::new(atom(fqcn), atom(method_name));
426    let declaring_method_id = get_declaring_method_identifier(codebase, &method_id);
427
428    get_method(codebase, declaring_method_id.get_class_name(), declaring_method_id.get_method_name())
429}
430
431pub fn get_method_by_id<'a>(
432    codebase: &'a CodebaseMetadata,
433    method_identifier: &MethodIdentifier,
434) -> Option<&'a FunctionLikeMetadata> {
435    let lowercase_fqcn = ascii_lowercase_atom(method_identifier.get_class_name());
436    let lowercase_method_name = ascii_lowercase_atom(method_identifier.get_method_name());
437
438    let function_identifier = (lowercase_fqcn, lowercase_method_name);
439
440    codebase.function_likes.get(&function_identifier)
441}
442
443pub fn get_method<'a>(
444    codebase: &'a CodebaseMetadata,
445    fqcn: &str,
446    method_name: &str,
447) -> Option<&'a FunctionLikeMetadata> {
448    let lowercase_fqcn = ascii_lowercase_atom(fqcn);
449    let lowercase_method_name = ascii_lowercase_atom(method_name);
450    let function_like_identifier = (lowercase_fqcn, lowercase_method_name);
451
452    codebase.function_likes.get(&function_like_identifier)
453}
454
455/// Retrieves the metadata for a property that is declared directly on the given class-like.
456///
457/// This does not search the inheritance hierarchy.
458/// The lookup for the class-like name is case-insensitive, but the property name is case-sensitive.
459pub fn get_property<'a>(
460    codebase: &'a CodebaseMetadata,
461    fqcn: &str,
462    property_name: &str,
463) -> Option<&'a PropertyMetadata> {
464    let lowercase_fqcn = ascii_lowercase_atom(fqcn);
465    let property_name = atom(property_name);
466
467    codebase.class_likes.get(&lowercase_fqcn)?.properties.get(&property_name)
468}
469
470/// An enum to represent either a class constant or an enum case.
471#[derive(Debug, PartialEq)]
472pub enum ClassConstantOrEnumCase<'a> {
473    Constant(&'a ClassLikeConstantMetadata),
474    EnumCase(&'a EnumCaseMetadata),
475}
476
477/// Retrieves the metadata for a class constant or an enum case from a class-like.
478///
479/// The lookup for the class-like name is case-insensitive, but the constant/case name is case-sensitive.
480pub fn get_class_like_constant_or_enum_case<'a>(
481    codebase: &'a CodebaseMetadata,
482    fqcn: &str,
483    constant_name: &str,
484) -> Option<ClassConstantOrEnumCase<'a>> {
485    let lowercase_fqcn = ascii_lowercase_atom(fqcn);
486    let constant_name = atom(constant_name);
487
488    let class_like = codebase.class_likes.get(&lowercase_fqcn)?;
489
490    if let Some(constant_meta) = class_like.constants.get(&constant_name) {
491        return Some(ClassConstantOrEnumCase::Constant(constant_meta));
492    }
493
494    if let Some(enum_case_meta) = class_like.enum_cases.get(&constant_name) {
495        return Some(ClassConstantOrEnumCase::EnumCase(enum_case_meta));
496    }
497
498    None
499}
500
501/// Checks if a class-like is an instance of another class-like.
502///
503/// This function checks if the `child` class-like is an instance of the `parent` class-like
504/// by looking up their metadata in the codebase.
505pub fn is_instance_of(codebase: &CodebaseMetadata, child_name: &str, parent_name: &str) -> bool {
506    if child_name == parent_name {
507        return true;
508    }
509
510    let lowercase_child_name = ascii_lowercase_atom(child_name);
511    let lowercase_parent_name = ascii_lowercase_atom(parent_name);
512
513    if lowercase_child_name == lowercase_parent_name {
514        return true;
515    }
516
517    let Some(child_meta) = codebase.class_likes.get(&lowercase_child_name) else {
518        return false;
519    };
520
521    child_meta.has_parent(&lowercase_parent_name)
522}
523
524pub fn inherits_class(codebase: &CodebaseMetadata, child_name: &str, parent_name: &str) -> bool {
525    let lowercase_child_name = ascii_lowercase_atom(child_name);
526    let lowercase_parent_name = ascii_lowercase_atom(parent_name);
527
528    let Some(child_meta) = codebase.class_likes.get(&lowercase_child_name) else {
529        return false;
530    };
531
532    child_meta.all_parent_classes.contains(&lowercase_parent_name)
533}
534
535pub fn directly_inherits_class(codebase: &CodebaseMetadata, child_name: &str, parent_name: &str) -> bool {
536    let lowercase_child_name = ascii_lowercase_atom(child_name);
537    let lowercase_parent_name = ascii_lowercase_atom(parent_name);
538
539    let Some(child_meta) = codebase.class_likes.get(&lowercase_child_name) else {
540        return false;
541    };
542
543    child_meta.direct_parent_class.as_ref().is_some_and(|parent_class| parent_class == &lowercase_parent_name)
544}
545
546pub fn inherits_interface(codebase: &CodebaseMetadata, child_name: &str, parent_name: &str) -> bool {
547    let lowercase_child_name = ascii_lowercase_atom(child_name);
548    let lowercase_parent_name = ascii_lowercase_atom(parent_name);
549
550    let Some(child_meta) = codebase.class_likes.get(&lowercase_child_name) else {
551        return false;
552    };
553
554    child_meta.all_parent_interfaces.contains(&lowercase_parent_name)
555}
556
557pub fn directly_inherits_interface(codebase: &CodebaseMetadata, child_name: &str, parent_name: &str) -> bool {
558    let lowercase_child_name = ascii_lowercase_atom(child_name);
559    let lowercase_parent_name = ascii_lowercase_atom(parent_name);
560
561    let Some(child_meta) = codebase.class_likes.get(&lowercase_child_name) else {
562        return false;
563    };
564
565    child_meta.direct_parent_interfaces.contains(&lowercase_parent_name)
566}
567
568pub fn uses_trait(codebase: &CodebaseMetadata, child_name: &str, trait_name: &str) -> bool {
569    let lowercase_child_name = ascii_lowercase_atom(child_name);
570    let lowercase_trait_name = ascii_lowercase_atom(trait_name);
571
572    let Some(child_meta) = codebase.class_likes.get(&lowercase_child_name) else {
573        return false;
574    };
575
576    child_meta.used_traits.contains(&lowercase_trait_name)
577}
578
579/// Recursively collects all descendant class/interface/enum FQCNs for a given class-like structure.
580/// Uses the pre-computed `all_classlike_descendants` map if available, otherwise might be empty.
581/// Warning: Recursive; could stack overflow on extremely deep hierarchies if map isn't precomputed well.
582#[inline]
583pub fn get_all_descendants(codebase: &CodebaseMetadata, fqcn: &str) -> AtomSet {
584    let lowercase_fqcn = ascii_lowercase_atom(fqcn);
585
586    // This implementation assumes direct_classlike_descendants is populated correctly.
587    let mut all_descendants = AtomSet::default();
588    let mut queue = vec![&lowercase_fqcn];
589    let mut visited = AtomSet::default();
590    visited.insert(lowercase_fqcn); // Don't include self in descendants
591
592    while let Some(current_name) = queue.pop() {
593        if let Some(direct_descendants) = codebase.direct_classlike_descendants.get(current_name) {
594            for descendant in direct_descendants {
595                if visited.insert(*descendant) {
596                    // Add to results only if not visited before
597                    all_descendants.insert(*descendant);
598                    queue.push(descendant); // Add to queue for further exploration
599                }
600            }
601        }
602    }
603
604    all_descendants
605}
606
607/// Checks if a method is overridden from a parent class-like.
608///
609/// This function checks if the method with the given name in the specified class-like
610/// is overridden from a parent class-like by looking up the metadata in the codebase.
611///
612/// The lookup for both the class-like name and the method name is case-insensitive.
613pub fn is_method_overriding(codebase: &CodebaseMetadata, fqcn: &str, method_name: &str) -> bool {
614    let lowercase_method_name = ascii_lowercase_atom(method_name);
615
616    get_class_like(codebase, fqcn)
617        .is_some_and(|metadata| metadata.overridden_method_ids.contains_key(&lowercase_method_name))
618}
619
620pub fn get_function_like_thrown_types<'a>(
621    codebase: &'a CodebaseMetadata,
622    class_like: Option<&'a ClassLikeMetadata>,
623    function_like: &'a FunctionLikeMetadata,
624) -> &'a [TypeMetadata] {
625    if !function_like.thrown_types.is_empty() {
626        return function_like.thrown_types.as_slice();
627    }
628
629    if !function_like.kind.is_method() {
630        return &[];
631    }
632
633    let Some(class_like) = class_like else {
634        return &[];
635    };
636
637    let Some(method_name) = function_like.name.as_ref() else {
638        return &[];
639    };
640
641    for parent_class_name_id in class_like.overridden_method_ids.get(method_name).into_iter().flatten() {
642        let Some(parent_class) = codebase.class_likes.get(parent_class_name_id) else {
643            continue;
644        };
645
646        let parent_method_id = (*parent_class_name_id, *method_name);
647        if let Some(parent_method) = codebase.function_likes.get(&parent_method_id) {
648            let thrown = get_function_like_thrown_types(codebase, Some(parent_class), parent_method);
649            if !thrown.is_empty() {
650                return thrown;
651            }
652        }
653    }
654
655    &[]
656}
657
658/// Retrieves the type of a class constant, considering type hints and inferred types.
659/// Returns `None` if the class or constant doesn't exist, or type cannot be determined.
660#[inline]
661pub fn get_class_constant_type<'a>(
662    codebase: &'a CodebaseMetadata,
663    fq_class_name: &str,
664    constant_name: &str,
665) -> Option<Cow<'a, TUnion>> {
666    let class_metadata = get_class_like(codebase, fq_class_name)?;
667    let constant_name = atom(constant_name);
668
669    if class_metadata.kind.is_enum() && class_metadata.enum_cases.contains_key(&constant_name) {
670        let atomic = TAtomic::Object(TObject::new_enum_case(class_metadata.original_name, constant_name));
671
672        return Some(Cow::Owned(TUnion::from_atomic(atomic)));
673    }
674
675    // It's a regular class constant
676    let constant_metadata = class_metadata.constants.get(&constant_name)?;
677
678    // Prefer the type signature if available
679    if let Some(type_metadata) = constant_metadata.type_metadata.as_ref() {
680        // Return borrowed signature type directly
681        // (Original logic about boring scalars/is_this seemed complex and possibly specific
682        //  to a particular analysis stage; simplifying here to return declared type if present)
683        return Some(Cow::Borrowed(&type_metadata.type_union));
684    }
685
686    // Fall back to inferred type if no signature
687    constant_metadata.inferred_type.as_ref().map(|atomic_type| {
688        // Wrap the atomic type in a TUnion if returning inferred type
689        Cow::Owned(TUnion::from_atomic(atomic_type.clone()))
690    })
691}