Skip to main content

mago_analyzer/plugin/libraries/stdlib/reflection/
mod.rs

1//! Generic-reflection type providers.
2//!
3//! These providers make `Reflection*<T>` carry useful types derived from the
4//! reflected class `T`. Because `ReflectionClass::getMethods()` erases *which*
5//! method each element represents, the tightest sound approximation for a
6//! single `ReflectionMethod<T>` is "*some* method of `T`": `getName()` returns
7//! the union of `T`'s method names, and `invoke()` returns the union of `T`'s
8//! method return types.
9
10mod class_has_method;
11mod method_get_name;
12mod method_invoke;
13
14pub use class_has_method::ReflectionClassHasMethodAssertionProvider;
15use mago_codex::ttype::combiner::CombinerOptions;
16pub use method_get_name::ReflectionMethodGetNameProvider;
17pub use method_invoke::ReflectionMethodInvokeProvider;
18
19use mago_codex::metadata::CodebaseMetadata;
20use mago_codex::ttype::atomic::TAtomic;
21use mago_codex::ttype::atomic::object::TObject;
22use mago_codex::ttype::atomic::object::named::TNamedObject;
23use mago_codex::ttype::atomic::scalar::TScalar;
24use mago_codex::ttype::combine_union_types;
25use mago_codex::ttype::combiner;
26use mago_codex::ttype::expander::StaticClassType;
27use mago_codex::ttype::get_mixed;
28use mago_codex::ttype::union::TUnion;
29use mago_word::Word;
30
31use crate::plugin::context::InvocationInfo;
32use crate::plugin::context::ProviderContext;
33
34/// The receiver object of the current method call, if it is a single named
35/// object (e.g. the `ReflectionMethod<Foo>` in `$method->getName()`).
36fn receiver_named_object<'ctx>(invocation: &InvocationInfo<'ctx, '_, '_>) -> Option<&'ctx TNamedObject> {
37    let method_context = invocation.inner().target.get_method_context()?;
38
39    match &method_context.class_type {
40        StaticClassType::Object(TObject::Named(named)) => Some(named),
41        _ => None,
42    }
43}
44
45/// Resolve the reflected class `T` from the receiver of a `Reflection*<T>`
46/// call, but only when the receiver itself is an instance of
47/// `expected_receiver` (e.g. `ReflectionMethod`).
48///
49/// The gate is on the *receiver* class rather than the declaring class passed
50/// to the provider: inherited methods like `getName()` (declared on
51/// `ReflectionFunctionAbstract`) report the declaring class, so checking the
52/// receiver is what distinguishes a `ReflectionMethod` call.
53///
54/// Returns `None` when there is no instantiated, single named class to read -
55/// e.g. an un-parameterized receiver or the default `T of object` - so callers
56/// fall back to the declared return type rather than narrowing unsoundly.
57fn reflected_class_name(
58    context: &ProviderContext<'_, '_, '_>,
59    invocation: &InvocationInfo<'_, '_, '_>,
60    expected_receiver: &[u8],
61) -> Option<Word> {
62    let receiver = receiver_named_object(invocation)?;
63    if !context.is_instance_of(receiver.get_name().as_bytes(), expected_receiver) {
64        return None;
65    }
66
67    let parameter = receiver.get_type_parameters()?.first()?;
68
69    Some(parameter.get_single_named_object()?.get_name())
70}
71
72/// Build a union of literal-string atomics, one per method that appears on
73/// `class_name` (including inherited methods). `None` if the class is unknown
74/// or has no methods.
75fn method_name_union(codebase: &CodebaseMetadata, class_name: Word) -> Option<TUnion> {
76    let class_metadata = codebase.get_class_like(class_name.as_bytes())?;
77
78    let mut atomics = Vec::new();
79    for method_id in class_metadata.appearing_method_ids.values() {
80        if let Some(metadata) =
81            codebase.get_method(method_id.get_class_name().as_bytes(), method_id.get_method_name().as_bytes())
82        {
83            atomics.push(TAtomic::Scalar(TScalar::literal_string(metadata.original_name)));
84        }
85    }
86
87    if atomics.is_empty() {
88        return None;
89    }
90
91    Some(TUnion::from_vec(combiner::combine(atomics, codebase, CombinerOptions::default())))
92}
93
94/// Build a union of the return types of every method that appears on
95/// `class_name` (including inherited methods). Methods without a declared
96/// return type contribute `mixed`. `None` if the class is unknown or has no
97/// methods.
98fn method_return_union(codebase: &CodebaseMetadata, class_name: Word) -> Option<TUnion> {
99    let class_metadata = codebase.get_class_like(class_name.as_bytes())?;
100
101    let mut result: Option<TUnion> = None;
102    for method_id in class_metadata.appearing_method_ids.values() {
103        let Some(metadata) =
104            codebase.get_method(method_id.get_class_name().as_bytes(), method_id.get_method_name().as_bytes())
105        else {
106            continue;
107        };
108
109        let return_type =
110            metadata.return_type_metadata.as_ref().map_or_else(get_mixed, |metadata| metadata.type_union.clone());
111
112        result = Some(match result {
113            None => return_type,
114            Some(accumulated) => combine_union_types(&accumulated, &return_type, codebase, CombinerOptions::default()),
115        });
116    }
117
118    result
119}