mago_codex/ttype/
expander.rs

1use std::borrow::Cow;
2
3use mago_atom::Atom;
4use mago_atom::ascii_lowercase_atom;
5
6use crate::get_class_like;
7use crate::get_closure;
8use crate::get_declaring_method;
9use crate::get_function;
10use crate::identifier::function_like::FunctionLikeIdentifier;
11use crate::metadata::CodebaseMetadata;
12use crate::metadata::function_like::FunctionLikeMetadata;
13use crate::ttype::TType;
14use crate::ttype::atomic::TAtomic;
15use crate::ttype::atomic::array::TArray;
16use crate::ttype::atomic::callable::TCallable;
17use crate::ttype::atomic::callable::TCallableSignature;
18use crate::ttype::atomic::callable::parameter::TCallableParameter;
19use crate::ttype::atomic::derived::TDerived;
20use crate::ttype::atomic::derived::key_of::TKeyOf;
21use crate::ttype::atomic::derived::value_of::TValueOf;
22use crate::ttype::atomic::mixed::TMixed;
23use crate::ttype::atomic::object::TObject;
24use crate::ttype::atomic::object::named::TNamedObject;
25use crate::ttype::atomic::reference::TReference;
26use crate::ttype::atomic::reference::TReferenceMemberSelector;
27use crate::ttype::atomic::scalar::TScalar;
28use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
29use crate::ttype::combiner;
30use crate::ttype::get_mixed;
31use crate::ttype::union::TUnion;
32
33#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
34pub enum StaticClassType {
35    #[default]
36    None,
37    Name(Atom),
38    Object(TObject),
39}
40
41#[derive(Debug)]
42pub struct TypeExpansionOptions {
43    pub self_class: Option<Atom>,
44    pub static_class_type: StaticClassType,
45    pub parent_class: Option<Atom>,
46    pub evaluate_class_constants: bool,
47    pub evaluate_conditional_types: bool,
48    pub function_is_final: bool,
49    pub expand_generic: bool,
50    pub expand_templates: bool,
51}
52
53impl Default for TypeExpansionOptions {
54    fn default() -> Self {
55        Self {
56            self_class: None,
57            static_class_type: StaticClassType::default(),
58            parent_class: None,
59            evaluate_class_constants: true,
60            evaluate_conditional_types: false,
61            function_is_final: false,
62            expand_generic: false,
63            expand_templates: true,
64        }
65    }
66}
67
68pub fn expand_union(codebase: &CodebaseMetadata, return_type: &mut TUnion, options: &TypeExpansionOptions) {
69    if !return_type.is_expandable() {
70        return;
71    }
72
73    let mut types = std::mem::take(&mut return_type.types).into_owned();
74
75    types = combiner::combine(types, codebase, false);
76
77    let mut new_return_type_parts = vec![];
78    let mut skipped_keys = vec![];
79
80    for (i, return_type_part) in types.iter_mut().enumerate() {
81        let mut skip_key = false;
82        expand_atomic(return_type_part, codebase, options, &mut skip_key, &mut new_return_type_parts);
83
84        if skip_key {
85            skipped_keys.push(i);
86        }
87    }
88
89    if !skipped_keys.is_empty() {
90        let mut i = 0;
91        types.retain(|_| {
92            let to_retain = !skipped_keys.contains(&i);
93            i += 1;
94            to_retain
95        });
96
97        new_return_type_parts.append(&mut types);
98
99        if new_return_type_parts.is_empty() {
100            new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
101        }
102
103        types = if new_return_type_parts.len() > 1 {
104            combiner::combine(new_return_type_parts, codebase, false)
105        } else {
106            new_return_type_parts
107        };
108    }
109
110    return_type.types = Cow::Owned(types);
111}
112
113pub(crate) fn expand_atomic(
114    return_type_part: &mut TAtomic,
115    codebase: &CodebaseMetadata,
116    options: &TypeExpansionOptions,
117    skip_key: &mut bool,
118    new_return_type_parts: &mut Vec<TAtomic>,
119) {
120    match return_type_part {
121        TAtomic::Array(array_type) => match array_type {
122            TArray::Keyed(keyed_data) => {
123                if let Some((key_parameter, value_parameter)) = &mut keyed_data.parameters {
124                    expand_union(codebase, key_parameter, options);
125                    expand_union(codebase, value_parameter, options);
126                }
127
128                if let Some(known_items) = &mut keyed_data.known_items {
129                    for (_, item_type) in known_items.values_mut() {
130                        expand_union(codebase, item_type, options);
131                    }
132                }
133            }
134            TArray::List(list_data) => {
135                expand_union(codebase, &mut list_data.element_type, options);
136
137                if let Some(known_elements) = &mut list_data.known_elements {
138                    for (_, element_type) in known_elements.values_mut() {
139                        expand_union(codebase, element_type, options);
140                    }
141                }
142            }
143        },
144        TAtomic::Object(TObject::Named(named_object)) => {
145            expand_named_object(named_object, codebase, options);
146        }
147        TAtomic::Callable(TCallable::Signature(signature)) => {
148            if let Some(return_type) = signature.get_return_type_mut() {
149                expand_union(codebase, return_type, options);
150            }
151
152            for param in signature.get_parameters_mut() {
153                if let Some(param_type) = param.get_type_signature_mut() {
154                    expand_union(codebase, param_type, options);
155                }
156            }
157        }
158        TAtomic::GenericParameter(parameter) => {
159            expand_union(codebase, parameter.constraint.as_mut(), options);
160        }
161        TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) => {
162            let mut atomic_return_type_parts = vec![];
163            expand_atomic(constraint, codebase, options, &mut false, &mut atomic_return_type_parts);
164
165            if !atomic_return_type_parts.is_empty() {
166                *constraint = Box::new(atomic_return_type_parts.remove(0));
167            }
168        }
169        TAtomic::Reference(TReference::Member { class_like_name, member_selector }) => {
170            *skip_key = true;
171
172            match member_selector {
173                TReferenceMemberSelector::Wildcard => {
174                    let Some(class_like) = get_class_like(codebase, class_like_name) else {
175                        new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
176
177                        return;
178                    };
179
180                    for constant in class_like.constants.values() {
181                        if let Some(inferred_type) = constant.inferred_type.as_ref() {
182                            let mut inferred_type = inferred_type.clone();
183
184                            let mut skip_inferred_type = false;
185                            expand_atomic(
186                                &mut inferred_type,
187                                codebase,
188                                options,
189                                &mut skip_inferred_type,
190                                new_return_type_parts,
191                            );
192
193                            if !skip_inferred_type {
194                                new_return_type_parts.push(inferred_type);
195                            }
196                        } else if let Some(type_metadata) = constant.type_metadata.as_ref() {
197                            let mut constant_type = type_metadata.type_union.clone();
198
199                            expand_union(codebase, &mut constant_type, options);
200
201                            new_return_type_parts.extend(constant_type.types.into_owned());
202                        } else {
203                            new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
204                        }
205                    }
206
207                    for enum_case_name in class_like.enum_cases.keys() {
208                        new_return_type_parts
209                            .push(TAtomic::Object(TObject::new_enum_case(class_like.original_name, *enum_case_name)));
210                    }
211                }
212                TReferenceMemberSelector::StartsWith(prefix) => {
213                    let Some(class_like) = get_class_like(codebase, class_like_name) else {
214                        new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
215
216                        return;
217                    };
218
219                    for (constant_name, constant) in class_like.constants.iter() {
220                        if !constant_name.starts_with(prefix.as_str()) {
221                            continue;
222                        }
223
224                        if let Some(inferred_type) = constant.inferred_type.as_ref() {
225                            let mut inferred_type = inferred_type.clone();
226
227                            let mut skip_inferred_type = false;
228                            expand_atomic(
229                                &mut inferred_type,
230                                codebase,
231                                options,
232                                &mut skip_inferred_type,
233                                new_return_type_parts,
234                            );
235
236                            if !skip_inferred_type {
237                                new_return_type_parts.push(inferred_type);
238                            }
239                        } else if let Some(type_metadata) = constant.type_metadata.as_ref() {
240                            let mut constant_type = type_metadata.type_union.clone();
241
242                            expand_union(codebase, &mut constant_type, options);
243
244                            new_return_type_parts.extend(constant_type.types.into_owned());
245                        } else {
246                            new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
247                        }
248                    }
249
250                    for enum_case_name in class_like.enum_cases.keys() {
251                        if !enum_case_name.starts_with(prefix.as_str()) {
252                            continue;
253                        }
254
255                        new_return_type_parts
256                            .push(TAtomic::Object(TObject::new_enum_case(class_like.original_name, *enum_case_name)));
257                    }
258                }
259                TReferenceMemberSelector::EndsWith(suffix) => {
260                    let Some(class_like) = get_class_like(codebase, class_like_name) else {
261                        new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
262
263                        return;
264                    };
265
266                    for (constant_name, constant) in class_like.constants.iter() {
267                        if !constant_name.ends_with(suffix.as_str()) {
268                            continue;
269                        }
270
271                        if let Some(inferred_type) = constant.inferred_type.as_ref() {
272                            let mut inferred_type = inferred_type.clone();
273
274                            let mut skip_inferred_type = false;
275                            expand_atomic(
276                                &mut inferred_type,
277                                codebase,
278                                options,
279                                &mut skip_inferred_type,
280                                new_return_type_parts,
281                            );
282
283                            if !skip_inferred_type {
284                                new_return_type_parts.push(inferred_type);
285                            }
286                        } else if let Some(type_metadata) = constant.type_metadata.as_ref() {
287                            let mut constant_type = type_metadata.type_union.clone();
288
289                            expand_union(codebase, &mut constant_type, options);
290
291                            new_return_type_parts.extend(constant_type.types.into_owned());
292                        } else {
293                            new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
294                        }
295                    }
296
297                    for enum_case_name in class_like.enum_cases.keys() {
298                        if !enum_case_name.ends_with(suffix.as_str()) {
299                            continue;
300                        }
301
302                        new_return_type_parts
303                            .push(TAtomic::Object(TObject::new_enum_case(class_like.original_name, *enum_case_name)));
304                    }
305                }
306                TReferenceMemberSelector::Identifier(member_name) => {
307                    let Some(class_like) = get_class_like(codebase, class_like_name) else {
308                        new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
309
310                        return;
311                    };
312
313                    if class_like.enum_cases.contains_key(member_name) {
314                        new_return_type_parts
315                            .push(TAtomic::Object(TObject::new_enum_case(class_like.original_name, *member_name)));
316                    } else if let Some(constant) = class_like.constants.get(member_name) {
317                        if let Some(inferred_type) = constant.inferred_type.as_ref() {
318                            let mut inferred_type = inferred_type.clone();
319
320                            let mut skip_inferred_type = false;
321                            expand_atomic(
322                                &mut inferred_type,
323                                codebase,
324                                options,
325                                &mut skip_inferred_type,
326                                new_return_type_parts,
327                            );
328
329                            if !skip_inferred_type {
330                                new_return_type_parts.push(inferred_type);
331                            }
332                        } else if let Some(type_metadata) = constant.type_metadata.as_ref() {
333                            let mut constant_type = type_metadata.type_union.clone();
334
335                            expand_union(codebase, &mut constant_type, options);
336
337                            new_return_type_parts.extend(constant_type.types.into_owned());
338                        } else {
339                            new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
340                        }
341                    } else {
342                        new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
343                    }
344                }
345            }
346        }
347        TAtomic::Callable(TCallable::Alias(id)) => {
348            if let Some(value) = get_atomic_of_function_like_identifier(id, codebase) {
349                *skip_key = true;
350                new_return_type_parts.push(value);
351            }
352        }
353        TAtomic::Conditional(conditional) => {
354            *skip_key = true;
355
356            let mut then = conditional.then.clone();
357            let mut otherwise = conditional.otherwise.clone();
358
359            expand_union(codebase, &mut then, options);
360            expand_union(codebase, &mut otherwise, options);
361
362            new_return_type_parts.extend(then.types.into_owned());
363            new_return_type_parts.extend(otherwise.types.into_owned());
364        }
365        TAtomic::Derived(derived) => match derived {
366            TDerived::KeyOf(key_of) => {
367                *skip_key = true;
368                new_return_type_parts.extend(expand_key_of(key_of, codebase, options));
369            }
370            TDerived::ValueOf(value_of) => {
371                *skip_key = true;
372                new_return_type_parts.extend(expand_value_of(value_of, codebase, options));
373            }
374            TDerived::PropertiesOf(_) => todo!("expand_properties_of"),
375        },
376        _ => {}
377    }
378}
379
380fn expand_named_object(named_object: &mut TNamedObject, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) {
381    let name_str_lc = ascii_lowercase_atom(named_object.name.as_str());
382
383    if named_object.is_this() || name_str_lc == "static" || name_str_lc == "$this" {
384        match &options.static_class_type {
385            StaticClassType::Object(TObject::Named(static_object)) => {
386                if let Some(static_object_intersections) = &static_object.intersection_types {
387                    let intersections = named_object.intersection_types.get_or_insert_with(Vec::new);
388                    intersections.extend(static_object_intersections.iter().cloned());
389                }
390
391                if named_object.type_parameters.is_none() {
392                    named_object.type_parameters = static_object.type_parameters.clone();
393                }
394
395                named_object.name = static_object.name;
396                named_object.is_this = true;
397            }
398            StaticClassType::Name(static_class_name) => {
399                named_object.name = *static_class_name;
400                named_object.is_this = options.function_is_final;
401            }
402            _ => {}
403        }
404    } else if name_str_lc == "self" {
405        if let Some(self_class_name) = options.self_class {
406            named_object.name = self_class_name;
407        }
408    } else if name_str_lc == "parent"
409        && let Some(self_class_name) = options.self_class
410        && let Some(class_metadata) = get_class_like(codebase, &self_class_name)
411        && let Some(parent_name) = class_metadata.direct_parent_class
412    {
413        named_object.name = parent_name;
414    }
415
416    if named_object.type_parameters.is_none()
417        && let Some(class_like_metadata) = get_class_like(codebase, &named_object.name)
418        && !class_like_metadata.template_types.is_empty()
419    {
420        let default_params: Vec<TUnion> = class_like_metadata
421            .template_types
422            .iter()
423            .map(|(_, template_map)| template_map.iter().map(|(_, t)| t).next().cloned().unwrap_or_else(get_mixed))
424            .collect();
425
426        if !default_params.is_empty() {
427            named_object.type_parameters = Some(default_params);
428        }
429    }
430}
431
432pub fn get_signature_of_function_like_identifier(
433    function_like_identifier: &FunctionLikeIdentifier,
434    codebase: &CodebaseMetadata,
435) -> Option<TCallableSignature> {
436    Some(match function_like_identifier {
437        FunctionLikeIdentifier::Function(name) => {
438            let function_like_metadata = get_function(codebase, name)?;
439
440            get_signature_of_function_like_metadata(
441                function_like_identifier,
442                function_like_metadata,
443                codebase,
444                &TypeExpansionOptions::default(),
445            )
446        }
447        FunctionLikeIdentifier::Closure(file_id, position) => {
448            let function_like_metadata = get_closure(codebase, file_id, position)?;
449
450            get_signature_of_function_like_metadata(
451                function_like_identifier,
452                function_like_metadata,
453                codebase,
454                &TypeExpansionOptions::default(),
455            )
456        }
457        FunctionLikeIdentifier::Method(classlike_name, method_name) => {
458            let function_like_metadata = get_declaring_method(codebase, classlike_name, method_name)?;
459
460            get_signature_of_function_like_metadata(
461                function_like_identifier,
462                function_like_metadata,
463                codebase,
464                &TypeExpansionOptions {
465                    self_class: Some(*classlike_name),
466                    static_class_type: StaticClassType::Name(*classlike_name),
467                    ..Default::default()
468                },
469            )
470        }
471    })
472}
473
474pub fn get_atomic_of_function_like_identifier(
475    function_like_identifier: &FunctionLikeIdentifier,
476    codebase: &CodebaseMetadata,
477) -> Option<TAtomic> {
478    let signature = get_signature_of_function_like_identifier(function_like_identifier, codebase)?;
479
480    Some(TAtomic::Callable(TCallable::Signature(signature)))
481}
482
483pub fn get_signature_of_function_like_metadata(
484    function_like_identifier: &FunctionLikeIdentifier,
485    function_like_metadata: &FunctionLikeMetadata,
486    codebase: &CodebaseMetadata,
487    options: &TypeExpansionOptions,
488) -> TCallableSignature {
489    let parameters: Vec<_> = function_like_metadata
490        .parameters
491        .iter()
492        .map(|parameter_metadata| {
493            let type_signature = if let Some(t) = parameter_metadata.get_type_metadata() {
494                let mut t = t.type_union.clone();
495                expand_union(codebase, &mut t, options);
496                Some(Box::new(t))
497            } else {
498                None
499            };
500
501            TCallableParameter::new(
502                type_signature,
503                parameter_metadata.flags.is_by_reference(),
504                parameter_metadata.flags.is_variadic(),
505                parameter_metadata.flags.has_default(),
506            )
507        })
508        .collect();
509
510    let return_type = if let Some(type_metadata) = function_like_metadata.return_type_metadata.as_ref() {
511        let mut return_type = type_metadata.type_union.clone();
512        expand_union(codebase, &mut return_type, options);
513        Some(Box::new(return_type))
514    } else {
515        None
516    };
517
518    let mut signature = TCallableSignature::new(function_like_metadata.flags.is_pure(), true)
519        .with_parameters(parameters)
520        .with_return_type(return_type)
521        .with_source(Some(*function_like_identifier));
522
523    if let FunctionLikeIdentifier::Closure(file_id, closure_position) = function_like_identifier {
524        signature = signature.with_closure_location(Some((*file_id, *closure_position)));
525    }
526
527    signature
528}
529
530fn expand_key_of(
531    return_type_key_of: &TKeyOf,
532    codebase: &CodebaseMetadata,
533    options: &TypeExpansionOptions,
534) -> Vec<TAtomic> {
535    let mut type_atomics = vec![];
536
537    let mut target_type = return_type_key_of.get_target_type().clone();
538    let mut new_atomics = vec![];
539    let mut remove_target_atomic = false;
540    expand_atomic(&mut target_type, codebase, options, &mut remove_target_atomic, &mut new_atomics);
541    type_atomics.extend(new_atomics);
542    if !remove_target_atomic {
543        type_atomics.push(target_type);
544    }
545
546    let Some(new_return_types) = TKeyOf::get_key_of_targets(&type_atomics, codebase, false) else {
547        return vec![TAtomic::Derived(TDerived::KeyOf(return_type_key_of.clone()))];
548    };
549
550    new_return_types.types.into_owned()
551}
552
553fn expand_value_of(
554    return_type_value_of: &TValueOf,
555    codebase: &CodebaseMetadata,
556    options: &TypeExpansionOptions,
557) -> Vec<TAtomic> {
558    let mut type_atomics = vec![];
559
560    let mut target_type = return_type_value_of.get_target_type().clone();
561    let mut new_atomics = vec![];
562    let mut remove_target_atomic = false;
563    expand_atomic(&mut target_type, codebase, options, &mut remove_target_atomic, &mut new_atomics);
564    type_atomics.extend(new_atomics);
565    if !remove_target_atomic {
566        type_atomics.push(target_type);
567    }
568
569    let Some(new_return_types) = TValueOf::get_value_of_targets(&type_atomics, codebase, false) else {
570        return vec![TAtomic::Derived(TDerived::ValueOf(return_type_value_of.clone()))];
571    };
572
573    new_return_types.types.into_owned()
574}