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