mago_codex/
lib.rs

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