mago_codex/
lib.rs

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