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