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