1use std::borrow::Cow;
2use std::cell::RefCell;
3use std::sync::Arc;
4
5use std::collections::HashSet;
6
7use foldhash::fast::FixedState;
8use mago_atom::Atom;
9use mago_atom::ascii_lowercase_atom;
10
11use crate::identifier::function_like::FunctionLikeIdentifier;
12use crate::metadata::CodebaseMetadata;
13use crate::metadata::function_like::FunctionLikeMetadata;
14use crate::ttype::TType;
15use crate::ttype::atomic::TAtomic;
16use crate::ttype::atomic::alias::TAlias;
17use crate::ttype::atomic::array::TArray;
18use crate::ttype::atomic::callable::TCallable;
19use crate::ttype::atomic::callable::TCallableSignature;
20use crate::ttype::atomic::callable::parameter::TCallableParameter;
21use crate::ttype::atomic::derived::TDerived;
22use crate::ttype::atomic::derived::index_access::TIndexAccess;
23use crate::ttype::atomic::derived::int_mask::TIntMask;
24use crate::ttype::atomic::derived::int_mask_of::TIntMaskOf;
25use crate::ttype::atomic::derived::key_of::TKeyOf;
26use crate::ttype::atomic::derived::properties_of::TPropertiesOf;
27use crate::ttype::atomic::derived::value_of::TValueOf;
28use crate::ttype::atomic::mixed::TMixed;
29use crate::ttype::atomic::object::TObject;
30use crate::ttype::atomic::object::named::TNamedObject;
31use crate::ttype::atomic::reference::TReference;
32use crate::ttype::atomic::reference::TReferenceMemberSelector;
33use crate::ttype::atomic::scalar::TScalar;
34use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
35use crate::ttype::combiner;
36use crate::ttype::union::TUnion;
37
38thread_local! {
39 static EXPANDING_ALIASES: RefCell<HashSet<(Atom, Atom), FixedState>> = const { RefCell::new(HashSet::with_hasher(FixedState::with_seed(0))) };
42
43 static EXPANDING_OBJECT_PARAMS: RefCell<HashSet<Atom, FixedState>> = const { RefCell::new(HashSet::with_hasher(FixedState::with_seed(0))) };
45}
46
47#[inline]
52pub fn reset_expansion_state() {
53 EXPANDING_ALIASES.with(|set| set.borrow_mut().clear());
54 EXPANDING_OBJECT_PARAMS.with(|set| set.borrow_mut().clear());
55}
56
57struct AliasExpansionGuard {
60 class_name: Atom,
61 alias_name: Atom,
62}
63
64impl AliasExpansionGuard {
65 fn new(class_name: Atom, alias_name: Atom) -> Self {
66 EXPANDING_ALIASES.with(|set| set.borrow_mut().insert((class_name, alias_name)));
67 Self { class_name, alias_name }
68 }
69}
70
71impl Drop for AliasExpansionGuard {
72 fn drop(&mut self) {
73 EXPANDING_ALIASES.with(|set| set.borrow_mut().remove(&(self.class_name, self.alias_name)));
74 }
75}
76
77struct ObjectParamsExpansionGuard {
79 object_name: Atom,
80}
81
82impl ObjectParamsExpansionGuard {
83 fn try_new(object_name: Atom) -> Option<Self> {
84 EXPANDING_OBJECT_PARAMS.with(|set| {
85 let mut set = set.borrow_mut();
86 if set.contains(&object_name) {
87 None
88 } else {
89 set.insert(object_name);
90 Some(Self { object_name })
91 }
92 })
93 }
94}
95
96impl Drop for ObjectParamsExpansionGuard {
97 fn drop(&mut self) {
98 EXPANDING_OBJECT_PARAMS.with(|set| set.borrow_mut().remove(&self.object_name));
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
103pub enum StaticClassType {
104 #[default]
105 None,
106 Name(Atom),
107 Object(TObject),
108}
109
110#[derive(Debug)]
111pub struct TypeExpansionOptions {
112 pub self_class: Option<Atom>,
113 pub static_class_type: StaticClassType,
114 pub parent_class: Option<Atom>,
115 pub evaluate_class_constants: bool,
116 pub evaluate_conditional_types: bool,
117 pub function_is_final: bool,
118 pub expand_generic: bool,
119 pub expand_templates: bool,
120}
121
122impl Default for TypeExpansionOptions {
123 fn default() -> Self {
124 Self {
125 self_class: None,
126 static_class_type: StaticClassType::default(),
127 parent_class: None,
128 evaluate_class_constants: true,
129 evaluate_conditional_types: false,
130 function_is_final: false,
131 expand_generic: false,
132 expand_templates: true,
133 }
134 }
135}
136
137pub fn expand_union(codebase: &CodebaseMetadata, return_type: &mut TUnion, options: &TypeExpansionOptions) {
140 if !return_type.is_expandable() {
141 return;
142 }
143
144 let mut types = std::mem::take(&mut return_type.types).into_owned();
145 let mut new_return_type_parts: Vec<TAtomic> = Vec::new();
146 let mut skip_mask: u64 = 0;
147
148 for (i, return_type_part) in types.iter_mut().enumerate() {
149 let mut skip_key = false;
150 expand_atomic(return_type_part, codebase, options, &mut skip_key, &mut new_return_type_parts);
151
152 if skip_key && i < 64 {
153 skip_mask |= 1u64 << i;
154 }
155 }
156
157 if skip_mask != 0 {
158 let mut idx = 0usize;
159 types.retain(|_| {
160 let retain = idx >= 64 || (skip_mask & (1u64 << idx)) == 0;
161 idx += 1;
162 retain
163 });
164
165 new_return_type_parts.append(&mut types);
166
167 if new_return_type_parts.is_empty() {
168 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
169 }
170
171 types = if new_return_type_parts.len() > 1 {
172 combiner::combine(new_return_type_parts, codebase, combiner::CombinerOptions::default())
173 } else {
174 new_return_type_parts
175 };
176 } else if types.len() > 1 {
177 types = combiner::combine(types, codebase, combiner::CombinerOptions::default());
178 }
179
180 return_type.types = Cow::Owned(types);
181}
182
183pub(crate) fn expand_atomic(
184 return_type_part: &mut TAtomic,
185 codebase: &CodebaseMetadata,
186 options: &TypeExpansionOptions,
187 skip_key: &mut bool,
188 new_return_type_parts: &mut Vec<TAtomic>,
189) {
190 match return_type_part {
191 TAtomic::Array(array_type) => match array_type {
192 TArray::Keyed(keyed_data) => {
193 if let Some((key_parameter, value_parameter)) = &mut keyed_data.parameters {
194 expand_union(codebase, Arc::make_mut(key_parameter), options);
195 expand_union(codebase, Arc::make_mut(value_parameter), options);
196 }
197
198 if let Some(known_items) = &mut keyed_data.known_items {
199 for (_, item_type) in known_items.values_mut() {
200 expand_union(codebase, item_type, options);
201 }
202 }
203 }
204 TArray::List(list_data) => {
205 expand_union(codebase, Arc::make_mut(&mut list_data.element_type), options);
206
207 if let Some(known_elements) = &mut list_data.known_elements {
208 for (_, element_type) in known_elements.values_mut() {
209 expand_union(codebase, element_type, options);
210 }
211 }
212 }
213 },
214 TAtomic::Object(object) => {
215 expand_object(object, codebase, options);
216 }
217 TAtomic::Callable(TCallable::Signature(signature)) => {
218 if let Some(return_type) = signature.get_return_type_mut() {
219 expand_union(codebase, return_type, options);
220 }
221
222 for param in signature.get_parameters_mut() {
223 if let Some(param_type) = param.get_type_signature_mut() {
224 expand_union(codebase, param_type, options);
225 }
226 }
227 }
228 TAtomic::GenericParameter(parameter) => {
229 expand_union(codebase, Arc::make_mut(&mut parameter.constraint), options);
230 }
231 TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) => {
232 let mut atomic_return_type_parts = vec![];
233 expand_atomic(Arc::make_mut(constraint), codebase, options, &mut false, &mut atomic_return_type_parts);
234
235 if !atomic_return_type_parts.is_empty() {
236 *Arc::make_mut(constraint) = atomic_return_type_parts.remove(0);
237 }
238 }
239 TAtomic::Reference(TReference::Member { class_like_name, member_selector }) => {
240 *skip_key = true;
241 expand_member_reference(*class_like_name, member_selector, codebase, options, new_return_type_parts);
242 }
243 TAtomic::Callable(TCallable::Alias(id)) => {
244 if let Some(value) = get_atomic_of_function_like_identifier(id, codebase) {
245 *skip_key = true;
246 new_return_type_parts.push(value);
247 }
248 }
249 TAtomic::Conditional(conditional) => {
250 *skip_key = true;
251
252 let mut then = (*conditional.then).clone();
253 let mut otherwise = (*conditional.otherwise).clone();
254
255 expand_union(codebase, &mut then, options);
256 expand_union(codebase, &mut otherwise, options);
257
258 new_return_type_parts.extend(then.types.into_owned());
259 new_return_type_parts.extend(otherwise.types.into_owned());
260 }
261 TAtomic::Alias(alias) => {
262 *skip_key = true;
263 new_return_type_parts.extend(expand_alias(alias, codebase, options));
264 }
265 TAtomic::Derived(derived) => match derived {
266 TDerived::KeyOf(key_of) => {
267 *skip_key = true;
268 new_return_type_parts.extend(expand_key_of(key_of, codebase, options));
269 }
270 TDerived::ValueOf(value_of) => {
271 *skip_key = true;
272 new_return_type_parts.extend(expand_value_of(value_of, codebase, options));
273 }
274 TDerived::IndexAccess(index_access) => {
275 *skip_key = true;
276 new_return_type_parts.extend(expand_index_access(index_access, codebase, options));
277 }
278 TDerived::IntMask(int_mask) => {
279 *skip_key = true;
280 new_return_type_parts.extend(expand_int_mask(int_mask, codebase, options));
281 }
282 TDerived::IntMaskOf(int_mask_of) => {
283 *skip_key = true;
284 new_return_type_parts.extend(expand_int_mask_of(int_mask_of, codebase, options));
285 }
286 TDerived::PropertiesOf(properties_of) => {
287 *skip_key = true;
288 new_return_type_parts.extend(expand_properties_of(properties_of, codebase, options));
289 }
290 },
291 TAtomic::Iterable(iterable) => {
292 expand_union(codebase, Arc::make_mut(&mut iterable.key_type), options);
293 expand_union(codebase, Arc::make_mut(&mut iterable.value_type), options);
294 }
295 _ => {}
296 }
297}
298
299#[cold]
300fn expand_member_reference(
301 class_like_name: Atom,
302 member_selector: &TReferenceMemberSelector,
303 codebase: &CodebaseMetadata,
304 options: &TypeExpansionOptions,
305 new_return_type_parts: &mut Vec<TAtomic>,
306) {
307 let Some(class_like) = codebase.get_class_like(&class_like_name) else {
308 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
309 return;
310 };
311
312 for (constant_name, constant) in &class_like.constants {
313 if !member_selector.matches(constant_name) {
314 continue;
315 }
316
317 if let Some(inferred_type) = constant.inferred_type.as_ref() {
318 let mut inferred_type = inferred_type.clone();
319 let mut skip_inferred_type = false;
320 expand_atomic(&mut inferred_type, codebase, options, &mut skip_inferred_type, new_return_type_parts);
321
322 if !skip_inferred_type {
323 new_return_type_parts.push(inferred_type);
324 }
325 } else if let Some(type_metadata) = constant.type_metadata.as_ref() {
326 let mut constant_type = type_metadata.type_union.clone();
327 expand_union(codebase, &mut constant_type, options);
328 new_return_type_parts.extend(constant_type.types.into_owned());
329 } else {
330 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
331 }
332 }
333
334 for enum_case_name in class_like.enum_cases.keys() {
335 if !member_selector.matches(enum_case_name) {
336 continue;
337 }
338 new_return_type_parts.push(TAtomic::Object(TObject::new_enum_case(class_like.original_name, *enum_case_name)));
339 }
340
341 if let TReferenceMemberSelector::Identifier(member_name) = member_selector
342 && let Some(type_alias) = class_like.type_aliases.get(member_name)
343 {
344 let mut alias_type = type_alias.type_union.clone();
345 expand_union(codebase, &mut alias_type, options);
346 new_return_type_parts.extend(alias_type.types.into_owned());
347 }
348
349 if new_return_type_parts.is_empty() {
350 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
351 }
352}
353
354fn expand_object(object: &mut TObject, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) {
355 resolve_special_class_names(object, codebase, options);
356
357 let TObject::Named(named) = object else {
358 return;
359 };
360
361 let Some(_guard) = ObjectParamsExpansionGuard::try_new(named.name) else {
362 return;
363 };
364
365 if let Some(class_metadata) = codebase.get_class_like(&named.name) {
366 for &required in class_metadata.require_extends.iter().chain(&class_metadata.require_implements) {
367 named.add_intersection_type(TAtomic::Object(TObject::Named(TNamedObject::new(required))));
368 }
369 }
370
371 expand_or_fill_type_parameters(named, codebase, options);
372}
373
374fn resolve_special_class_names(object: &mut TObject, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) {
376 if let TObject::Named(named) = object {
377 let name_lc = ascii_lowercase_atom(&named.name);
378 let needs_static_resolution = matches!(name_lc.as_str(), "static" | "$this") || named.is_this;
379
380 if needs_static_resolution
381 && let StaticClassType::Object(TObject::Enum(static_enum)) = &options.static_class_type
382 {
383 *object = TObject::Enum(static_enum.clone());
384 return;
385 }
386 }
387
388 let TObject::Named(named) = object else {
389 return;
390 };
391
392 let name_lc = ascii_lowercase_atom(&named.name);
393 let is_this = named.is_this;
394
395 match name_lc.as_str() {
396 "static" | "$this" => resolve_static_type(named, false, codebase, options),
397 "self" => {
398 if let Some(self_class) = options.self_class {
399 named.name = self_class;
400 }
401 }
402 "parent" => {
403 if let Some(self_class) = options.self_class
404 && let Some(class_metadata) = codebase.get_class_like(&self_class)
405 && let Some(parent) = class_metadata.direct_parent_class
406 {
407 named.name = parent;
408 }
409 }
410 _ if is_this => resolve_static_type(named, true, codebase, options),
411 _ => {}
412 }
413}
414
415fn resolve_static_type(
418 named: &mut TNamedObject,
419 check_compatibility: bool,
420 codebase: &CodebaseMetadata,
421 options: &TypeExpansionOptions,
422) {
423 match &options.static_class_type {
424 StaticClassType::Object(TObject::Named(static_obj)) => {
425 if check_compatibility && !is_static_type_compatible(named, static_obj, codebase) {
426 return;
427 }
428
429 if let Some(intersections) = &static_obj.intersection_types {
430 named.intersection_types.get_or_insert_with(Vec::new).extend(intersections.iter().cloned());
431 }
432
433 if static_obj.type_parameters.is_some() && should_use_static_type_params(named, static_obj, codebase) {
434 named.type_parameters.clone_from(&static_obj.type_parameters);
435 }
436
437 named.name = static_obj.name;
438 named.is_this = true;
439 }
440 StaticClassType::Name(static_class) => {
441 if !check_compatibility || codebase.is_instance_of(static_class, &named.name) {
442 named.name = *static_class;
443 named.is_this = false;
444 }
445 }
446 _ => {}
447 }
448}
449
450fn is_static_type_compatible(named: &TNamedObject, static_obj: &TNamedObject, codebase: &CodebaseMetadata) -> bool {
452 codebase.is_instance_of(&static_obj.name, &named.name)
453 || static_obj
454 .intersection_types
455 .iter()
456 .flatten()
457 .filter_map(|t| if let TAtomic::Object(obj) = t { obj.get_name() } else { None })
458 .any(|name| codebase.is_instance_of(name, &named.name))
459}
460
461fn should_use_static_type_params(named: &TNamedObject, static_obj: &TNamedObject, codebase: &CodebaseMetadata) -> bool {
464 let Some(current_params) = &named.type_parameters else {
465 return true;
466 };
467
468 let Some(class_metadata) = codebase.get_class_like(&static_obj.name) else {
469 return false;
470 };
471
472 let templates = &class_metadata.template_types;
473
474 current_params.len() == templates.len()
475 && current_params.iter().zip(templates.values()).all(|(current, template)| current == &template.constraint)
476}
477
478fn expand_or_fill_type_parameters(
480 named: &mut TNamedObject,
481 codebase: &CodebaseMetadata,
482 options: &TypeExpansionOptions,
483) {
484 if let Some(params) = &mut named.type_parameters
485 && !params.is_empty()
486 {
487 for param in params.iter_mut() {
488 expand_union(codebase, param, options);
489 }
490 return;
491 }
492
493 let Some(class_metadata) = codebase.get_class_like(&named.name) else {
494 return;
495 };
496
497 if class_metadata.template_types.is_empty() {
498 return;
499 }
500
501 let defaults: Vec<TUnion> =
502 class_metadata.template_types.values().map(|template| template.constraint.clone()).collect();
503
504 named.type_parameters = Some(defaults);
505}
506
507#[must_use]
508pub fn get_signature_of_function_like_identifier(
509 function_like_identifier: &FunctionLikeIdentifier,
510 codebase: &CodebaseMetadata,
511) -> Option<TCallableSignature> {
512 Some(match function_like_identifier {
513 FunctionLikeIdentifier::Function(name) => {
514 let function_like_metadata = codebase.get_function(name)?;
515
516 get_signature_of_function_like_metadata(
517 function_like_identifier,
518 function_like_metadata,
519 codebase,
520 &TypeExpansionOptions::default(),
521 )
522 }
523 FunctionLikeIdentifier::Closure(file_id, position) => {
524 let function_like_metadata = codebase.get_closure(file_id, position)?;
525
526 get_signature_of_function_like_metadata(
527 function_like_identifier,
528 function_like_metadata,
529 codebase,
530 &TypeExpansionOptions::default(),
531 )
532 }
533 FunctionLikeIdentifier::Method(classlike_name, method_name) => {
534 let function_like_metadata = codebase.get_declaring_method(classlike_name, method_name)?;
535
536 get_signature_of_function_like_metadata(
537 function_like_identifier,
538 function_like_metadata,
539 codebase,
540 &TypeExpansionOptions {
541 self_class: Some(*classlike_name),
542 static_class_type: StaticClassType::Name(*classlike_name),
543 ..Default::default()
544 },
545 )
546 }
547 })
548}
549
550#[must_use]
551pub fn get_atomic_of_function_like_identifier(
552 function_like_identifier: &FunctionLikeIdentifier,
553 codebase: &CodebaseMetadata,
554) -> Option<TAtomic> {
555 let signature = get_signature_of_function_like_identifier(function_like_identifier, codebase)?;
556
557 Some(TAtomic::Callable(TCallable::Signature(signature)))
558}
559
560#[must_use]
561pub fn get_signature_of_function_like_metadata(
562 function_like_identifier: &FunctionLikeIdentifier,
563 function_like_metadata: &FunctionLikeMetadata,
564 codebase: &CodebaseMetadata,
565 options: &TypeExpansionOptions,
566) -> TCallableSignature {
567 let parameters: Vec<_> = function_like_metadata
568 .parameters
569 .iter()
570 .map(|parameter_metadata| {
571 let type_signature = if let Some(t) = parameter_metadata.get_type_metadata() {
572 let mut t = t.type_union.clone();
573 expand_union(codebase, &mut t, options);
574 Some(Arc::new(t))
575 } else {
576 None
577 };
578
579 TCallableParameter::new(
580 type_signature,
581 parameter_metadata.flags.is_by_reference(),
582 parameter_metadata.flags.is_variadic(),
583 parameter_metadata.flags.has_default(),
584 )
585 })
586 .collect();
587
588 let return_type = if let Some(type_metadata) = function_like_metadata.return_type_metadata.as_ref() {
589 let mut return_type = type_metadata.type_union.clone();
590 expand_union(codebase, &mut return_type, options);
591 Some(Arc::new(return_type))
592 } else {
593 None
594 };
595
596 let mut signature = TCallableSignature::new(function_like_metadata.flags.is_pure(), true)
597 .with_parameters(parameters)
598 .with_return_type(return_type)
599 .with_source(Some(*function_like_identifier));
600
601 if let FunctionLikeIdentifier::Closure(file_id, closure_position) = function_like_identifier {
602 signature = signature.with_closure_location(Some((*file_id, *closure_position)));
603 }
604
605 signature
606}
607
608#[cold]
609fn expand_key_of(
610 return_type_key_of: &TKeyOf,
611 codebase: &CodebaseMetadata,
612 options: &TypeExpansionOptions,
613) -> Vec<TAtomic> {
614 let mut target_type = return_type_key_of.get_target_type().clone();
615 expand_union(codebase, &mut target_type, options);
616
617 let Some(new_return_types) = TKeyOf::get_key_of_targets(&target_type.types, codebase, false) else {
618 return vec![TAtomic::Derived(TDerived::KeyOf(return_type_key_of.clone()))];
619 };
620
621 new_return_types.types.into_owned()
622}
623
624#[cold]
625fn expand_value_of(
626 return_type_value_of: &TValueOf,
627 codebase: &CodebaseMetadata,
628 options: &TypeExpansionOptions,
629) -> Vec<TAtomic> {
630 let mut target_type = return_type_value_of.get_target_type().clone();
631 expand_union(codebase, &mut target_type, options);
632
633 let Some(new_return_types) = TValueOf::get_value_of_targets(&target_type.types, codebase, false) else {
634 return vec![TAtomic::Derived(TDerived::ValueOf(return_type_value_of.clone()))];
635 };
636
637 new_return_types.types.into_owned()
638}
639
640#[cold]
641fn expand_index_access(
642 return_type_index_access: &TIndexAccess,
643 codebase: &CodebaseMetadata,
644 options: &TypeExpansionOptions,
645) -> Vec<TAtomic> {
646 let mut target_type = return_type_index_access.get_target_type().clone();
647 expand_union(codebase, &mut target_type, options);
648
649 let mut index_type = return_type_index_access.get_index_type().clone();
650 expand_union(codebase, &mut index_type, options);
651
652 let Some(new_return_types) = TIndexAccess::get_indexed_access_result(&target_type.types, &index_type.types, false)
653 else {
654 return vec![TAtomic::Derived(TDerived::IndexAccess(return_type_index_access.clone()))];
655 };
656
657 new_return_types.types.into_owned()
658}
659
660#[cold]
661fn expand_int_mask(int_mask: &TIntMask, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
662 let mut literal_values = Vec::new();
663
664 for value in int_mask.get_values() {
665 let mut expanded = value.clone();
666 expand_union(codebase, &mut expanded, options);
667
668 if let Some(int_val) = expanded.get_single_literal_int_value() {
669 literal_values.push(int_val);
670 }
671 }
672
673 if literal_values.is_empty() {
674 return vec![TAtomic::Scalar(TScalar::int())];
675 }
676
677 let combinations = TIntMask::calculate_mask_combinations(&literal_values);
678 combinations.into_iter().map(|v| TAtomic::Scalar(TScalar::literal_int(v))).collect()
679}
680
681#[cold]
682fn expand_int_mask_of(
683 int_mask_of: &TIntMaskOf,
684 codebase: &CodebaseMetadata,
685 options: &TypeExpansionOptions,
686) -> Vec<TAtomic> {
687 let mut target = int_mask_of.get_target_type().clone();
688 expand_union(codebase, &mut target, options);
689
690 let mut literal_values = Vec::new();
691 for atomic in target.types.iter() {
692 if let Some(int_val) = atomic.get_literal_int_value() {
693 literal_values.push(int_val);
694 }
695 }
696
697 if literal_values.is_empty() {
698 return vec![TAtomic::Scalar(TScalar::int())];
699 }
700
701 let combinations = TIntMask::calculate_mask_combinations(&literal_values);
702 combinations.into_iter().map(|v| TAtomic::Scalar(TScalar::literal_int(v))).collect()
703}
704
705#[cold]
706fn expand_properties_of(
707 properties_of: &TPropertiesOf,
708 codebase: &CodebaseMetadata,
709 options: &TypeExpansionOptions,
710) -> Vec<TAtomic> {
711 let mut target_type = properties_of.get_target_type().clone();
712 expand_union(codebase, &mut target_type, options);
713
714 let Some(keyed_array) =
715 TPropertiesOf::get_properties_of_targets(&target_type.types, codebase, properties_of.visibility(), false)
716 else {
717 return vec![TAtomic::Derived(TDerived::PropertiesOf(properties_of.clone()))];
718 };
719
720 vec![keyed_array]
721}
722
723#[cold]
724fn expand_alias(alias: &TAlias, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
725 let class_name = alias.get_class_name();
726 let alias_name = alias.get_alias_name();
727
728 let is_cycle = EXPANDING_ALIASES.with(|set| set.borrow().contains(&(class_name, alias_name)));
730
731 if is_cycle {
732 return vec![TAtomic::Alias(alias.clone())];
733 }
734
735 let Some(mut expanded_union) = alias.resolve(codebase).cloned() else {
736 return vec![TAtomic::Alias(alias.clone())];
737 };
738
739 let _ = AliasExpansionGuard::new(class_name, alias_name);
740
741 expand_union(codebase, &mut expanded_union, options);
742
743 expanded_union.types.into_owned()
744}
745
746#[cfg(test)]
747mod tests {
748 use super::*;
749
750 use std::borrow::Cow;
751 use std::sync::Arc;
752
753 use bumpalo::Bump;
754
755 use mago_atom::atom;
756 use mago_database::Database;
757 use mago_database::DatabaseReader;
758 use mago_database::file::File;
759 use mago_database::file::FileId;
760 use mago_names::resolver::NameResolver;
761 use mago_span::Position;
762 use mago_syntax::parser::parse_file;
763
764 use crate::metadata::CodebaseMetadata;
765 use crate::misc::GenericParent;
766 use crate::populator::populate_codebase;
767 use crate::reference::SymbolReferences;
768 use crate::scanner::scan_program;
769 use crate::ttype::atomic::array::TArray;
770 use crate::ttype::atomic::array::keyed::TKeyedArray;
771 use crate::ttype::atomic::array::list::TList;
772 use crate::ttype::atomic::callable::TCallable;
773 use crate::ttype::atomic::callable::TCallableSignature;
774 use crate::ttype::atomic::callable::parameter::TCallableParameter;
775 use crate::ttype::atomic::conditional::TConditional;
776 use crate::ttype::atomic::derived::TDerived;
777 use crate::ttype::atomic::derived::index_access::TIndexAccess;
778 use crate::ttype::atomic::derived::key_of::TKeyOf;
779 use crate::ttype::atomic::derived::value_of::TValueOf;
780 use crate::ttype::atomic::generic::TGenericParameter;
781 use crate::ttype::atomic::iterable::TIterable;
782 use crate::ttype::atomic::object::r#enum::TEnum;
783 use crate::ttype::atomic::object::named::TNamedObject;
784 use crate::ttype::atomic::reference::TReference;
785 use crate::ttype::atomic::reference::TReferenceMemberSelector;
786 use crate::ttype::atomic::scalar::TScalar;
787 use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
788 use crate::ttype::atomic::scalar::class_like_string::TClassLikeStringKind;
789 use crate::ttype::get_int;
790 use crate::ttype::get_mixed;
791 use crate::ttype::get_never;
792 use crate::ttype::get_null;
793 use crate::ttype::get_string;
794 use crate::ttype::get_void;
795 use crate::ttype::union::UnionFlags;
796
797 fn create_test_codebase(code: &'static str) -> CodebaseMetadata {
798 let file = File::ephemeral(Cow::Borrowed("code.php"), Cow::Borrowed(code));
799 let config =
800 mago_database::DatabaseConfiguration::new(std::path::Path::new("/"), vec![], vec![], vec![], vec![])
801 .into_static();
802 let database = Database::single(file, config);
803
804 let mut codebase = CodebaseMetadata::new();
805 let arena = Bump::new();
806 for file in database.files() {
807 let program = parse_file(&arena, &file);
808 assert!(!program.has_errors(), "Parse failed: {:?}", program.errors);
809 let resolved_names = NameResolver::new(&arena).resolve(program);
810 let program_codebase = scan_program(&arena, &file, program, &resolved_names);
811
812 codebase.extend(program_codebase);
813 }
814
815 populate_codebase(&mut codebase, &mut SymbolReferences::new(), Default::default(), Default::default());
816
817 codebase
818 }
819
820 fn options_with_self(self_class: &str) -> TypeExpansionOptions {
821 TypeExpansionOptions { self_class: Some(ascii_lowercase_atom(self_class)), ..Default::default() }
822 }
823
824 fn options_with_static(static_class: &str) -> TypeExpansionOptions {
825 TypeExpansionOptions {
826 self_class: Some(ascii_lowercase_atom(static_class)),
827 static_class_type: StaticClassType::Name(ascii_lowercase_atom(static_class)),
828 ..Default::default()
829 }
830 }
831
832 fn options_with_static_object(object: TObject) -> TypeExpansionOptions {
833 let name = object.get_name().copied();
834 TypeExpansionOptions {
835 self_class: name,
836 static_class_type: StaticClassType::Object(object),
837 ..Default::default()
838 }
839 }
840
841 macro_rules! assert_expands_to {
842 ($codebase:expr, $input:expr, $expected:expr) => {
843 assert_expands_to!($codebase, $input, $expected, &TypeExpansionOptions::default())
844 };
845 ($codebase:expr, $input:expr, $expected:expr, $options:expr) => {{
846 let mut actual = $input.clone();
847 expand_union($codebase, &mut actual, $options);
848 assert_eq!(
849 actual.types.as_ref(),
850 $expected.types.as_ref(),
851 "Type expansion mismatch.\nInput: {:?}\nExpected: {:?}\nActual: {:?}",
852 $input,
853 $expected,
854 actual
855 );
856 }};
857 }
858
859 fn make_self_object() -> TUnion {
860 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))))
861 }
862
863 fn make_static_object() -> TUnion {
864 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("static")))))
865 }
866
867 fn make_parent_object() -> TUnion {
868 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("parent")))))
869 }
870
871 fn make_named_object(name: &str) -> TUnion {
872 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom(name)))))
873 }
874
875 #[test]
876 fn test_expand_null_type() {
877 let codebase = CodebaseMetadata::new();
878 let null_type = get_null();
879 assert_expands_to!(&codebase, null_type, get_null());
880 }
881
882 #[test]
883 fn test_expand_void_type() {
884 let codebase = CodebaseMetadata::new();
885 let void_type = get_void();
886 assert_expands_to!(&codebase, void_type, get_void());
887 }
888
889 #[test]
890 fn test_expand_never_type() {
891 let codebase = CodebaseMetadata::new();
892 let never_type = get_never();
893 assert_expands_to!(&codebase, never_type, get_never());
894 }
895
896 #[test]
897 fn test_expand_int_type() {
898 let codebase = CodebaseMetadata::new();
899 let int_type = get_int();
900 assert_expands_to!(&codebase, int_type, get_int());
901 }
902
903 #[test]
904 fn test_expand_mixed_type() {
905 let codebase = CodebaseMetadata::new();
906 let mixed_type = get_mixed();
907 assert_expands_to!(&codebase, mixed_type, get_mixed());
908 }
909
910 #[test]
911 fn test_expand_keyed_array_with_self_key() {
912 let code = r"<?php class Foo {}";
913 let codebase = create_test_codebase(code);
914
915 let mut keyed = TKeyedArray::new();
916 keyed.parameters = Some((Arc::new(make_self_object()), Arc::new(get_int())));
917 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
918
919 let options = options_with_self("Foo");
920 let mut actual = input.clone();
921 expand_union(&codebase, &mut actual, &options);
922
923 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
924 && let Some((key, _)) = &keyed.parameters
925 {
926 assert!(key.types.iter().any(|t| {
927 if let TAtomic::Object(TObject::Named(named)) = t {
928 named.name == ascii_lowercase_atom("foo")
929 } else {
930 false
931 }
932 }));
933 }
934 }
935
936 #[test]
937 fn test_expand_keyed_array_with_self_value() {
938 let code = r"<?php class Foo {}";
939 let codebase = create_test_codebase(code);
940
941 let mut keyed = TKeyedArray::new();
942 keyed.parameters = Some((Arc::new(get_string()), Arc::new(make_self_object())));
943 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
944
945 let options = options_with_self("Foo");
946 let mut actual = input.clone();
947 expand_union(&codebase, &mut actual, &options);
948
949 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
950 && let Some((_, value)) = &keyed.parameters
951 {
952 assert!(value.types.iter().any(|t| {
953 if let TAtomic::Object(TObject::Named(named)) = t {
954 named.name == ascii_lowercase_atom("foo")
955 } else {
956 false
957 }
958 }));
959 }
960 }
961
962 #[test]
963 fn test_expand_keyed_array_known_items() {
964 let code = r"<?php class Foo {}";
965 let codebase = create_test_codebase(code);
966
967 use crate::ttype::atomic::array::key::ArrayKey;
968 use std::collections::BTreeMap;
969
970 let mut keyed = TKeyedArray::new();
971 let mut known_items = BTreeMap::new();
972 known_items.insert(ArrayKey::String(atom("key")), (false, make_self_object()));
973 keyed.known_items = Some(known_items);
974 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
975
976 let options = options_with_self("Foo");
977 let mut actual = input.clone();
978 expand_union(&codebase, &mut actual, &options);
979
980 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
981 && let Some(items) = &keyed.known_items
982 {
983 let (_, item_type) = items.get(&ArrayKey::String(atom("key"))).unwrap();
984 assert!(item_type.types.iter().any(|t| {
985 if let TAtomic::Object(TObject::Named(named)) = t {
986 named.name == ascii_lowercase_atom("foo")
987 } else {
988 false
989 }
990 }));
991 }
992 }
993
994 #[test]
995 fn test_expand_list_with_self_element() {
996 let code = r"<?php class Foo {}";
997 let codebase = create_test_codebase(code);
998
999 let list = TList::new(Arc::new(make_self_object()));
1000 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1001
1002 let options = options_with_self("Foo");
1003 let mut actual = input.clone();
1004 expand_union(&codebase, &mut actual, &options);
1005
1006 if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1007 assert!(list.element_type.types.iter().any(|t| {
1008 if let TAtomic::Object(TObject::Named(named)) = t {
1009 named.name == ascii_lowercase_atom("foo")
1010 } else {
1011 false
1012 }
1013 }));
1014 }
1015 }
1016
1017 #[test]
1018 fn test_expand_list_known_elements() {
1019 let code = r"<?php class Foo {}";
1020 let codebase = create_test_codebase(code);
1021
1022 use std::collections::BTreeMap;
1023
1024 let mut list = TList::new(Arc::new(get_mixed()));
1025 let mut known_elements = BTreeMap::new();
1026 known_elements.insert(0, (false, make_self_object()));
1027 list.known_elements = Some(known_elements);
1028 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1029
1030 let options = options_with_self("Foo");
1031 let mut actual = input.clone();
1032 expand_union(&codebase, &mut actual, &options);
1033
1034 if let TAtomic::Array(TArray::List(list)) = &actual.types[0]
1035 && let Some(elements) = &list.known_elements
1036 {
1037 let (_, element_type) = elements.get(&0).unwrap();
1038 assert!(element_type.types.iter().any(|t| {
1039 if let TAtomic::Object(TObject::Named(named)) = t {
1040 named.name == ascii_lowercase_atom("foo")
1041 } else {
1042 false
1043 }
1044 }));
1045 }
1046 }
1047
1048 #[test]
1049 fn test_expand_nested_array() {
1050 let code = r"<?php class Foo {}";
1051 let codebase = create_test_codebase(code);
1052
1053 let inner_list = TList::new(Arc::new(make_self_object()));
1054 let inner_array = TUnion::from_atomic(TAtomic::Array(TArray::List(inner_list)));
1055
1056 let mut outer = TKeyedArray::new();
1057 outer.parameters = Some((Arc::new(make_self_object()), Arc::new(inner_array)));
1058 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(outer)));
1059
1060 let options = options_with_self("Foo");
1061 let mut actual = input.clone();
1062 expand_union(&codebase, &mut actual, &options);
1063
1064 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1065 && let Some((key, value)) = &keyed.parameters
1066 {
1067 assert!(key.types.iter().any(|t| {
1068 if let TAtomic::Object(TObject::Named(named)) = t {
1069 named.name == ascii_lowercase_atom("foo")
1070 } else {
1071 false
1072 }
1073 }));
1074 if let TAtomic::Array(TArray::List(inner)) = &value.types[0] {
1075 assert!(inner.element_type.types.iter().any(|t| {
1076 if let TAtomic::Object(TObject::Named(named)) = t {
1077 named.name == ascii_lowercase_atom("foo")
1078 } else {
1079 false
1080 }
1081 }));
1082 }
1083 }
1084 }
1085
1086 #[test]
1087 fn test_expand_empty_array() {
1088 let codebase = CodebaseMetadata::new();
1089 let keyed = TKeyedArray::new();
1090 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed.clone())));
1091 let expected = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1092 assert_expands_to!(&codebase, input, expected);
1093 }
1094
1095 #[test]
1096 fn test_expand_non_empty_list() {
1097 let code = r"<?php class Foo {}";
1098 let codebase = create_test_codebase(code);
1099
1100 let mut list = TList::new(Arc::new(make_self_object()));
1101 list.non_empty = true;
1102 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1103
1104 let options = options_with_self("Foo");
1105 let mut actual = input.clone();
1106 expand_union(&codebase, &mut actual, &options);
1107
1108 if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1109 assert!(list.non_empty);
1110 assert!(list.element_type.types.iter().any(|t| {
1111 if let TAtomic::Object(TObject::Named(named)) = t {
1112 named.name == ascii_lowercase_atom("foo")
1113 } else {
1114 false
1115 }
1116 }));
1117 }
1118 }
1119
1120 #[test]
1121 fn test_expand_self_to_class_name() {
1122 let code = r"<?php class Foo {}";
1123 let codebase = create_test_codebase(code);
1124
1125 let input = make_self_object();
1126 let options = options_with_self("Foo");
1127 let mut actual = input.clone();
1128 expand_union(&codebase, &mut actual, &options);
1129
1130 assert!(actual.types.iter().any(|t| {
1131 if let TAtomic::Object(TObject::Named(named)) = t {
1132 named.name == ascii_lowercase_atom("foo")
1133 } else {
1134 false
1135 }
1136 }));
1137 }
1138
1139 #[test]
1140 fn test_expand_static_to_class_name() {
1141 let code = r"<?php class Foo {}";
1142 let codebase = create_test_codebase(code);
1143
1144 let input = make_static_object();
1145 let options = options_with_static("Foo");
1146 let mut actual = input.clone();
1147 expand_union(&codebase, &mut actual, &options);
1148
1149 assert!(actual.types.iter().any(|t| {
1150 if let TAtomic::Object(TObject::Named(named)) = t {
1151 named.name == ascii_lowercase_atom("foo")
1152 } else {
1153 false
1154 }
1155 }));
1156 }
1157
1158 #[test]
1159 fn test_expand_static_with_object_type() {
1160 let code = r"<?php class Foo {}";
1161 let codebase = create_test_codebase(code);
1162
1163 let input = make_static_object();
1164 let static_obj = TObject::Named(TNamedObject::new(ascii_lowercase_atom("foo")));
1165 let options = options_with_static_object(static_obj);
1166 let mut actual = input.clone();
1167 expand_union(&codebase, &mut actual, &options);
1168
1169 assert!(actual.types.iter().any(|t| {
1170 if let TAtomic::Object(TObject::Named(named)) = t {
1171 named.name == ascii_lowercase_atom("foo") && named.is_this
1172 } else {
1173 false
1174 }
1175 }));
1176 }
1177
1178 #[test]
1179 fn test_expand_static_with_enum_type() {
1180 let code = r"<?php enum Status { case Active; case Inactive; }";
1181 let codebase = create_test_codebase(code);
1182
1183 let input = make_static_object();
1184 let static_enum = TObject::Enum(TEnum::new(ascii_lowercase_atom("status")));
1185 let options = options_with_static_object(static_enum);
1186 let mut actual = input.clone();
1187 expand_union(&codebase, &mut actual, &options);
1188
1189 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1190 }
1191
1192 #[test]
1193 fn test_expand_parent_to_parent_class() {
1194 let code = r"<?php
1195 class BaseClass {}
1196 class ChildClass extends BaseClass {}
1197 ";
1198 let codebase = create_test_codebase(code);
1199
1200 let input = make_parent_object();
1201 let options = options_with_self("ChildClass");
1202 let mut actual = input.clone();
1203 expand_union(&codebase, &mut actual, &options);
1204
1205 assert!(actual.types.iter().any(|t| {
1206 if let TAtomic::Object(TObject::Named(named)) = t {
1207 named.name == ascii_lowercase_atom("baseclass")
1208 } else {
1209 false
1210 }
1211 }));
1212 }
1213
1214 #[test]
1215 fn test_expand_parent_without_parent_class() {
1216 let code = r"<?php class Foo {}";
1217 let codebase = create_test_codebase(code);
1218
1219 let input = make_parent_object();
1220 let options = options_with_self("Foo");
1221 let mut actual = input.clone();
1222 expand_union(&codebase, &mut actual, &options);
1223
1224 assert!(actual.types.iter().any(|t| {
1225 if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("parent") } else { false }
1226 }));
1227 }
1228
1229 #[test]
1230 fn test_expand_this_variable() {
1231 let code = r"<?php class Foo {}";
1232 let codebase = create_test_codebase(code);
1233
1234 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new_this(atom("$this")))));
1235 let options = options_with_static("Foo");
1236 let mut actual = input.clone();
1237 expand_union(&codebase, &mut actual, &options);
1238
1239 assert!(actual.types.iter().any(|t| {
1240 if let TAtomic::Object(TObject::Named(named)) = t {
1241 named.name == ascii_lowercase_atom("foo")
1242 } else {
1243 false
1244 }
1245 }));
1246 }
1247
1248 #[test]
1249 fn test_expand_this_with_final_function() {
1250 let code = r"<?php class Foo {}";
1251 let codebase = create_test_codebase(code);
1252
1253 let input = make_static_object();
1254 let options = TypeExpansionOptions {
1255 self_class: Some(ascii_lowercase_atom("foo")),
1256 static_class_type: StaticClassType::Name(ascii_lowercase_atom("foo")),
1257 function_is_final: true,
1258 ..Default::default()
1259 };
1260 let mut actual = input.clone();
1261 expand_union(&codebase, &mut actual, &options);
1262
1263 assert!(actual.types.iter().any(|t| {
1264 if let TAtomic::Object(TObject::Named(named)) = t {
1265 named.name == ascii_lowercase_atom("foo") && !named.is_this
1266 } else {
1267 false
1268 }
1269 }));
1270 }
1271
1272 #[test]
1273 fn test_expand_object_with_type_parameters() {
1274 let code = r"<?php class Container {}";
1275 let codebase = create_test_codebase(code);
1276
1277 let named =
1278 TNamedObject::new_with_type_parameters(ascii_lowercase_atom("container"), Some(vec![make_self_object()]));
1279 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1280
1281 let options = options_with_self("Foo");
1282 let mut actual = input.clone();
1283 expand_union(&codebase, &mut actual, &options);
1284
1285 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
1286 && let Some(params) = &named.type_parameters
1287 {
1288 assert!(params[0].types.iter().any(|t| {
1289 if let TAtomic::Object(TObject::Named(named)) = t {
1290 named.name == ascii_lowercase_atom("foo")
1291 } else {
1292 false
1293 }
1294 }));
1295 }
1296 }
1297
1298 #[test]
1299 fn test_expand_object_gets_default_type_params() {
1300 let code = r"<?php
1301 /** @template T */
1302 class Container {}
1303 ";
1304 let codebase = create_test_codebase(code);
1305
1306 let named = TNamedObject::new(ascii_lowercase_atom("container"));
1307 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1308
1309 let mut actual = input.clone();
1310 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1311
1312 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1313 assert!(named.type_parameters.is_some());
1314 }
1315 }
1316
1317 #[test]
1318 fn test_expand_object_intersection_from_static() {
1319 let code = r"<?php
1320 interface Stringable {}
1321 class Foo implements Stringable {}
1322 ";
1323 let codebase = create_test_codebase(code);
1324
1325 let input = make_static_object();
1326
1327 let mut static_named = TNamedObject::new(ascii_lowercase_atom("foo"));
1328 static_named.intersection_types =
1329 Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom("stringable"))))]);
1330 let static_obj = TObject::Named(static_named);
1331 let options = options_with_static_object(static_obj);
1332
1333 let mut actual = input.clone();
1334 expand_union(&codebase, &mut actual, &options);
1335
1336 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1337 assert!(named.intersection_types.is_some());
1338 }
1339 }
1340
1341 #[test]
1342 fn test_expand_self_without_self_class_option() {
1343 let codebase = CodebaseMetadata::new();
1344
1345 let input = make_self_object();
1346 let mut actual = input.clone();
1347 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1348
1349 assert!(actual.types.iter().any(|t| {
1350 if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("self") } else { false }
1351 }));
1352 }
1353
1354 #[test]
1355 fn test_expand_callable_return_type() {
1356 let code = r"<?php class Foo {}";
1357 let codebase = create_test_codebase(code);
1358
1359 let sig = TCallableSignature::new(false, false).with_return_type(Some(Arc::new(make_self_object())));
1360 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1361
1362 let options = options_with_self("Foo");
1363 let mut actual = input.clone();
1364 expand_union(&codebase, &mut actual, &options);
1365
1366 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1367 && let Some(ret) = sig.get_return_type()
1368 {
1369 assert!(ret.types.iter().any(|t| {
1370 if let TAtomic::Object(TObject::Named(named)) = t {
1371 named.name == ascii_lowercase_atom("foo")
1372 } else {
1373 false
1374 }
1375 }));
1376 }
1377 }
1378
1379 #[test]
1380 fn test_expand_callable_parameter_types() {
1381 let code = r"<?php class Foo {}";
1382 let codebase = create_test_codebase(code);
1383
1384 let param = TCallableParameter::new(Some(Arc::new(make_self_object())), false, false, false);
1385 let sig = TCallableSignature::new(false, false).with_parameters(vec![param]);
1386 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1387
1388 let options = options_with_self("Foo");
1389 let mut actual = input.clone();
1390 expand_union(&codebase, &mut actual, &options);
1391
1392 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1393 && let Some(param) = sig.get_parameters().first()
1394 && let Some(param_type) = param.get_type_signature()
1395 {
1396 assert!(param_type.types.iter().any(|t| {
1397 if let TAtomic::Object(TObject::Named(named)) = t {
1398 named.name == ascii_lowercase_atom("foo")
1399 } else {
1400 false
1401 }
1402 }));
1403 }
1404 }
1405
1406 #[test]
1407 fn test_expand_callable_alias_to_function() {
1408 let code = r"<?php
1409 function myFunc(): int { return 1; }
1410 ";
1411 let codebase = create_test_codebase(code);
1412
1413 let alias = TCallable::Alias(FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc")));
1414 let input = TUnion::from_atomic(TAtomic::Callable(alias));
1415
1416 let mut actual = input.clone();
1417 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1418
1419 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1420 }
1421
1422 #[test]
1423 fn test_expand_callable_alias_to_method() {
1424 let code = r"<?php
1425 class Foo {
1426 public function bar(): int { return 1; }
1427 }
1428 ";
1429 let codebase = create_test_codebase(code);
1430
1431 let alias =
1432 TCallable::Alias(FunctionLikeIdentifier::Method(ascii_lowercase_atom("foo"), ascii_lowercase_atom("bar")));
1433 let input = TUnion::from_atomic(TAtomic::Callable(alias));
1434
1435 let mut actual = input.clone();
1436 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1437
1438 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1439 }
1440
1441 #[test]
1442 fn test_expand_callable_alias_unknown() {
1443 let codebase = CodebaseMetadata::new();
1444
1445 let alias = TCallable::Alias(FunctionLikeIdentifier::Function(atom("nonexistent")));
1446 let input = TUnion::from_atomic(TAtomic::Callable(alias.clone()));
1447
1448 let mut actual = input.clone();
1449 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1450
1451 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Alias(_)))));
1452 }
1453
1454 #[test]
1455 fn test_expand_closure_signature() {
1456 let code = r"<?php class Foo {}";
1457 let codebase = create_test_codebase(code);
1458
1459 let sig = TCallableSignature::new(false, true).with_return_type(Some(Arc::new(make_self_object())));
1460 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1461
1462 let options = options_with_self("Foo");
1463 let mut actual = input.clone();
1464 expand_union(&codebase, &mut actual, &options);
1465
1466 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1467 && let Some(ret) = sig.get_return_type()
1468 {
1469 assert!(ret.types.iter().any(|t| {
1470 if let TAtomic::Object(TObject::Named(named)) = t {
1471 named.name == ascii_lowercase_atom("foo")
1472 } else {
1473 false
1474 }
1475 }));
1476 }
1477 }
1478
1479 #[test]
1480 fn test_expand_generic_parameter_constraint() {
1481 let code = r"<?php class Foo {}";
1482 let codebase = create_test_codebase(code);
1483
1484 let generic = TGenericParameter::new(
1485 atom("T"),
1486 Arc::new(make_self_object()),
1487 GenericParent::ClassLike(ascii_lowercase_atom("foo")),
1488 );
1489 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1490
1491 let options = options_with_self("Foo");
1492 let mut actual = input.clone();
1493 expand_union(&codebase, &mut actual, &options);
1494
1495 if let TAtomic::GenericParameter(param) = &actual.types[0] {
1496 assert!(param.constraint.types.iter().any(|t| {
1497 if let TAtomic::Object(TObject::Named(named)) = t {
1498 named.name == ascii_lowercase_atom("foo")
1499 } else {
1500 false
1501 }
1502 }));
1503 }
1504 }
1505
1506 #[test]
1507 fn test_expand_nested_generic_constraint() {
1508 let code = r"<?php class Foo {} class Bar {}";
1509 let codebase = create_test_codebase(code);
1510
1511 let container =
1512 TNamedObject::new_with_type_parameters(ascii_lowercase_atom("container"), Some(vec![make_self_object()]));
1513 let constraint = TUnion::from_atomic(TAtomic::Object(TObject::Named(container)));
1514
1515 let generic = TGenericParameter::new(
1516 atom("T"),
1517 Arc::new(constraint),
1518 GenericParent::ClassLike(ascii_lowercase_atom("bar")),
1519 );
1520 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1521
1522 let options = options_with_self("Foo");
1523 let mut actual = input.clone();
1524 expand_union(&codebase, &mut actual, &options);
1525
1526 if let TAtomic::GenericParameter(param) = &actual.types[0]
1527 && let TAtomic::Object(TObject::Named(named)) = ¶m.constraint.types[0]
1528 && let Some(params) = &named.type_parameters
1529 {
1530 assert!(params[0].types.iter().any(|t| {
1531 if let TAtomic::Object(TObject::Named(named)) = t {
1532 named.name == ascii_lowercase_atom("foo")
1533 } else {
1534 false
1535 }
1536 }));
1537 }
1538 }
1539
1540 #[test]
1541 fn test_expand_generic_with_intersection() {
1542 let code = r"<?php
1543 interface Stringable {}
1544 class Foo {}
1545 ";
1546 let codebase = create_test_codebase(code);
1547
1548 let mut generic = TGenericParameter::new(
1549 atom("T"),
1550 Arc::new(make_self_object()),
1551 GenericParent::ClassLike(ascii_lowercase_atom("foo")),
1552 );
1553 generic.intersection_types =
1554 Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom("stringable"))))]);
1555 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1556
1557 let options = options_with_self("Foo");
1558 let mut actual = input.clone();
1559 expand_union(&codebase, &mut actual, &options);
1560
1561 if let TAtomic::GenericParameter(param) = &actual.types[0] {
1562 assert!(param.intersection_types.is_some());
1563 assert!(param.constraint.types.iter().any(|t| {
1564 if let TAtomic::Object(TObject::Named(named)) = t {
1565 named.name == ascii_lowercase_atom("foo")
1566 } else {
1567 false
1568 }
1569 }));
1570 }
1571 }
1572
1573 #[test]
1574 fn test_expand_class_string_of_self() {
1575 let code = r"<?php class Foo {}";
1576 let codebase = create_test_codebase(code);
1577
1578 let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))));
1579 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1580 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1581
1582 let options = options_with_self("Foo");
1583 let mut actual = input.clone();
1584 expand_union(&codebase, &mut actual, &options);
1585
1586 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1587 && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1588 {
1589 assert_eq!(named.name, ascii_lowercase_atom("foo"));
1590 }
1591 }
1592
1593 #[test]
1594 fn test_expand_class_string_of_static() {
1595 let code = r"<?php class Foo {}";
1596 let codebase = create_test_codebase(code);
1597
1598 let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("static")))));
1599 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1600 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1601
1602 let options = options_with_static("Foo");
1603 let mut actual = input.clone();
1604 expand_union(&codebase, &mut actual, &options);
1605
1606 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1607 && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1608 {
1609 assert_eq!(named.name, ascii_lowercase_atom("foo"));
1610 }
1611 }
1612
1613 #[test]
1614 fn test_expand_interface_string_of_type() {
1615 let code = r"<?php interface MyInterface {}";
1616 let codebase = create_test_codebase(code);
1617
1618 let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))));
1619 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Interface, constraint };
1620 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1621
1622 let options = options_with_self("MyInterface");
1623 let mut actual = input.clone();
1624 expand_union(&codebase, &mut actual, &options);
1625
1626 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { kind, constraint })) =
1627 &actual.types[0]
1628 {
1629 assert!(matches!(kind, TClassLikeStringKind::Interface));
1630 if let TAtomic::Object(TObject::Named(named)) = constraint.as_ref() {
1631 assert_eq!(named.name, ascii_lowercase_atom("myinterface"));
1632 }
1633 }
1634 }
1635
1636 #[test]
1637 fn test_expand_member_reference_wildcard_constants() {
1638 let code = r"<?php
1639 class Foo {
1640 public const A = 1;
1641 public const B = 2;
1642 }
1643 ";
1644 let codebase = create_test_codebase(code);
1645
1646 let reference = TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Wildcard);
1647 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1648
1649 let mut actual = input.clone();
1650 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1651
1652 assert!(!actual.types.is_empty());
1653 }
1654
1655 #[test]
1656 fn test_expand_member_reference_wildcard_enum_cases() {
1657 let code = r"<?php
1658 enum Status {
1659 case Active;
1660 case Inactive;
1661 }
1662 ";
1663 let codebase = create_test_codebase(code);
1664
1665 let reference = TReference::new_member(ascii_lowercase_atom("status"), TReferenceMemberSelector::Wildcard);
1666 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1667
1668 let mut actual = input.clone();
1669 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1670
1671 assert_eq!(actual.types.len(), 2);
1672 assert!(actual.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1673 }
1674
1675 #[test]
1676 fn test_expand_member_reference_starts_with() {
1677 let code = r"<?php
1678 class Foo {
1679 public const STATUS_ACTIVE = 1;
1680 public const STATUS_INACTIVE = 2;
1681 public const OTHER = 3;
1682 }
1683 ";
1684 let codebase = create_test_codebase(code);
1685
1686 let reference =
1687 TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::StartsWith(atom("STATUS_")));
1688 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1689
1690 let mut actual = input.clone();
1691 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1692
1693 assert!(!actual.types.is_empty());
1694 }
1695
1696 #[test]
1697 fn test_expand_member_reference_ends_with() {
1698 let code = r"<?php
1699 class Foo {
1700 public const READ_ERROR = 1;
1701 public const WRITE_ERROR = 2;
1702 public const SUCCESS = 0;
1703 }
1704 ";
1705 let codebase = create_test_codebase(code);
1706
1707 let reference =
1708 TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::EndsWith(atom("_ERROR")));
1709 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1710
1711 let mut actual = input.clone();
1712 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1713
1714 assert!(!actual.types.is_empty());
1715 }
1716
1717 #[test]
1718 fn test_expand_member_reference_identifier_constant() {
1719 let code = r"<?php
1720 class Foo {
1721 public const BAR = 42;
1722 }
1723 ";
1724 let codebase = create_test_codebase(code);
1725
1726 let reference =
1727 TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("BAR")));
1728 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1729
1730 let mut actual = input.clone();
1731 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1732
1733 assert_eq!(actual.types.len(), 1);
1734 }
1735
1736 #[test]
1737 fn test_expand_member_reference_identifier_enum_case() {
1738 let code = r"<?php
1739 enum Status {
1740 case Active;
1741 }
1742 ";
1743 let codebase = create_test_codebase(code);
1744
1745 let reference = TReference::new_member(
1746 ascii_lowercase_atom("status"),
1747 TReferenceMemberSelector::Identifier(atom("Active")),
1748 );
1749 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1750
1751 let mut actual = input.clone();
1752 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1753
1754 assert_eq!(actual.types.len(), 1);
1755 assert!(matches!(&actual.types[0], TAtomic::Object(TObject::Enum(_))));
1756 }
1757
1758 #[test]
1759 fn test_expand_member_reference_unknown_class() {
1760 let codebase = CodebaseMetadata::new();
1761
1762 let reference = TReference::new_member(atom("NonExistent"), TReferenceMemberSelector::Identifier(atom("FOO")));
1763 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1764
1765 let mut actual = input.clone();
1766 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1767
1768 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
1769 }
1770
1771 #[test]
1772 fn test_expand_member_reference_unknown_member() {
1773 let code = r"<?php class Foo {}";
1774 let codebase = create_test_codebase(code);
1775
1776 let reference = TReference::new_member(
1777 ascii_lowercase_atom("foo"),
1778 TReferenceMemberSelector::Identifier(atom("NONEXISTENT")),
1779 );
1780 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1781
1782 let mut actual = input.clone();
1783 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1784
1785 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
1786 }
1787
1788 #[test]
1789 fn test_expand_member_reference_constant_with_inferred_type() {
1790 let code = r#"<?php
1791 class Foo {
1792 public const VALUE = "hello";
1793 }
1794 "#;
1795 let codebase = create_test_codebase(code);
1796
1797 let reference =
1798 TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("VALUE")));
1799 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1800
1801 let mut actual = input.clone();
1802 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1803
1804 assert_eq!(actual.types.len(), 1);
1805 }
1806
1807 #[test]
1808 fn test_expand_member_reference_constant_with_type_metadata() {
1809 let code = r"<?php
1810 class Foo {
1811 /** @var int */
1812 public const VALUE = 42;
1813 }
1814 ";
1815 let codebase = create_test_codebase(code);
1816
1817 let reference =
1818 TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("VALUE")));
1819 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1820
1821 let mut actual = input.clone();
1822 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1823
1824 assert_eq!(actual.types.len(), 1);
1825 }
1826
1827 #[test]
1828 fn test_expand_conditional_both_branches() {
1829 let code = r"<?php class Foo {} class Bar {}";
1830 let codebase = create_test_codebase(code);
1831
1832 let conditional = TConditional::new(
1833 Arc::new(get_mixed()),
1834 Arc::new(get_string()),
1835 Arc::new(make_self_object()),
1836 Arc::new(make_self_object()),
1837 false,
1838 );
1839 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
1840
1841 let options = options_with_self("Foo");
1842 let mut actual = input.clone();
1843 expand_union(&codebase, &mut actual, &options);
1844
1845 assert!(actual.types.iter().any(|t| {
1846 if let TAtomic::Object(TObject::Named(named)) = t {
1847 named.name == ascii_lowercase_atom("foo")
1848 } else {
1849 false
1850 }
1851 }));
1852 }
1853
1854 #[test]
1855 fn test_expand_conditional_with_self_in_then() {
1856 let code = r"<?php class Foo {}";
1857 let codebase = create_test_codebase(code);
1858
1859 let conditional = TConditional::new(
1860 Arc::new(get_mixed()),
1861 Arc::new(get_string()),
1862 Arc::new(make_self_object()),
1863 Arc::new(get_int()),
1864 false,
1865 );
1866 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
1867
1868 let options = options_with_self("Foo");
1869 let mut actual = input.clone();
1870 expand_union(&codebase, &mut actual, &options);
1871
1872 assert!(!actual.types.is_empty());
1873 }
1874
1875 #[test]
1876 fn test_expand_conditional_with_self_in_otherwise() {
1877 let code = r"<?php class Foo {}";
1878 let codebase = create_test_codebase(code);
1879
1880 let conditional = TConditional::new(
1881 Arc::new(get_mixed()),
1882 Arc::new(get_string()),
1883 Arc::new(get_int()),
1884 Arc::new(make_self_object()),
1885 false,
1886 );
1887 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
1888
1889 let options = options_with_self("Foo");
1890 let mut actual = input.clone();
1891 expand_union(&codebase, &mut actual, &options);
1892
1893 assert!(!actual.types.is_empty());
1894 }
1895
1896 #[test]
1897 fn test_expand_simple_alias() {
1898 let code = r"<?php
1899 class Foo {
1900 /** @phpstan-type MyInt = int */
1901 }
1902 ";
1903 let codebase = create_test_codebase(code);
1904
1905 let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("MyInt"));
1906 let input = TUnion::from_atomic(TAtomic::Alias(alias));
1907
1908 let mut actual = input.clone();
1909 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1910
1911 assert!(!actual.types.is_empty());
1912 }
1913
1914 #[test]
1915 fn test_expand_nested_alias() {
1916 let code = r"<?php
1917 class Foo {
1918 /** @phpstan-type Inner = int */
1919 /** @phpstan-type Outer = Inner */
1920 }
1921 ";
1922 let codebase = create_test_codebase(code);
1923
1924 let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("Outer"));
1925 let input = TUnion::from_atomic(TAtomic::Alias(alias));
1926
1927 let mut actual = input.clone();
1928 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1929
1930 assert!(!actual.types.is_empty());
1931 }
1932
1933 #[test]
1934 fn test_expand_alias_cycle_detection() {
1935 let codebase = CodebaseMetadata::new();
1936
1937 let alias = TAlias::new(atom("Foo"), atom("SelfRef"));
1938 let input = TUnion::from_atomic(TAtomic::Alias(alias.clone()));
1939
1940 let mut actual = input.clone();
1941 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1942
1943 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Alias(_))));
1944 }
1945
1946 #[test]
1947 fn test_expand_alias_unknown() {
1948 let codebase = CodebaseMetadata::new();
1949
1950 let alias = TAlias::new(atom("NonExistent"), atom("Unknown"));
1951 let input = TUnion::from_atomic(TAtomic::Alias(alias.clone()));
1952
1953 let mut actual = input.clone();
1954 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1955
1956 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Alias(_))));
1957 }
1958
1959 #[test]
1960 fn test_expand_alias_with_self_inside() {
1961 let code = r"<?php
1962 class Foo {
1963 /** @phpstan-type MySelf = self */
1964 }
1965 ";
1966 let codebase = create_test_codebase(code);
1967
1968 let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("MySelf"));
1969 let input = TUnion::from_atomic(TAtomic::Alias(alias));
1970
1971 let options = options_with_self("Foo");
1972 let mut actual = input.clone();
1973 expand_union(&codebase, &mut actual, &options);
1974
1975 assert!(!actual.types.is_empty());
1976 }
1977
1978 #[test]
1979 fn test_expand_key_of_array() {
1980 let codebase = CodebaseMetadata::new();
1981
1982 let mut keyed = TKeyedArray::new();
1983 keyed.parameters = Some((Arc::new(get_string()), Arc::new(get_int())));
1984 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1985
1986 let key_of = TKeyOf::new(Arc::new(array_type));
1987 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
1988
1989 let mut actual = input.clone();
1990 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1991
1992 assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_string));
1993 }
1994
1995 #[test]
1996 fn test_expand_key_of_with_self() {
1997 let code = r"<?php class Foo {}";
1998 let codebase = create_test_codebase(code);
1999
2000 let mut keyed = TKeyedArray::new();
2001 keyed.parameters = Some((Arc::new(make_self_object()), Arc::new(get_int())));
2002 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2003
2004 let key_of = TKeyOf::new(Arc::new(array_type));
2005 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
2006
2007 let options = options_with_self("Foo");
2008 let mut actual = input.clone();
2009 expand_union(&codebase, &mut actual, &options);
2010
2011 assert!(!actual.types.is_empty());
2012 }
2013
2014 #[test]
2015 fn test_expand_value_of_array() {
2016 let codebase = CodebaseMetadata::new();
2017
2018 let mut keyed = TKeyedArray::new();
2019 keyed.parameters = Some((Arc::new(get_string()), Arc::new(get_int())));
2020 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2021
2022 let value_of = TValueOf::new(Arc::new(array_type));
2023 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
2024
2025 let mut actual = input.clone();
2026 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2027
2028 assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_int));
2029 }
2030
2031 #[test]
2032 fn test_expand_value_of_enum() {
2033 let code = r"<?php
2034 enum Status: string {
2035 case Active = 'active';
2036 case Inactive = 'inactive';
2037 }
2038 ";
2039 let codebase = create_test_codebase(code);
2040
2041 let enum_type = TUnion::from_atomic(TAtomic::Object(TObject::Enum(TEnum::new(ascii_lowercase_atom("status")))));
2042
2043 let value_of = TValueOf::new(Arc::new(enum_type));
2044 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
2045
2046 let mut actual = input.clone();
2047 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2048
2049 assert!(!actual.types.is_empty());
2050 }
2051
2052 #[test]
2053 fn test_expand_index_access() {
2054 let codebase = CodebaseMetadata::new();
2055
2056 use crate::ttype::atomic::array::key::ArrayKey;
2057 use std::collections::BTreeMap;
2058
2059 let mut keyed = TKeyedArray::new();
2060 let mut known_items = BTreeMap::new();
2061 known_items.insert(ArrayKey::String(atom("key")), (false, get_int()));
2062 keyed.known_items = Some(known_items);
2063 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2064
2065 use crate::ttype::get_literal_string;
2066 let index_type = get_literal_string(atom("key"));
2067
2068 let index_access = TIndexAccess::new(array_type, index_type);
2069 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2070
2071 let mut actual = input.clone();
2072 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2073
2074 assert!(!actual.types.is_empty());
2075 }
2076
2077 #[test]
2078 fn test_expand_index_access_with_self() {
2079 let code = r"<?php class Foo {}";
2080 let codebase = create_test_codebase(code);
2081
2082 use crate::ttype::atomic::array::key::ArrayKey;
2083 use std::collections::BTreeMap;
2084
2085 let mut keyed = TKeyedArray::new();
2086 let mut known_items = BTreeMap::new();
2087 known_items.insert(ArrayKey::String(atom("key")), (false, make_self_object()));
2088 keyed.known_items = Some(known_items);
2089 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2090
2091 use crate::ttype::get_literal_string;
2092 let index_type = get_literal_string(atom("key"));
2093
2094 let index_access = TIndexAccess::new(array_type, index_type);
2095 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2096
2097 let options = options_with_self("Foo");
2098 let mut actual = input.clone();
2099 expand_union(&codebase, &mut actual, &options);
2100
2101 assert!(!actual.types.is_empty());
2102 }
2103
2104 #[test]
2105 fn test_expand_iterable_key_type() {
2106 let code = r"<?php class Foo {}";
2107 let codebase = create_test_codebase(code);
2108
2109 let iterable = TIterable::new(Arc::new(make_self_object()), Arc::new(get_int()));
2110 let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2111
2112 let options = options_with_self("Foo");
2113 let mut actual = input.clone();
2114 expand_union(&codebase, &mut actual, &options);
2115
2116 if let TAtomic::Iterable(iter) = &actual.types[0] {
2117 assert!(iter.get_key_type().types.iter().any(|t| {
2118 if let TAtomic::Object(TObject::Named(named)) = t {
2119 named.name == ascii_lowercase_atom("foo")
2120 } else {
2121 false
2122 }
2123 }));
2124 }
2125 }
2126
2127 #[test]
2128 fn test_expand_iterable_value_type() {
2129 let code = r"<?php class Foo {}";
2130 let codebase = create_test_codebase(code);
2131
2132 let iterable = TIterable::new(Arc::new(get_int()), Arc::new(make_self_object()));
2133 let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2134
2135 let options = options_with_self("Foo");
2136 let mut actual = input.clone();
2137 expand_union(&codebase, &mut actual, &options);
2138
2139 if let TAtomic::Iterable(iter) = &actual.types[0] {
2140 assert!(iter.get_value_type().types.iter().any(|t| {
2141 if let TAtomic::Object(TObject::Named(named)) = t {
2142 named.name == ascii_lowercase_atom("foo")
2143 } else {
2144 false
2145 }
2146 }));
2147 }
2148 }
2149
2150 #[test]
2151 fn test_get_signature_of_function() {
2152 let code = r#"<?php
2153 function myFunc(int $a): string { return ""; }
2154 "#;
2155 let codebase = create_test_codebase(code);
2156
2157 let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc"));
2158
2159 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2160 assert!(sig.is_some());
2161
2162 let sig = sig.unwrap();
2163 assert_eq!(sig.get_parameters().len(), 1);
2164 assert!(sig.get_return_type().is_some());
2165 }
2166
2167 #[test]
2168 fn test_get_signature_of_method() {
2169 let code = r"<?php
2170 class Foo {
2171 public function bar(string $s): int { return 0; }
2172 }
2173 ";
2174 let codebase = create_test_codebase(code);
2175
2176 let id = FunctionLikeIdentifier::Method(ascii_lowercase_atom("foo"), ascii_lowercase_atom("bar"));
2177
2178 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2179 assert!(sig.is_some());
2180
2181 let sig = sig.unwrap();
2182 assert_eq!(sig.get_parameters().len(), 1);
2183 }
2184
2185 #[test]
2186 fn test_get_signature_of_closure() {
2187 let codebase = CodebaseMetadata::new();
2188
2189 let id = FunctionLikeIdentifier::Closure(FileId::new("test"), Position::new(0));
2190 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2191
2192 assert!(sig.is_none());
2193 }
2194
2195 #[test]
2196 fn test_get_atomic_of_function() {
2197 let code = r"<?php
2198 function myFunc(): void {}
2199 ";
2200 let codebase = create_test_codebase(code);
2201
2202 let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc"));
2203
2204 let atomic = get_atomic_of_function_like_identifier(&id, &codebase);
2205 assert!(atomic.is_some());
2206 assert!(matches!(atomic.unwrap(), TAtomic::Callable(TCallable::Signature(_))));
2207 }
2208
2209 #[test]
2210 fn test_get_signature_with_parameters() {
2211 let code = r"<?php
2212 function multiParam(int $a, string $b, ?float $c = null): bool { return true; }
2213 ";
2214 let codebase = create_test_codebase(code);
2215
2216 let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("multiparam"));
2217
2218 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2219 assert!(sig.is_some());
2220
2221 let sig = sig.unwrap();
2222 assert_eq!(sig.get_parameters().len(), 3);
2223
2224 let third_param = &sig.get_parameters()[2];
2225 assert!(third_param.has_default());
2226 }
2227
2228 #[test]
2229 fn test_expand_preserves_by_reference_flag() {
2230 let code = r"<?php class Foo {}";
2231 let codebase = create_test_codebase(code);
2232
2233 let mut input = make_self_object();
2234 input.flags.insert(UnionFlags::BY_REFERENCE);
2235
2236 let options = options_with_self("Foo");
2237 let mut actual = input.clone();
2238 expand_union(&codebase, &mut actual, &options);
2239
2240 assert!(actual.flags.contains(UnionFlags::BY_REFERENCE));
2241 }
2242
2243 #[test]
2244 fn test_expand_preserves_possibly_undefined_flag() {
2245 let code = r"<?php class Foo {}";
2246 let codebase = create_test_codebase(code);
2247
2248 let mut input = make_self_object();
2249 input.flags.insert(UnionFlags::POSSIBLY_UNDEFINED);
2250
2251 let options = options_with_self("Foo");
2252 let mut actual = input.clone();
2253 expand_union(&codebase, &mut actual, &options);
2254
2255 assert!(actual.flags.contains(UnionFlags::POSSIBLY_UNDEFINED));
2256 }
2257
2258 #[test]
2259 fn test_expand_multiple_self_in_union() {
2260 let code = r"<?php class Foo {}";
2261 let codebase = create_test_codebase(code);
2262
2263 let input = TUnion::from_vec(vec![
2264 TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))),
2265 TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))),
2266 ]);
2267
2268 let options = options_with_self("Foo");
2269 let mut actual = input.clone();
2270 expand_union(&codebase, &mut actual, &options);
2271
2272 assert!(actual.types.len() <= 2);
2273 }
2274
2275 #[test]
2276 fn test_expand_deeply_nested_types() {
2277 let code = r"<?php class Foo {}";
2278 let codebase = create_test_codebase(code);
2279
2280 let inner = TList::new(Arc::new(make_self_object()));
2281 let middle = TList::new(Arc::new(TUnion::from_atomic(TAtomic::Array(TArray::List(inner)))));
2282 let outer = TList::new(Arc::new(TUnion::from_atomic(TAtomic::Array(TArray::List(middle)))));
2283 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(outer)));
2284
2285 let options = options_with_self("Foo");
2286 let mut actual = input.clone();
2287 expand_union(&codebase, &mut actual, &options);
2288
2289 if let TAtomic::Array(TArray::List(outer)) = &actual.types[0]
2290 && let TAtomic::Array(TArray::List(middle)) = &outer.element_type.types[0]
2291 && let TAtomic::Array(TArray::List(inner)) = &middle.element_type.types[0]
2292 {
2293 assert!(inner.element_type.types.iter().any(|t| {
2294 if let TAtomic::Object(TObject::Named(named)) = t {
2295 named.name == ascii_lowercase_atom("foo")
2296 } else {
2297 false
2298 }
2299 }));
2300 }
2301 }
2302
2303 #[test]
2304 fn test_expand_with_all_options_disabled() {
2305 let code = r"<?php class Foo {}";
2306 let codebase = create_test_codebase(code);
2307
2308 let input = make_self_object();
2309 let options = TypeExpansionOptions {
2310 self_class: None,
2311 static_class_type: StaticClassType::None,
2312 parent_class: None,
2313 evaluate_class_constants: false,
2314 evaluate_conditional_types: false,
2315 function_is_final: false,
2316 expand_generic: false,
2317 expand_templates: false,
2318 };
2319
2320 let mut actual = input.clone();
2321 expand_union(&codebase, &mut actual, &options);
2322
2323 assert!(actual.types.iter().any(|t| {
2324 if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("self") } else { false }
2325 }));
2326 }
2327
2328 #[test]
2329 fn test_expand_already_expanded_type() {
2330 let code = r"<?php class Foo {}";
2331 let codebase = create_test_codebase(code);
2332
2333 let input = make_named_object("Foo");
2334 let options = options_with_self("Foo");
2335
2336 let mut actual = input.clone();
2337 expand_union(&codebase, &mut actual, &options);
2338
2339 let mut actual2 = actual.clone();
2340 expand_union(&codebase, &mut actual2, &options);
2341
2342 assert_eq!(actual.types.as_ref(), actual2.types.as_ref());
2343 }
2344
2345 #[test]
2346 fn test_expand_complex_generic_class() {
2347 let code = r"<?php
2348 /**
2349 * @template T
2350 * @template U
2351 */
2352 class Container {}
2353 ";
2354 let codebase = create_test_codebase(code);
2355
2356 let named = TNamedObject::new_with_type_parameters(
2357 ascii_lowercase_atom("container"),
2358 Some(vec![make_self_object(), make_static_object()]),
2359 );
2360 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
2361
2362 let options = TypeExpansionOptions {
2363 self_class: Some(ascii_lowercase_atom("foo")),
2364 static_class_type: StaticClassType::Name(ascii_lowercase_atom("bar")),
2365 ..Default::default()
2366 };
2367
2368 let mut actual = input.clone();
2369 expand_union(&codebase, &mut actual, &options);
2370
2371 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
2372 && let Some(params) = &named.type_parameters
2373 {
2374 assert!(params[0].types.iter().any(|t| {
2375 if let TAtomic::Object(TObject::Named(named)) = t {
2376 named.name == ascii_lowercase_atom("foo")
2377 } else {
2378 false
2379 }
2380 }));
2381 assert!(params[1].types.iter().any(|t| {
2382 if let TAtomic::Object(TObject::Named(named)) = t {
2383 named.name == ascii_lowercase_atom("bar")
2384 } else {
2385 false
2386 }
2387 }));
2388 }
2389 }
2390}