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 source_id = position.source.value();
291    let closure_id = interner.intern(position.to_string());
292
293    codebase.function_likes.get(&(source_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!(
363        "class@anonymous:{}-{}:{}",
364        span.start.source.0.value(),
365        span.start.offset,
366        span.end.offset,
367    ))
368}
369
370/// Retrieves the metadata for an anonymous class based on its span.
371///
372/// This function generates a unique name for the anonymous class based on its span,
373/// which includes the source file and the start and end offsets.
374pub fn get_anonymous_class<'a>(
375    codebase: &'a CodebaseMetadata,
376    interner: &ThreadedInterner,
377    span: Span,
378) -> Option<&'a ClassLikeMetadata> {
379    let name = get_anonymous_class_name(interner, span);
380
381    if class_exists(codebase, interner, &name) { codebase.class_likes.get(&name) } else { None }
382}
383
384/// Retrieves the metadata for any class-like (class, interface, enum, or trait).
385///
386/// This lookup is case-insensitive.
387pub fn get_class_like<'a>(
388    codebase: &'a CodebaseMetadata,
389    interner: &ThreadedInterner,
390    id: &StringIdentifier,
391) -> Option<&'a ClassLikeMetadata> {
392    let lowered_id = interner.lowered(id);
393    codebase.class_likes.get(&lowered_id)
394}
395
396pub fn get_declaring_class_for_property(
397    codebase: &CodebaseMetadata,
398    interner: &ThreadedInterner,
399    fqc_id: &StringIdentifier,
400    property_id: &StringIdentifier,
401) -> Option<StringIdentifier> {
402    let lowered_fqc_id = interner.lowered(fqc_id);
403
404    let class_like = codebase.class_likes.get(&lowered_fqc_id)?;
405
406    class_like.declaring_property_ids.get(property_id).copied()
407}
408
409/// Retrieves the metadata for a property, searching the inheritance hierarchy.
410///
411/// This function finds where the property was originally declared and returns its metadata.
412/// The lookup for the class-like name is case-insensitive, but the property name is case-sensitive.
413pub fn get_declaring_property<'a>(
414    codebase: &'a CodebaseMetadata,
415    interner: &ThreadedInterner,
416    fqc_id: &StringIdentifier,
417    property_id: &StringIdentifier,
418) -> Option<&'a PropertyMetadata> {
419    let declaring_fqc_id = get_declaring_class_for_property(codebase, interner, fqc_id, property_id)?;
420    let declaring_class_like = codebase.class_likes.get(&declaring_fqc_id)?;
421
422    declaring_class_like.properties.get(property_id)
423}
424
425pub fn get_method_id(fqc_id: &StringIdentifier, method_name_id: &StringIdentifier) -> MethodIdentifier {
426    MethodIdentifier::new(*fqc_id, *method_name_id)
427}
428
429pub fn get_declaring_method_id(
430    codebase: &CodebaseMetadata,
431    interner: &ThreadedInterner,
432    method_id: &MethodIdentifier,
433) -> MethodIdentifier {
434    let lowered_fqc_id = interner.lowered(method_id.get_class_name());
435    let lowered_method_id = interner.lowered(method_id.get_method_name());
436
437    let Some(class_like_metadata) = codebase.class_likes.get(&lowered_fqc_id) else {
438        // If the class-like doesn't exist, return the method ID as is
439        return *method_id;
440    };
441
442    let declaring_method_ids = class_like_metadata.get_declaring_method_ids();
443    if let Some(declaring_fqcn) = declaring_method_ids.get(&lowered_method_id)
444        && let Some(declaring_class_metadata) = codebase.class_likes.get(declaring_fqcn)
445    {
446        return MethodIdentifier::new(declaring_class_metadata.original_name, *method_id.get_method_name());
447    };
448
449    if class_like_metadata.is_abstract {
450        let overridden_method_ids = class_like_metadata.get_overridden_method_ids();
451        if let Some(overridden_classes) = overridden_method_ids.get(&lowered_method_id)
452            && let Some(first_class) = overridden_classes.iter().next()
453            && let Some(first_class_metadata) = codebase.class_likes.get(first_class)
454        {
455            return MethodIdentifier::new(first_class_metadata.original_name, *method_id.get_method_name());
456        }
457    }
458
459    // If the method isn't declared in this class, return the method ID as is
460    *method_id
461}
462
463/// Retrieves the metadata for a method, searching the inheritance hierarchy.
464///
465/// This function finds where the method is declared (which could be an ancestor class/trait)
466/// and returns the metadata from there.
467///
468/// The lookup for both the class-like name and the method name is case-insensitive.
469pub fn get_declaring_method<'a>(
470    codebase: &'a CodebaseMetadata,
471    interner: &ThreadedInterner,
472    fqc_id: &StringIdentifier,
473    method_name_id: &StringIdentifier,
474) -> Option<&'a FunctionLikeMetadata> {
475    let method_id = MethodIdentifier::new(interner.lowered(fqc_id), interner.lowered(method_name_id));
476    let declaring_method_id = get_declaring_method_id(codebase, interner, &method_id);
477
478    get_method(codebase, interner, declaring_method_id.get_class_name(), declaring_method_id.get_method_name())
479}
480
481pub fn get_method_by_id<'a>(
482    codebase: &'a CodebaseMetadata,
483    interner: &ThreadedInterner,
484    method_id: &MethodIdentifier,
485) -> Option<&'a FunctionLikeMetadata> {
486    let lowered_fqc_id = interner.lowered(method_id.get_class_name());
487    let lowered_method_id = interner.lowered(method_id.get_method_name());
488
489    codebase.function_likes.get(&(lowered_fqc_id, lowered_method_id))
490}
491
492pub fn get_method<'a>(
493    codebase: &'a CodebaseMetadata,
494    interner: &ThreadedInterner,
495    fqc_id: &StringIdentifier,
496    method_name_id: &StringIdentifier,
497) -> Option<&'a FunctionLikeMetadata> {
498    let lowered_fqc_id = interner.lowered(fqc_id);
499    let lowered_method_id = interner.lowered(method_name_id);
500
501    codebase.function_likes.get(&(lowered_fqc_id, lowered_method_id))
502}
503
504/// Retrieves the metadata for a property that is declared directly on the given class-like.
505///
506/// This does not search the inheritance hierarchy.
507/// The lookup for the class-like name is case-insensitive, but the property name is case-sensitive.
508pub fn get_property<'a>(
509    codebase: &'a CodebaseMetadata,
510    interner: &ThreadedInterner,
511    fqc_id: &StringIdentifier,
512    property_id: &StringIdentifier,
513) -> Option<&'a PropertyMetadata> {
514    let lowered_fqc_id = interner.lowered(fqc_id);
515
516    let class_like = codebase.class_likes.get(&lowered_fqc_id)?;
517
518    class_like.properties.get(property_id)
519}
520
521/// An enum to represent either a class constant or an enum case.
522#[derive(Debug, PartialEq)]
523pub enum ClassConstantOrEnumCase<'a> {
524    Constant(&'a ClassLikeConstantMetadata),
525    EnumCase(&'a EnumCaseMetadata),
526}
527
528/// Retrieves the metadata for a class constant or an enum case from a class-like.
529///
530/// The lookup for the class-like name is case-insensitive, but the constant/case name is case-sensitive.
531pub fn get_class_like_constant_or_enum_case<'a>(
532    codebase: &'a CodebaseMetadata,
533    interner: &ThreadedInterner,
534    fqc_id: &StringIdentifier,
535    constant_id: &StringIdentifier,
536) -> Option<ClassConstantOrEnumCase<'a>> {
537    let lowered_fqc_id = interner.lowered(fqc_id);
538    let class_like = codebase.class_likes.get(&lowered_fqc_id)?;
539
540    if let Some(constant_meta) = class_like.constants.get(constant_id) {
541        return Some(ClassConstantOrEnumCase::Constant(constant_meta));
542    }
543
544    if let Some(enum_case_meta) = class_like.enum_cases.get(constant_id) {
545        return Some(ClassConstantOrEnumCase::EnumCase(enum_case_meta));
546    }
547
548    None
549}
550
551/// Checks if a class-like is an instance of another class-like.
552///
553/// This function checks if the `child` class-like is an instance of the `parent` class-like
554/// by looking up their metadata in the codebase.
555pub fn is_instance_of(
556    codebase: &CodebaseMetadata,
557    interner: &ThreadedInterner,
558    child: &StringIdentifier,
559    parent: &StringIdentifier,
560) -> bool {
561    let lowered_child = interner.lowered(child);
562    let lowered_parent = interner.lowered(parent);
563
564    if lowered_child == lowered_parent {
565        return true;
566    }
567
568    let Some(child_meta) = codebase.class_likes.get(&lowered_child) else {
569        return false;
570    };
571
572    child_meta.has_parent(&lowered_parent)
573}
574
575pub fn inherits_class(
576    codebase: &CodebaseMetadata,
577    interner: &ThreadedInterner,
578    child: &StringIdentifier,
579    parent: &StringIdentifier,
580) -> bool {
581    let lowered_child = interner.lowered(child);
582    let lowered_parent = interner.lowered(parent);
583
584    let Some(child_meta) = codebase.class_likes.get(&lowered_child) else {
585        return false;
586    };
587
588    child_meta.all_parent_classes.contains(&lowered_parent)
589}
590
591pub fn directly_inherits_class(
592    codebase: &CodebaseMetadata,
593    interner: &ThreadedInterner,
594    child: &StringIdentifier,
595    parent: &StringIdentifier,
596) -> bool {
597    let lowered_child = interner.lowered(child);
598    let lowered_parent = interner.lowered(parent);
599
600    let Some(child_meta) = codebase.class_likes.get(&lowered_child) else {
601        return false;
602    };
603
604    child_meta.direct_parent_class.as_ref().is_some_and(|parent_class| parent_class == &lowered_parent)
605}
606
607pub fn inherits_interface(
608    codebase: &CodebaseMetadata,
609    interner: &ThreadedInterner,
610    child: &StringIdentifier,
611    parent: &StringIdentifier,
612) -> bool {
613    let lowered_child = interner.lowered(child);
614    let lowered_parent = interner.lowered(parent);
615
616    let Some(child_meta) = codebase.class_likes.get(&lowered_child) else {
617        return false;
618    };
619
620    child_meta.all_parent_interfaces.contains(&lowered_parent)
621}
622
623pub fn directly_inherits_interface(
624    codebase: &CodebaseMetadata,
625    interner: &ThreadedInterner,
626    child: &StringIdentifier,
627    parent: &StringIdentifier,
628) -> bool {
629    let lowered_child = interner.lowered(child);
630    let lowered_parent = interner.lowered(parent);
631
632    let Some(child_meta) = codebase.class_likes.get(&lowered_child) else {
633        return false;
634    };
635
636    child_meta.direct_parent_interfaces.contains(&lowered_parent)
637}
638
639pub fn uses_trait(
640    codebase: &CodebaseMetadata,
641    interner: &ThreadedInterner,
642    child: &StringIdentifier,
643    trait_name: &StringIdentifier,
644) -> bool {
645    let lowered_child = interner.lowered(child);
646    let lowered_trait_name = interner.lowered(trait_name);
647
648    let Some(child_meta) = codebase.class_likes.get(&lowered_child) else {
649        return false;
650    };
651
652    child_meta.used_traits.contains(&lowered_trait_name)
653}
654
655/// Recursively collects all descendant class/interface/enum FQCNs for a given class-like structure.
656/// Uses the pre-computed `all_classlike_descendants` map if available, otherwise might be empty.
657/// Warning: Recursive; could stack overflow on extremely deep hierarchies if map isn't precomputed well.
658#[inline]
659pub fn get_all_descendants(
660    codebase: &CodebaseMetadata,
661    interner: &ThreadedInterner,
662    class_like_name: &StringIdentifier,
663) -> HashSet<StringIdentifier> {
664    let fqc_id = interner.lowered(class_like_name);
665
666    // This implementation assumes direct_classlike_descendants is populated correctly.
667    let mut all_descendants = HashSet::default();
668    let mut queue = vec![&fqc_id];
669    let mut visited = HashSet::default();
670    visited.insert(&fqc_id); // Don't include self in descendants
671
672    while let Some(current_name) = queue.pop() {
673        if let Some(direct_descendants) = codebase.direct_classlike_descendants.get(current_name) {
674            for descendant in direct_descendants {
675                if visited.insert(descendant) {
676                    // Add to results only if not visited before
677                    all_descendants.insert(*descendant);
678                    queue.push(descendant); // Add to queue for further exploration
679                }
680            }
681        }
682    }
683    all_descendants
684}
685
686/// Checks if a method is overridden from a parent class-like.
687///
688/// This function checks if the method with the given name in the specified class-like
689/// is overridden from a parent class-like by looking up the metadata in the codebase.
690///
691/// The lookup for both the class-like name and the method name is case-insensitive.
692pub fn is_method_overriding(
693    codebase: &CodebaseMetadata,
694    interner: &ThreadedInterner,
695    fqc_id: &StringIdentifier,
696    method_name: &StringIdentifier,
697) -> bool {
698    let lowered_method_name = interner.lowered(method_name);
699
700    get_class_like(codebase, interner, fqc_id)
701        .is_some_and(|metadata| metadata.overridden_method_ids.contains_key(&lowered_method_name))
702}
703
704/// Retrieves the type of a class constant, considering type hints and inferred types.
705/// Returns `None` if the class or constant doesn't exist, or type cannot be determined.
706#[inline]
707pub fn get_class_constant_type<'a>(
708    codebase: &'a CodebaseMetadata,
709    interner: &ThreadedInterner,
710    fq_class_name: &StringIdentifier,
711    constant_name: &StringIdentifier,
712) -> Option<Cow<'a, TUnion>> {
713    let class_metadata = get_class_like(codebase, interner, fq_class_name)?;
714
715    if class_metadata.kind.is_enum() && class_metadata.enum_cases.contains_key(constant_name) {
716        return Some(Cow::Owned(TUnion::new(vec![TAtomic::Object(TObject::new_enum_case(
717            class_metadata.original_name,
718            *constant_name,
719        ))])));
720    }
721
722    // It's a regular class constant
723    let constant_metadata = class_metadata.constants.get(constant_name)?;
724
725    // Prefer the type signature if available
726    if let Some(type_metadata) = constant_metadata.type_metadata.as_ref() {
727        // Return borrowed signature type directly
728        // (Original logic about boring scalars/is_this seemed complex and possibly specific
729        //  to a particular analysis stage; simplifying here to return declared type if present)
730        return Some(Cow::Borrowed(&type_metadata.type_union));
731    }
732
733    // Fall back to inferred type if no signature
734    constant_metadata.inferred_type.as_ref().map(|atomic_type| {
735        // Wrap the atomic type in a TUnion if returning inferred type
736        Cow::Owned(TUnion::new(vec![atomic_type.clone()]))
737    })
738}
739
740/// Lowers the namespace part of a fully qualified constant name while preserving the case of the constant name itself.
741///
742/// For example, `My\Namespace\MY_CONST` becomes `my\namespace\MY_CONST`. This is necessary because
743/// PHP constant lookups are case-insensitive for the namespace but case-sensitive for the final constant name.
744fn lower_constant_name(interner: &ThreadedInterner, name: &StringIdentifier) -> StringIdentifier {
745    let name_str = interner.lookup(name);
746    if !name_str.contains('\\') {
747        return *name;
748    }
749
750    let mut parts: Vec<_> = name_str.split('\\').map(str::to_owned).collect();
751    let total_parts = parts.len();
752    if total_parts > 1 {
753        parts = parts
754            .into_iter()
755            .enumerate()
756            .map(|(i, part)| if i < total_parts - 1 { part.to_ascii_lowercase() } else { part })
757            .collect::<Vec<_>>();
758    }
759
760    interner.intern(parts.join("\\"))
761}