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