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::metadata::ttype::TypeMetadata;
20use crate::symbol::SymbolKind;
21use crate::ttype::atomic::TAtomic;
22use crate::ttype::atomic::object::TObject;
23use crate::ttype::union::TUnion;
24
25pub mod assertion;
26pub mod consts;
27pub mod context;
28pub mod diff;
29pub mod flags;
30pub mod identifier;
31pub mod issue;
32pub mod metadata;
33pub mod misc;
34pub mod populator;
35pub mod reference;
36pub mod scanner;
37pub mod symbol;
38pub mod ttype;
39pub mod visibility;
40
41mod utils;
42
43/// Checks if a global function exists in the codebase.
44///
45/// This lookup is case-insensitive, in line with PHP's behavior for function names.
46pub fn function_exists(codebase: &CodebaseMetadata, interner: &ThreadedInterner, id: &StringIdentifier) -> bool {
47    let lowered_id = interner.lowered(id);
48    codebase.function_likes.contains_key(&(StringIdentifier::empty(), lowered_id))
49}
50
51/// Checks if a global constant exists in the codebase.
52///
53/// The lookup for the namespace part of the constant name is case-insensitive,
54/// but the constant name itself is case-sensitive, matching PHP's behavior.
55pub fn constant_exists(codebase: &CodebaseMetadata, interner: &ThreadedInterner, id: &StringIdentifier) -> bool {
56    let lowered_id = lower_constant_name(interner, id);
57    codebase.constants.contains_key(&lowered_id)
58}
59
60/// Checks if a class exists in the codebase.
61///
62/// This lookup is case-insensitive, in line with PHP's behavior for class names.
63pub fn class_exists(codebase: &CodebaseMetadata, interner: &ThreadedInterner, id: &StringIdentifier) -> bool {
64    let lowered_id = interner.lowered(id);
65
66    matches!(codebase.symbols.get_kind(&lowered_id), Some(SymbolKind::Class))
67}
68
69/// Checks if a class or trait exists in the codebase.
70///
71/// This lookup is case-insensitive, in line with PHP's behavior for class names.
72pub fn class_or_trait_exists(codebase: &CodebaseMetadata, interner: &ThreadedInterner, id: &StringIdentifier) -> bool {
73    let lowered_id = interner.lowered(id);
74
75    matches!(codebase.symbols.get_kind(&lowered_id), Some(SymbolKind::Class | SymbolKind::Trait))
76}
77
78/// Checks if an interface exists in the codebase.
79///
80/// This lookup is case-insensitive, in line with PHP's behavior for interface names.
81pub fn interface_exists(codebase: &CodebaseMetadata, interner: &ThreadedInterner, id: &StringIdentifier) -> bool {
82    let lowered_id = interner.lowered(id);
83
84    matches!(codebase.symbols.get_kind(&lowered_id), Some(SymbolKind::Interface))
85}
86
87/// Checks if a class or interface exists in the codebase.
88///
89/// This lookup is case-insensitive, in line with PHP's behavior for class names.
90pub fn class_or_interface_exists(
91    codebase: &CodebaseMetadata,
92    interner: &ThreadedInterner,
93    id: &StringIdentifier,
94) -> bool {
95    let lowered_id = interner.lowered(id);
96
97    matches!(codebase.symbols.get_kind(&lowered_id), Some(SymbolKind::Class | SymbolKind::Interface))
98}
99
100/// Checks if an enum exists in the codebase.
101///
102/// This lookup is case-insensitive, in line with PHP's behavior for enum names.
103pub fn enum_exists(codebase: &CodebaseMetadata, interner: &ThreadedInterner, id: &StringIdentifier) -> bool {
104    let lowered_id = interner.lowered(id);
105
106    matches!(codebase.symbols.get_kind(&lowered_id), Some(SymbolKind::Enum))
107}
108
109/// Checks if a trait exists in the codebase.
110///
111/// This lookup is case-insensitive, in line with PHP's behavior for trait names.
112pub fn trait_exists(codebase: &CodebaseMetadata, interner: &ThreadedInterner, id: &StringIdentifier) -> bool {
113    let lowered_id = interner.lowered(id);
114
115    matches!(codebase.symbols.get_kind(&lowered_id), Some(SymbolKind::Trait))
116}
117
118/// Checks if a class-like (class, interface, enum, or trait) exists in the codebase.
119///
120/// This lookup is case-insensitive.
121pub fn class_like_exists(codebase: &CodebaseMetadata, interner: &ThreadedInterner, id: &StringIdentifier) -> bool {
122    let lowered_id = interner.lowered(id);
123
124    matches!(
125        codebase.symbols.get_kind(&lowered_id),
126        Some(SymbolKind::Class | SymbolKind::Interface | SymbolKind::Enum | SymbolKind::Trait)
127    )
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(
134    codebase: &CodebaseMetadata,
135    interner: &ThreadedInterner,
136    fqc_id: &StringIdentifier,
137    method_id: &StringIdentifier,
138) -> bool {
139    let lowered_fqc_id = interner.lowered(fqc_id);
140    let lowered_method_id = interner.lowered(method_id);
141
142    codebase
143        .class_likes
144        .get(&lowered_fqc_id)
145        .is_some_and(|meta| meta.appearing_method_ids.contains_key(&lowered_method_id))
146}
147
148pub fn method_id_exists(
149    codebase: &CodebaseMetadata,
150    interner: &ThreadedInterner,
151    method_id: &MethodIdentifier,
152) -> bool {
153    let lowered_fqc_id = interner.lowered(method_id.get_class_name());
154    let lowered_method_id = interner.lowered(method_id.get_method_name());
155
156    codebase.function_likes.contains_key(&(lowered_fqc_id, lowered_method_id))
157}
158
159pub fn is_method_abstract(
160    codebase: &CodebaseMetadata,
161    interner: &ThreadedInterner,
162    fqc_id: &StringIdentifier,
163    method_id: &StringIdentifier,
164) -> bool {
165    let lowered_fqc_id = interner.lowered(fqc_id);
166    let lowered_method_id = interner.lowered(method_id);
167
168    codebase
169        .function_likes
170        .get(&(lowered_fqc_id, lowered_method_id))
171        .and_then(|meta| meta.method_metadata.as_ref())
172        .is_some_and(|method| method.is_abstract)
173}
174
175pub fn is_method_static(
176    codebase: &CodebaseMetadata,
177    interner: &ThreadedInterner,
178    fqc_id: &StringIdentifier,
179    method_id: &StringIdentifier,
180) -> bool {
181    let lowered_fqc_id = interner.lowered(fqc_id);
182    let lowered_method_id = interner.lowered(method_id);
183
184    codebase
185        .function_likes
186        .get(&(lowered_fqc_id, lowered_method_id))
187        .and_then(|meta| meta.method_metadata.as_ref())
188        .is_some_and(|method| method.is_static)
189}
190
191pub fn is_method_final(
192    codebase: &CodebaseMetadata,
193    interner: &ThreadedInterner,
194    fqc_id: &StringIdentifier,
195    method_id: &StringIdentifier,
196) -> bool {
197    let lowered_fqc_id = interner.lowered(fqc_id);
198    let lowered_method_id = interner.lowered(method_id);
199
200    codebase
201        .function_likes
202        .get(&(lowered_fqc_id, lowered_method_id))
203        .and_then(|meta| meta.method_metadata.as_ref())
204        .is_some_and(|method| method.is_final)
205}
206
207/// Checks if a property exists on a given class-like (including inherited properties).
208///
209/// The lookup for the class-like name is case-insensitive, but the property name is case-sensitive.
210pub fn property_exists(
211    codebase: &CodebaseMetadata,
212    interner: &ThreadedInterner,
213    fqc_id: &StringIdentifier,
214    property_id: &StringIdentifier,
215) -> bool {
216    let lowered_fqc_id = interner.lowered(fqc_id);
217
218    codebase.class_likes.get(&lowered_fqc_id).is_some_and(|meta| meta.appearing_property_ids.contains_key(property_id))
219}
220
221/// Checks if a method is declared directly on a given class-like (not inherited).
222///
223/// The lookup for both the class-like name and the method name is case-insensitive.
224pub fn declaring_method_exists(
225    codebase: &CodebaseMetadata,
226    interner: &ThreadedInterner,
227    fqc_id: &StringIdentifier,
228    method_id: &StringIdentifier,
229) -> bool {
230    let lowered_fqc_id = interner.lowered(fqc_id);
231    let lowered_method_id = interner.lowered(method_id);
232
233    codebase
234        .class_likes
235        .get(&lowered_fqc_id)
236        .is_some_and(|meta| meta.declaring_method_ids.contains_key(&lowered_method_id))
237}
238
239/// Checks if a property is declared directly on a given class-like (not inherited).
240///
241/// The lookup for the class-like name is case-insensitive, but the property name is case-sensitive.
242pub fn declaring_property_exists(
243    codebase: &CodebaseMetadata,
244    interner: &ThreadedInterner,
245    fqc_id: &StringIdentifier,
246    property_id: &StringIdentifier,
247) -> bool {
248    let lowered_fqc_id = interner.lowered(fqc_id);
249
250    codebase.class_likes.get(&lowered_fqc_id).is_some_and(|meta| meta.properties.contains_key(property_id))
251}
252
253/// Checks if a constant or enum case exists on a given class-like.
254///
255/// The lookup for the class-like name is case-insensitive, but the constant/case name is case-sensitive.
256pub fn class_like_constant_or_enum_case_exists(
257    codebase: &CodebaseMetadata,
258    interner: &ThreadedInterner,
259    fqc_id: &StringIdentifier,
260    constant_id: &StringIdentifier,
261) -> bool {
262    let lowered_fqc_id = interner.lowered(fqc_id);
263
264    if let Some(meta) = codebase.class_likes.get(&lowered_fqc_id) {
265        return meta.constants.contains_key(constant_id) || meta.enum_cases.contains_key(constant_id);
266    }
267
268    false
269}
270
271/// Retrieves the metadata for a global function.
272///
273/// This lookup is case-insensitive.
274pub fn get_function<'a>(
275    codebase: &'a CodebaseMetadata,
276    interner: &ThreadedInterner,
277    id: &StringIdentifier,
278) -> Option<&'a FunctionLikeMetadata> {
279    let lowered_id = interner.lowered(id);
280
281    codebase.function_likes.get(&(StringIdentifier::empty(), lowered_id))
282}
283
284/// Retrieves the metadata for a closure based on its position in the source code.
285///
286/// This function uses the source ID and the closure's position to uniquely identify it.
287pub fn get_closure<'a>(
288    codebase: &'a CodebaseMetadata,
289    interner: &ThreadedInterner,
290    file_id: &FileId,
291    position: &Position,
292) -> Option<&'a FunctionLikeMetadata> {
293    let file_id = interner.intern(file_id.to_string());
294    let closure_id = interner.intern(position.to_string());
295
296    codebase.function_likes.get(&(file_id, closure_id))
297}
298
299/// Retrieves the metadata for a global constant.
300///
301/// The namespace lookup is case-insensitive, but the constant name itself is case-sensitive.
302pub fn get_constant<'a>(
303    codebase: &'a CodebaseMetadata,
304    interner: &ThreadedInterner,
305    id: &StringIdentifier,
306) -> Option<&'a ConstantMetadata> {
307    let lowered_id = lower_constant_name(interner, id);
308
309    codebase.constants.get(&lowered_id)
310}
311
312/// Retrieves the metadata for a class.
313///
314/// This lookup is case-insensitive.
315pub fn get_class<'a>(
316    codebase: &'a CodebaseMetadata,
317    interner: &ThreadedInterner,
318    id: &StringIdentifier,
319) -> Option<&'a ClassLikeMetadata> {
320    let lowered_id = interner.lowered(id);
321
322    if class_exists(codebase, interner, id) { codebase.class_likes.get(&lowered_id) } else { None }
323}
324
325/// Retrieves the metadata for an interface.
326///
327/// This lookup is case-insensitive.
328pub fn get_interface<'a>(
329    codebase: &'a CodebaseMetadata,
330    interner: &ThreadedInterner,
331    id: &StringIdentifier,
332) -> Option<&'a ClassLikeMetadata> {
333    let lowered_id = interner.lowered(id);
334
335    if interface_exists(codebase, interner, id) { codebase.class_likes.get(&lowered_id) } else { None }
336}
337
338/// Retrieves the metadata for an enum.
339///
340/// This lookup is case-insensitive.
341pub fn get_enum<'a>(
342    codebase: &'a CodebaseMetadata,
343    interner: &ThreadedInterner,
344    id: &StringIdentifier,
345) -> Option<&'a ClassLikeMetadata> {
346    let lowered_id = interner.lowered(id);
347
348    if enum_exists(codebase, interner, id) { codebase.class_likes.get(&lowered_id) } else { None }
349}
350
351/// Retrieves the metadata for a trait.
352///
353/// This lookup is case-insensitive.
354pub fn get_trait<'a>(
355    codebase: &'a CodebaseMetadata,
356    interner: &ThreadedInterner,
357    id: &StringIdentifier,
358) -> Option<&'a ClassLikeMetadata> {
359    let lowered_id = interner.lowered(id);
360
361    if trait_exists(codebase, interner, id) { codebase.class_likes.get(&lowered_id) } else { None }
362}
363
364pub fn get_anonymous_class_name(interner: &ThreadedInterner, span: Span) -> StringIdentifier {
365    interner.intern(format!("class@anonymous:{}-{}:{}", span.file_id, span.start.offset, span.end.offset,))
366}
367
368/// Retrieves the metadata for an anonymous class based on its span.
369///
370/// This function generates a unique name for the anonymous class based on its span,
371/// which includes the source file and the start and end offsets.
372pub fn get_anonymous_class<'a>(
373    codebase: &'a CodebaseMetadata,
374    interner: &ThreadedInterner,
375    span: Span,
376) -> Option<&'a ClassLikeMetadata> {
377    let name = get_anonymous_class_name(interner, span);
378
379    if class_exists(codebase, interner, &name) { codebase.class_likes.get(&name) } else { None }
380}
381
382/// Retrieves the metadata for any class-like (class, interface, enum, or trait).
383///
384/// This lookup is case-insensitive.
385pub fn get_class_like<'a>(
386    codebase: &'a CodebaseMetadata,
387    interner: &ThreadedInterner,
388    id: &StringIdentifier,
389) -> Option<&'a ClassLikeMetadata> {
390    let lowered_id = interner.lowered(id);
391    codebase.class_likes.get(&lowered_id)
392}
393
394pub fn get_declaring_class_for_property(
395    codebase: &CodebaseMetadata,
396    interner: &ThreadedInterner,
397    fqc_id: &StringIdentifier,
398    property_id: &StringIdentifier,
399) -> Option<StringIdentifier> {
400    let lowered_fqc_id = interner.lowered(fqc_id);
401
402    let class_like = codebase.class_likes.get(&lowered_fqc_id)?;
403
404    class_like.declaring_property_ids.get(property_id).copied()
405}
406
407/// Retrieves the metadata for a property, searching the inheritance hierarchy.
408///
409/// This function finds where the property was originally declared and returns its metadata.
410/// The lookup for the class-like name is case-insensitive, but the property name is case-sensitive.
411pub fn get_declaring_property<'a>(
412    codebase: &'a CodebaseMetadata,
413    interner: &ThreadedInterner,
414    fqc_id: &StringIdentifier,
415    property_id: &StringIdentifier,
416) -> Option<&'a PropertyMetadata> {
417    let declaring_fqc_id = get_declaring_class_for_property(codebase, interner, fqc_id, property_id)?;
418    let declaring_class_like = codebase.class_likes.get(&declaring_fqc_id)?;
419
420    declaring_class_like.properties.get(property_id)
421}
422
423pub fn get_method_id(fqc_id: &StringIdentifier, method_name_id: &StringIdentifier) -> MethodIdentifier {
424    MethodIdentifier::new(*fqc_id, *method_name_id)
425}
426
427pub fn get_declaring_method_id(
428    codebase: &CodebaseMetadata,
429    interner: &ThreadedInterner,
430    method_id: &MethodIdentifier,
431) -> MethodIdentifier {
432    let lowered_fqc_id = interner.lowered(method_id.get_class_name());
433    let lowered_method_id = interner.lowered(method_id.get_method_name());
434
435    let Some(class_like_metadata) = codebase.class_likes.get(&lowered_fqc_id) else {
436        // If the class-like doesn't exist, return the method ID as is
437        return *method_id;
438    };
439
440    if let Some(declaring_fqcn) = class_like_metadata.declaring_method_ids.get(&lowered_method_id)
441        && let Some(declaring_class_metadata) = codebase.class_likes.get(declaring_fqcn)
442    {
443        return MethodIdentifier::new(declaring_class_metadata.original_name, *method_id.get_method_name());
444    };
445
446    if class_like_metadata.flags.is_abstract()
447        && let Some(overridden_classes) = class_like_metadata.overridden_method_ids.get(&lowered_method_id)
448        && let Some(first_class) = overridden_classes.iter().next()
449        && let Some(first_class_metadata) = codebase.class_likes.get(first_class)
450    {
451        return MethodIdentifier::new(first_class_metadata.original_name, *method_id.get_method_name());
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
699pub fn get_function_like_thrown_types<'a>(
700    codebase: &'a CodebaseMetadata,
701    class_like: Option<&'a ClassLikeMetadata>,
702    function_like: &'a FunctionLikeMetadata,
703) -> &'a [TypeMetadata] {
704    if !function_like.thrown_types.is_empty() {
705        return function_like.thrown_types.as_slice();
706    }
707
708    if !function_like.kind.is_method() {
709        return &[];
710    }
711
712    let Some(class_like) = class_like else {
713        return &[];
714    };
715
716    let Some(method_name) = function_like.name.as_ref() else {
717        return &[];
718    };
719
720    for parent_class_name_id in class_like.overridden_method_ids.get(method_name).into_iter().flatten() {
721        let Some(parent_class) = codebase.class_likes.get(parent_class_name_id) else {
722            continue;
723        };
724
725        let parent_method_id = (*parent_class_name_id, *method_name);
726        if let Some(parent_method) = codebase.function_likes.get(&parent_method_id) {
727            let thrown = get_function_like_thrown_types(codebase, Some(parent_class), parent_method);
728            if !thrown.is_empty() {
729                return thrown;
730            }
731        }
732    }
733
734    &[]
735}
736
737/// Retrieves the type of a class constant, considering type hints and inferred types.
738/// Returns `None` if the class or constant doesn't exist, or type cannot be determined.
739#[inline]
740pub fn get_class_constant_type<'a>(
741    codebase: &'a CodebaseMetadata,
742    interner: &ThreadedInterner,
743    fq_class_name: &StringIdentifier,
744    constant_name: &StringIdentifier,
745) -> Option<Cow<'a, TUnion>> {
746    let class_metadata = get_class_like(codebase, interner, fq_class_name)?;
747
748    if class_metadata.kind.is_enum() && class_metadata.enum_cases.contains_key(constant_name) {
749        return Some(Cow::Owned(TUnion::new(vec![TAtomic::Object(TObject::new_enum_case(
750            class_metadata.original_name,
751            *constant_name,
752        ))])));
753    }
754
755    // It's a regular class constant
756    let constant_metadata = class_metadata.constants.get(constant_name)?;
757
758    // Prefer the type signature if available
759    if let Some(type_metadata) = constant_metadata.type_metadata.as_ref() {
760        // Return borrowed signature type directly
761        // (Original logic about boring scalars/is_this seemed complex and possibly specific
762        //  to a particular analysis stage; simplifying here to return declared type if present)
763        return Some(Cow::Borrowed(&type_metadata.type_union));
764    }
765
766    // Fall back to inferred type if no signature
767    constant_metadata.inferred_type.as_ref().map(|atomic_type| {
768        // Wrap the atomic type in a TUnion if returning inferred type
769        Cow::Owned(TUnion::new(vec![atomic_type.clone()]))
770    })
771}
772
773/// Lowers the namespace part of a fully qualified constant name while preserving the case of the constant name itself.
774///
775/// For example, `My\Namespace\MY_CONST` becomes `my\namespace\MY_CONST`. This is necessary because
776/// PHP constant lookups are case-insensitive for the namespace but case-sensitive for the final constant name.
777fn lower_constant_name(interner: &ThreadedInterner, name: &StringIdentifier) -> StringIdentifier {
778    let name_str = interner.lookup(name);
779    if !name_str.contains('\\') {
780        return *name;
781    }
782
783    let mut parts: Vec<_> = name_str.split('\\').map(str::to_owned).collect();
784    let total_parts = parts.len();
785    if total_parts > 1 {
786        parts = parts
787            .into_iter()
788            .enumerate()
789            .map(|(i, part)| if i < total_parts - 1 { part.to_ascii_lowercase() } else { part })
790            .collect::<Vec<_>>();
791    }
792
793    interner.intern(parts.join("\\"))
794}