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