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).0;
804 let resolved_names = NameResolver::new(&arena).resolve(program);
805 let program_codebase = scan_program(&arena, &file, program, &resolved_names);
806
807 codebase.extend(program_codebase);
808 }
809
810 populate_codebase(&mut codebase, &mut SymbolReferences::new(), Default::default(), Default::default());
811
812 codebase
813 }
814
815 fn options_with_self(self_class: &str) -> TypeExpansionOptions {
816 TypeExpansionOptions { self_class: Some(ascii_lowercase_atom(self_class)), ..Default::default() }
817 }
818
819 fn options_with_static(static_class: &str) -> TypeExpansionOptions {
820 TypeExpansionOptions {
821 self_class: Some(ascii_lowercase_atom(static_class)),
822 static_class_type: StaticClassType::Name(ascii_lowercase_atom(static_class)),
823 ..Default::default()
824 }
825 }
826
827 fn options_with_static_object(object: TObject) -> TypeExpansionOptions {
828 let name = object.get_name().copied();
829 TypeExpansionOptions {
830 self_class: name,
831 static_class_type: StaticClassType::Object(object),
832 ..Default::default()
833 }
834 }
835
836 macro_rules! assert_expands_to {
837 ($codebase:expr, $input:expr, $expected:expr) => {
838 assert_expands_to!($codebase, $input, $expected, &TypeExpansionOptions::default())
839 };
840 ($codebase:expr, $input:expr, $expected:expr, $options:expr) => {{
841 let mut actual = $input.clone();
842 expand_union($codebase, &mut actual, $options);
843 assert_eq!(
844 actual.types.as_ref(),
845 $expected.types.as_ref(),
846 "Type expansion mismatch.\nInput: {:?}\nExpected: {:?}\nActual: {:?}",
847 $input,
848 $expected,
849 actual
850 );
851 }};
852 }
853
854 fn make_self_object() -> TUnion {
855 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))))
856 }
857
858 fn make_static_object() -> TUnion {
859 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("static")))))
860 }
861
862 fn make_parent_object() -> TUnion {
863 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("parent")))))
864 }
865
866 fn make_named_object(name: &str) -> TUnion {
867 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom(name)))))
868 }
869
870 #[test]
871 fn test_expand_null_type() {
872 let codebase = CodebaseMetadata::new();
873 let null_type = get_null();
874 assert_expands_to!(&codebase, null_type, get_null());
875 }
876
877 #[test]
878 fn test_expand_void_type() {
879 let codebase = CodebaseMetadata::new();
880 let void_type = get_void();
881 assert_expands_to!(&codebase, void_type, get_void());
882 }
883
884 #[test]
885 fn test_expand_never_type() {
886 let codebase = CodebaseMetadata::new();
887 let never_type = get_never();
888 assert_expands_to!(&codebase, never_type, get_never());
889 }
890
891 #[test]
892 fn test_expand_int_type() {
893 let codebase = CodebaseMetadata::new();
894 let int_type = get_int();
895 assert_expands_to!(&codebase, int_type, get_int());
896 }
897
898 #[test]
899 fn test_expand_mixed_type() {
900 let codebase = CodebaseMetadata::new();
901 let mixed_type = get_mixed();
902 assert_expands_to!(&codebase, mixed_type, get_mixed());
903 }
904
905 #[test]
906 fn test_expand_keyed_array_with_self_key() {
907 let code = r"<?php class Foo {}";
908 let codebase = create_test_codebase(code);
909
910 let mut keyed = TKeyedArray::new();
911 keyed.parameters = Some((Box::new(make_self_object()), Box::new(get_int())));
912 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
913
914 let options = options_with_self("Foo");
915 let mut actual = input.clone();
916 expand_union(&codebase, &mut actual, &options);
917
918 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
919 && let Some((key, _)) = &keyed.parameters
920 {
921 assert!(key.types.iter().any(|t| {
922 if let TAtomic::Object(TObject::Named(named)) = t {
923 named.name == ascii_lowercase_atom("foo")
924 } else {
925 false
926 }
927 }));
928 }
929 }
930
931 #[test]
932 fn test_expand_keyed_array_with_self_value() {
933 let code = r"<?php class Foo {}";
934 let codebase = create_test_codebase(code);
935
936 let mut keyed = TKeyedArray::new();
937 keyed.parameters = Some((Box::new(get_string()), Box::new(make_self_object())));
938 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
939
940 let options = options_with_self("Foo");
941 let mut actual = input.clone();
942 expand_union(&codebase, &mut actual, &options);
943
944 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
945 && let Some((_, value)) = &keyed.parameters
946 {
947 assert!(value.types.iter().any(|t| {
948 if let TAtomic::Object(TObject::Named(named)) = t {
949 named.name == ascii_lowercase_atom("foo")
950 } else {
951 false
952 }
953 }));
954 }
955 }
956
957 #[test]
958 fn test_expand_keyed_array_known_items() {
959 let code = r"<?php class Foo {}";
960 let codebase = create_test_codebase(code);
961
962 use crate::ttype::atomic::array::key::ArrayKey;
963 use std::collections::BTreeMap;
964
965 let mut keyed = TKeyedArray::new();
966 let mut known_items = BTreeMap::new();
967 known_items.insert(ArrayKey::String(atom("key")), (false, make_self_object()));
968 keyed.known_items = Some(known_items);
969 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
970
971 let options = options_with_self("Foo");
972 let mut actual = input.clone();
973 expand_union(&codebase, &mut actual, &options);
974
975 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
976 && let Some(items) = &keyed.known_items
977 {
978 let (_, item_type) = items.get(&ArrayKey::String(atom("key"))).unwrap();
979 assert!(item_type.types.iter().any(|t| {
980 if let TAtomic::Object(TObject::Named(named)) = t {
981 named.name == ascii_lowercase_atom("foo")
982 } else {
983 false
984 }
985 }));
986 }
987 }
988
989 #[test]
990 fn test_expand_list_with_self_element() {
991 let code = r"<?php class Foo {}";
992 let codebase = create_test_codebase(code);
993
994 let list = TList::new(Box::new(make_self_object()));
995 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
996
997 let options = options_with_self("Foo");
998 let mut actual = input.clone();
999 expand_union(&codebase, &mut actual, &options);
1000
1001 if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1002 assert!(list.element_type.types.iter().any(|t| {
1003 if let TAtomic::Object(TObject::Named(named)) = t {
1004 named.name == ascii_lowercase_atom("foo")
1005 } else {
1006 false
1007 }
1008 }));
1009 }
1010 }
1011
1012 #[test]
1013 fn test_expand_list_known_elements() {
1014 let code = r"<?php class Foo {}";
1015 let codebase = create_test_codebase(code);
1016
1017 use std::collections::BTreeMap;
1018
1019 let mut list = TList::new(Box::new(get_mixed()));
1020 let mut known_elements = BTreeMap::new();
1021 known_elements.insert(0, (false, make_self_object()));
1022 list.known_elements = Some(known_elements);
1023 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1024
1025 let options = options_with_self("Foo");
1026 let mut actual = input.clone();
1027 expand_union(&codebase, &mut actual, &options);
1028
1029 if let TAtomic::Array(TArray::List(list)) = &actual.types[0]
1030 && let Some(elements) = &list.known_elements
1031 {
1032 let (_, element_type) = elements.get(&0).unwrap();
1033 assert!(element_type.types.iter().any(|t| {
1034 if let TAtomic::Object(TObject::Named(named)) = t {
1035 named.name == ascii_lowercase_atom("foo")
1036 } else {
1037 false
1038 }
1039 }));
1040 }
1041 }
1042
1043 #[test]
1044 fn test_expand_nested_array() {
1045 let code = r"<?php class Foo {}";
1046 let codebase = create_test_codebase(code);
1047
1048 let inner_list = TList::new(Box::new(make_self_object()));
1049 let inner_array = TUnion::from_atomic(TAtomic::Array(TArray::List(inner_list)));
1050
1051 let mut outer = TKeyedArray::new();
1052 outer.parameters = Some((Box::new(make_self_object()), Box::new(inner_array)));
1053 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(outer)));
1054
1055 let options = options_with_self("Foo");
1056 let mut actual = input.clone();
1057 expand_union(&codebase, &mut actual, &options);
1058
1059 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1060 && let Some((key, value)) = &keyed.parameters
1061 {
1062 assert!(key.types.iter().any(|t| {
1063 if let TAtomic::Object(TObject::Named(named)) = t {
1064 named.name == ascii_lowercase_atom("foo")
1065 } else {
1066 false
1067 }
1068 }));
1069 if let TAtomic::Array(TArray::List(inner)) = &value.types[0] {
1070 assert!(inner.element_type.types.iter().any(|t| {
1071 if let TAtomic::Object(TObject::Named(named)) = t {
1072 named.name == ascii_lowercase_atom("foo")
1073 } else {
1074 false
1075 }
1076 }));
1077 }
1078 }
1079 }
1080
1081 #[test]
1082 fn test_expand_empty_array() {
1083 let codebase = CodebaseMetadata::new();
1084 let keyed = TKeyedArray::new();
1085 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed.clone())));
1086 let expected = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1087 assert_expands_to!(&codebase, input, expected);
1088 }
1089
1090 #[test]
1091 fn test_expand_non_empty_list() {
1092 let code = r"<?php class Foo {}";
1093 let codebase = create_test_codebase(code);
1094
1095 let mut list = TList::new(Box::new(make_self_object()));
1096 list.non_empty = true;
1097 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1098
1099 let options = options_with_self("Foo");
1100 let mut actual = input.clone();
1101 expand_union(&codebase, &mut actual, &options);
1102
1103 if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1104 assert!(list.non_empty);
1105 assert!(list.element_type.types.iter().any(|t| {
1106 if let TAtomic::Object(TObject::Named(named)) = t {
1107 named.name == ascii_lowercase_atom("foo")
1108 } else {
1109 false
1110 }
1111 }));
1112 }
1113 }
1114
1115 #[test]
1116 fn test_expand_self_to_class_name() {
1117 let code = r"<?php class Foo {}";
1118 let codebase = create_test_codebase(code);
1119
1120 let input = make_self_object();
1121 let options = options_with_self("Foo");
1122 let mut actual = input.clone();
1123 expand_union(&codebase, &mut actual, &options);
1124
1125 assert!(actual.types.iter().any(|t| {
1126 if let TAtomic::Object(TObject::Named(named)) = t {
1127 named.name == ascii_lowercase_atom("foo")
1128 } else {
1129 false
1130 }
1131 }));
1132 }
1133
1134 #[test]
1135 fn test_expand_static_to_class_name() {
1136 let code = r"<?php class Foo {}";
1137 let codebase = create_test_codebase(code);
1138
1139 let input = make_static_object();
1140 let options = options_with_static("Foo");
1141 let mut actual = input.clone();
1142 expand_union(&codebase, &mut actual, &options);
1143
1144 assert!(actual.types.iter().any(|t| {
1145 if let TAtomic::Object(TObject::Named(named)) = t {
1146 named.name == ascii_lowercase_atom("foo")
1147 } else {
1148 false
1149 }
1150 }));
1151 }
1152
1153 #[test]
1154 fn test_expand_static_with_object_type() {
1155 let code = r"<?php class Foo {}";
1156 let codebase = create_test_codebase(code);
1157
1158 let input = make_static_object();
1159 let static_obj = TObject::Named(TNamedObject::new(ascii_lowercase_atom("foo")));
1160 let options = options_with_static_object(static_obj);
1161 let mut actual = input.clone();
1162 expand_union(&codebase, &mut actual, &options);
1163
1164 assert!(actual.types.iter().any(|t| {
1165 if let TAtomic::Object(TObject::Named(named)) = t {
1166 named.name == ascii_lowercase_atom("foo") && named.is_this
1167 } else {
1168 false
1169 }
1170 }));
1171 }
1172
1173 #[test]
1174 fn test_expand_static_with_enum_type() {
1175 let code = r"<?php enum Status { case Active; case Inactive; }";
1176 let codebase = create_test_codebase(code);
1177
1178 let input = make_static_object();
1179 let static_enum = TObject::Enum(TEnum::new(ascii_lowercase_atom("status")));
1180 let options = options_with_static_object(static_enum);
1181 let mut actual = input.clone();
1182 expand_union(&codebase, &mut actual, &options);
1183
1184 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1185 }
1186
1187 #[test]
1188 fn test_expand_parent_to_parent_class() {
1189 let code = r"<?php
1190 class BaseClass {}
1191 class ChildClass extends BaseClass {}
1192 ";
1193 let codebase = create_test_codebase(code);
1194
1195 let input = make_parent_object();
1196 let options = options_with_self("ChildClass");
1197 let mut actual = input.clone();
1198 expand_union(&codebase, &mut actual, &options);
1199
1200 assert!(actual.types.iter().any(|t| {
1201 if let TAtomic::Object(TObject::Named(named)) = t {
1202 named.name == ascii_lowercase_atom("baseclass")
1203 } else {
1204 false
1205 }
1206 }));
1207 }
1208
1209 #[test]
1210 fn test_expand_parent_without_parent_class() {
1211 let code = r"<?php class Foo {}";
1212 let codebase = create_test_codebase(code);
1213
1214 let input = make_parent_object();
1215 let options = options_with_self("Foo");
1216 let mut actual = input.clone();
1217 expand_union(&codebase, &mut actual, &options);
1218
1219 assert!(actual.types.iter().any(|t| {
1220 if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("parent") } else { false }
1221 }));
1222 }
1223
1224 #[test]
1225 fn test_expand_this_variable() {
1226 let code = r"<?php class Foo {}";
1227 let codebase = create_test_codebase(code);
1228
1229 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new_this(atom("$this")))));
1230 let options = options_with_static("Foo");
1231 let mut actual = input.clone();
1232 expand_union(&codebase, &mut actual, &options);
1233
1234 assert!(actual.types.iter().any(|t| {
1235 if let TAtomic::Object(TObject::Named(named)) = t {
1236 named.name == ascii_lowercase_atom("foo")
1237 } else {
1238 false
1239 }
1240 }));
1241 }
1242
1243 #[test]
1244 fn test_expand_this_with_final_function() {
1245 let code = r"<?php class Foo {}";
1246 let codebase = create_test_codebase(code);
1247
1248 let input = make_static_object();
1249 let options = TypeExpansionOptions {
1250 self_class: Some(ascii_lowercase_atom("foo")),
1251 static_class_type: StaticClassType::Name(ascii_lowercase_atom("foo")),
1252 function_is_final: true,
1253 ..Default::default()
1254 };
1255 let mut actual = input.clone();
1256 expand_union(&codebase, &mut actual, &options);
1257
1258 assert!(actual.types.iter().any(|t| {
1259 if let TAtomic::Object(TObject::Named(named)) = t {
1260 named.name == ascii_lowercase_atom("foo") && !named.is_this
1261 } else {
1262 false
1263 }
1264 }));
1265 }
1266
1267 #[test]
1268 fn test_expand_object_with_type_parameters() {
1269 let code = r"<?php class Container {}";
1270 let codebase = create_test_codebase(code);
1271
1272 let named =
1273 TNamedObject::new_with_type_parameters(ascii_lowercase_atom("container"), Some(vec![make_self_object()]));
1274 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1275
1276 let options = options_with_self("Foo");
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 && let Some(params) = &named.type_parameters
1282 {
1283 assert!(params[0].types.iter().any(|t| {
1284 if let TAtomic::Object(TObject::Named(named)) = t {
1285 named.name == ascii_lowercase_atom("foo")
1286 } else {
1287 false
1288 }
1289 }));
1290 }
1291 }
1292
1293 #[test]
1294 fn test_expand_object_gets_default_type_params() {
1295 let code = r"<?php
1296 /** @template T */
1297 class Container {}
1298 ";
1299 let codebase = create_test_codebase(code);
1300
1301 let named = TNamedObject::new(ascii_lowercase_atom("container"));
1302 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1303
1304 let mut actual = input.clone();
1305 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1306
1307 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1308 assert!(named.type_parameters.is_some());
1309 }
1310 }
1311
1312 #[test]
1313 fn test_expand_object_intersection_from_static() {
1314 let code = r"<?php
1315 interface Stringable {}
1316 class Foo implements Stringable {}
1317 ";
1318 let codebase = create_test_codebase(code);
1319
1320 let input = make_static_object();
1321
1322 let mut static_named = TNamedObject::new(ascii_lowercase_atom("foo"));
1323 static_named.intersection_types =
1324 Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom("stringable"))))]);
1325 let static_obj = TObject::Named(static_named);
1326 let options = options_with_static_object(static_obj);
1327
1328 let mut actual = input.clone();
1329 expand_union(&codebase, &mut actual, &options);
1330
1331 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1332 assert!(named.intersection_types.is_some());
1333 }
1334 }
1335
1336 #[test]
1337 fn test_expand_self_without_self_class_option() {
1338 let codebase = CodebaseMetadata::new();
1339
1340 let input = make_self_object();
1341 let mut actual = input.clone();
1342 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1343
1344 assert!(actual.types.iter().any(|t| {
1345 if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("self") } else { false }
1346 }));
1347 }
1348
1349 #[test]
1350 fn test_expand_callable_return_type() {
1351 let code = r"<?php class Foo {}";
1352 let codebase = create_test_codebase(code);
1353
1354 let sig = TCallableSignature::new(false, false).with_return_type(Some(Box::new(make_self_object())));
1355 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1356
1357 let options = options_with_self("Foo");
1358 let mut actual = input.clone();
1359 expand_union(&codebase, &mut actual, &options);
1360
1361 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1362 && let Some(ret) = sig.get_return_type()
1363 {
1364 assert!(ret.types.iter().any(|t| {
1365 if let TAtomic::Object(TObject::Named(named)) = t {
1366 named.name == ascii_lowercase_atom("foo")
1367 } else {
1368 false
1369 }
1370 }));
1371 }
1372 }
1373
1374 #[test]
1375 fn test_expand_callable_parameter_types() {
1376 let code = r"<?php class Foo {}";
1377 let codebase = create_test_codebase(code);
1378
1379 let param = TCallableParameter::new(Some(Box::new(make_self_object())), false, false, false);
1380 let sig = TCallableSignature::new(false, false).with_parameters(vec![param]);
1381 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1382
1383 let options = options_with_self("Foo");
1384 let mut actual = input.clone();
1385 expand_union(&codebase, &mut actual, &options);
1386
1387 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1388 && let Some(param) = sig.get_parameters().first()
1389 && let Some(param_type) = param.get_type_signature()
1390 {
1391 assert!(param_type.types.iter().any(|t| {
1392 if let TAtomic::Object(TObject::Named(named)) = t {
1393 named.name == ascii_lowercase_atom("foo")
1394 } else {
1395 false
1396 }
1397 }));
1398 }
1399 }
1400
1401 #[test]
1402 fn test_expand_callable_alias_to_function() {
1403 let code = r"<?php
1404 function myFunc(): int { return 1; }
1405 ";
1406 let codebase = create_test_codebase(code);
1407
1408 let alias = TCallable::Alias(FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc")));
1409 let input = TUnion::from_atomic(TAtomic::Callable(alias));
1410
1411 let mut actual = input.clone();
1412 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1413
1414 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1415 }
1416
1417 #[test]
1418 fn test_expand_callable_alias_to_method() {
1419 let code = r"<?php
1420 class Foo {
1421 public function bar(): int { return 1; }
1422 }
1423 ";
1424 let codebase = create_test_codebase(code);
1425
1426 let alias =
1427 TCallable::Alias(FunctionLikeIdentifier::Method(ascii_lowercase_atom("foo"), ascii_lowercase_atom("bar")));
1428 let input = TUnion::from_atomic(TAtomic::Callable(alias));
1429
1430 let mut actual = input.clone();
1431 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1432
1433 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1434 }
1435
1436 #[test]
1437 fn test_expand_callable_alias_unknown() {
1438 let codebase = CodebaseMetadata::new();
1439
1440 let alias = TCallable::Alias(FunctionLikeIdentifier::Function(atom("nonexistent")));
1441 let input = TUnion::from_atomic(TAtomic::Callable(alias.clone()));
1442
1443 let mut actual = input.clone();
1444 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1445
1446 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Alias(_)))));
1447 }
1448
1449 #[test]
1450 fn test_expand_closure_signature() {
1451 let code = r"<?php class Foo {}";
1452 let codebase = create_test_codebase(code);
1453
1454 let sig = TCallableSignature::new(false, true).with_return_type(Some(Box::new(make_self_object())));
1455 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1456
1457 let options = options_with_self("Foo");
1458 let mut actual = input.clone();
1459 expand_union(&codebase, &mut actual, &options);
1460
1461 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1462 && let Some(ret) = sig.get_return_type()
1463 {
1464 assert!(ret.types.iter().any(|t| {
1465 if let TAtomic::Object(TObject::Named(named)) = t {
1466 named.name == ascii_lowercase_atom("foo")
1467 } else {
1468 false
1469 }
1470 }));
1471 }
1472 }
1473
1474 #[test]
1475 fn test_expand_generic_parameter_constraint() {
1476 let code = r"<?php class Foo {}";
1477 let codebase = create_test_codebase(code);
1478
1479 let generic = TGenericParameter::new(
1480 atom("T"),
1481 Box::new(make_self_object()),
1482 GenericParent::ClassLike(ascii_lowercase_atom("foo")),
1483 );
1484 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1485
1486 let options = options_with_self("Foo");
1487 let mut actual = input.clone();
1488 expand_union(&codebase, &mut actual, &options);
1489
1490 if let TAtomic::GenericParameter(param) = &actual.types[0] {
1491 assert!(param.constraint.types.iter().any(|t| {
1492 if let TAtomic::Object(TObject::Named(named)) = t {
1493 named.name == ascii_lowercase_atom("foo")
1494 } else {
1495 false
1496 }
1497 }));
1498 }
1499 }
1500
1501 #[test]
1502 fn test_expand_nested_generic_constraint() {
1503 let code = r"<?php class Foo {} class Bar {}";
1504 let codebase = create_test_codebase(code);
1505
1506 let container =
1507 TNamedObject::new_with_type_parameters(ascii_lowercase_atom("container"), Some(vec![make_self_object()]));
1508 let constraint = TUnion::from_atomic(TAtomic::Object(TObject::Named(container)));
1509
1510 let generic = TGenericParameter::new(
1511 atom("T"),
1512 Box::new(constraint),
1513 GenericParent::ClassLike(ascii_lowercase_atom("bar")),
1514 );
1515 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1516
1517 let options = options_with_self("Foo");
1518 let mut actual = input.clone();
1519 expand_union(&codebase, &mut actual, &options);
1520
1521 if let TAtomic::GenericParameter(param) = &actual.types[0]
1522 && let TAtomic::Object(TObject::Named(named)) = ¶m.constraint.types[0]
1523 && let Some(params) = &named.type_parameters
1524 {
1525 assert!(params[0].types.iter().any(|t| {
1526 if let TAtomic::Object(TObject::Named(named)) = t {
1527 named.name == ascii_lowercase_atom("foo")
1528 } else {
1529 false
1530 }
1531 }));
1532 }
1533 }
1534
1535 #[test]
1536 fn test_expand_generic_with_intersection() {
1537 let code = r"<?php
1538 interface Stringable {}
1539 class Foo {}
1540 ";
1541 let codebase = create_test_codebase(code);
1542
1543 let mut generic = TGenericParameter::new(
1544 atom("T"),
1545 Box::new(make_self_object()),
1546 GenericParent::ClassLike(ascii_lowercase_atom("foo")),
1547 );
1548 generic.intersection_types =
1549 Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom("stringable"))))]);
1550 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1551
1552 let options = options_with_self("Foo");
1553 let mut actual = input.clone();
1554 expand_union(&codebase, &mut actual, &options);
1555
1556 if let TAtomic::GenericParameter(param) = &actual.types[0] {
1557 assert!(param.intersection_types.is_some());
1558 assert!(param.constraint.types.iter().any(|t| {
1559 if let TAtomic::Object(TObject::Named(named)) = t {
1560 named.name == ascii_lowercase_atom("foo")
1561 } else {
1562 false
1563 }
1564 }));
1565 }
1566 }
1567
1568 #[test]
1569 fn test_expand_class_string_of_self() {
1570 let code = r"<?php class Foo {}";
1571 let codebase = create_test_codebase(code);
1572
1573 let constraint = Box::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))));
1574 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1575 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1576
1577 let options = options_with_self("Foo");
1578 let mut actual = input.clone();
1579 expand_union(&codebase, &mut actual, &options);
1580
1581 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1582 && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1583 {
1584 assert_eq!(named.name, ascii_lowercase_atom("foo"));
1585 }
1586 }
1587
1588 #[test]
1589 fn test_expand_class_string_of_static() {
1590 let code = r"<?php class Foo {}";
1591 let codebase = create_test_codebase(code);
1592
1593 let constraint = Box::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("static")))));
1594 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1595 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1596
1597 let options = options_with_static("Foo");
1598 let mut actual = input.clone();
1599 expand_union(&codebase, &mut actual, &options);
1600
1601 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1602 && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1603 {
1604 assert_eq!(named.name, ascii_lowercase_atom("foo"));
1605 }
1606 }
1607
1608 #[test]
1609 fn test_expand_interface_string_of_type() {
1610 let code = r"<?php interface MyInterface {}";
1611 let codebase = create_test_codebase(code);
1612
1613 let constraint = Box::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))));
1614 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Interface, constraint };
1615 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1616
1617 let options = options_with_self("MyInterface");
1618 let mut actual = input.clone();
1619 expand_union(&codebase, &mut actual, &options);
1620
1621 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { kind, constraint })) =
1622 &actual.types[0]
1623 {
1624 assert!(matches!(kind, TClassLikeStringKind::Interface));
1625 if let TAtomic::Object(TObject::Named(named)) = constraint.as_ref() {
1626 assert_eq!(named.name, ascii_lowercase_atom("myinterface"));
1627 }
1628 }
1629 }
1630
1631 #[test]
1632 fn test_expand_member_reference_wildcard_constants() {
1633 let code = r"<?php
1634 class Foo {
1635 public const A = 1;
1636 public const B = 2;
1637 }
1638 ";
1639 let codebase = create_test_codebase(code);
1640
1641 let reference = TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Wildcard);
1642 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1643
1644 let mut actual = input.clone();
1645 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1646
1647 assert!(!actual.types.is_empty());
1648 }
1649
1650 #[test]
1651 fn test_expand_member_reference_wildcard_enum_cases() {
1652 let code = r"<?php
1653 enum Status {
1654 case Active;
1655 case Inactive;
1656 }
1657 ";
1658 let codebase = create_test_codebase(code);
1659
1660 let reference = TReference::new_member(ascii_lowercase_atom("status"), TReferenceMemberSelector::Wildcard);
1661 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1662
1663 let mut actual = input.clone();
1664 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1665
1666 assert_eq!(actual.types.len(), 2);
1667 assert!(actual.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1668 }
1669
1670 #[test]
1671 fn test_expand_member_reference_starts_with() {
1672 let code = r"<?php
1673 class Foo {
1674 public const STATUS_ACTIVE = 1;
1675 public const STATUS_INACTIVE = 2;
1676 public const OTHER = 3;
1677 }
1678 ";
1679 let codebase = create_test_codebase(code);
1680
1681 let reference =
1682 TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::StartsWith(atom("STATUS_")));
1683 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1684
1685 let mut actual = input.clone();
1686 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1687
1688 assert!(!actual.types.is_empty());
1689 }
1690
1691 #[test]
1692 fn test_expand_member_reference_ends_with() {
1693 let code = r"<?php
1694 class Foo {
1695 public const READ_ERROR = 1;
1696 public const WRITE_ERROR = 2;
1697 public const SUCCESS = 0;
1698 }
1699 ";
1700 let codebase = create_test_codebase(code);
1701
1702 let reference =
1703 TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::EndsWith(atom("_ERROR")));
1704 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1705
1706 let mut actual = input.clone();
1707 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1708
1709 assert!(!actual.types.is_empty());
1710 }
1711
1712 #[test]
1713 fn test_expand_member_reference_identifier_constant() {
1714 let code = r"<?php
1715 class Foo {
1716 public const BAR = 42;
1717 }
1718 ";
1719 let codebase = create_test_codebase(code);
1720
1721 let reference =
1722 TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("BAR")));
1723 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1724
1725 let mut actual = input.clone();
1726 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1727
1728 assert_eq!(actual.types.len(), 1);
1729 }
1730
1731 #[test]
1732 fn test_expand_member_reference_identifier_enum_case() {
1733 let code = r"<?php
1734 enum Status {
1735 case Active;
1736 }
1737 ";
1738 let codebase = create_test_codebase(code);
1739
1740 let reference = TReference::new_member(
1741 ascii_lowercase_atom("status"),
1742 TReferenceMemberSelector::Identifier(atom("Active")),
1743 );
1744 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1745
1746 let mut actual = input.clone();
1747 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1748
1749 assert_eq!(actual.types.len(), 1);
1750 assert!(matches!(&actual.types[0], TAtomic::Object(TObject::Enum(_))));
1751 }
1752
1753 #[test]
1754 fn test_expand_member_reference_unknown_class() {
1755 let codebase = CodebaseMetadata::new();
1756
1757 let reference = TReference::new_member(atom("NonExistent"), TReferenceMemberSelector::Identifier(atom("FOO")));
1758 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1759
1760 let mut actual = input.clone();
1761 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1762
1763 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
1764 }
1765
1766 #[test]
1767 fn test_expand_member_reference_unknown_member() {
1768 let code = r"<?php class Foo {}";
1769 let codebase = create_test_codebase(code);
1770
1771 let reference = TReference::new_member(
1772 ascii_lowercase_atom("foo"),
1773 TReferenceMemberSelector::Identifier(atom("NONEXISTENT")),
1774 );
1775 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1776
1777 let mut actual = input.clone();
1778 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1779
1780 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
1781 }
1782
1783 #[test]
1784 fn test_expand_member_reference_constant_with_inferred_type() {
1785 let code = r#"<?php
1786 class Foo {
1787 public const VALUE = "hello";
1788 }
1789 "#;
1790 let codebase = create_test_codebase(code);
1791
1792 let reference =
1793 TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("VALUE")));
1794 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1795
1796 let mut actual = input.clone();
1797 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1798
1799 assert_eq!(actual.types.len(), 1);
1800 }
1801
1802 #[test]
1803 fn test_expand_member_reference_constant_with_type_metadata() {
1804 let code = r"<?php
1805 class Foo {
1806 /** @var int */
1807 public const VALUE = 42;
1808 }
1809 ";
1810 let codebase = create_test_codebase(code);
1811
1812 let reference =
1813 TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("VALUE")));
1814 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1815
1816 let mut actual = input.clone();
1817 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1818
1819 assert_eq!(actual.types.len(), 1);
1820 }
1821
1822 #[test]
1823 fn test_expand_conditional_both_branches() {
1824 let code = r"<?php class Foo {} class Bar {}";
1825 let codebase = create_test_codebase(code);
1826
1827 let conditional = TConditional::new(
1828 Box::new(get_mixed()),
1829 Box::new(get_string()),
1830 Box::new(make_self_object()),
1831 Box::new(make_self_object()),
1832 false,
1833 );
1834 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
1835
1836 let options = options_with_self("Foo");
1837 let mut actual = input.clone();
1838 expand_union(&codebase, &mut actual, &options);
1839
1840 assert!(actual.types.iter().any(|t| {
1841 if let TAtomic::Object(TObject::Named(named)) = t {
1842 named.name == ascii_lowercase_atom("foo")
1843 } else {
1844 false
1845 }
1846 }));
1847 }
1848
1849 #[test]
1850 fn test_expand_conditional_with_self_in_then() {
1851 let code = r"<?php class Foo {}";
1852 let codebase = create_test_codebase(code);
1853
1854 let conditional = TConditional::new(
1855 Box::new(get_mixed()),
1856 Box::new(get_string()),
1857 Box::new(make_self_object()),
1858 Box::new(get_int()),
1859 false,
1860 );
1861 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
1862
1863 let options = options_with_self("Foo");
1864 let mut actual = input.clone();
1865 expand_union(&codebase, &mut actual, &options);
1866
1867 assert!(!actual.types.is_empty());
1868 }
1869
1870 #[test]
1871 fn test_expand_conditional_with_self_in_otherwise() {
1872 let code = r"<?php class Foo {}";
1873 let codebase = create_test_codebase(code);
1874
1875 let conditional = TConditional::new(
1876 Box::new(get_mixed()),
1877 Box::new(get_string()),
1878 Box::new(get_int()),
1879 Box::new(make_self_object()),
1880 false,
1881 );
1882 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
1883
1884 let options = options_with_self("Foo");
1885 let mut actual = input.clone();
1886 expand_union(&codebase, &mut actual, &options);
1887
1888 assert!(!actual.types.is_empty());
1889 }
1890
1891 #[test]
1892 fn test_expand_simple_alias() {
1893 let code = r"<?php
1894 class Foo {
1895 /** @phpstan-type MyInt = int */
1896 }
1897 ";
1898 let codebase = create_test_codebase(code);
1899
1900 let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("MyInt"));
1901 let input = TUnion::from_atomic(TAtomic::Alias(alias));
1902
1903 let mut actual = input.clone();
1904 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1905
1906 assert!(!actual.types.is_empty());
1907 }
1908
1909 #[test]
1910 fn test_expand_nested_alias() {
1911 let code = r"<?php
1912 class Foo {
1913 /** @phpstan-type Inner = int */
1914 /** @phpstan-type Outer = Inner */
1915 }
1916 ";
1917 let codebase = create_test_codebase(code);
1918
1919 let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("Outer"));
1920 let input = TUnion::from_atomic(TAtomic::Alias(alias));
1921
1922 let mut actual = input.clone();
1923 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1924
1925 assert!(!actual.types.is_empty());
1926 }
1927
1928 #[test]
1929 fn test_expand_alias_cycle_detection() {
1930 let codebase = CodebaseMetadata::new();
1931
1932 let alias = TAlias::new(atom("Foo"), atom("SelfRef"));
1933 let input = TUnion::from_atomic(TAtomic::Alias(alias.clone()));
1934
1935 let mut actual = input.clone();
1936 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1937
1938 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Alias(_))));
1939 }
1940
1941 #[test]
1942 fn test_expand_alias_unknown() {
1943 let codebase = CodebaseMetadata::new();
1944
1945 let alias = TAlias::new(atom("NonExistent"), atom("Unknown"));
1946 let input = TUnion::from_atomic(TAtomic::Alias(alias.clone()));
1947
1948 let mut actual = input.clone();
1949 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1950
1951 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Alias(_))));
1952 }
1953
1954 #[test]
1955 fn test_expand_alias_with_self_inside() {
1956 let code = r"<?php
1957 class Foo {
1958 /** @phpstan-type MySelf = self */
1959 }
1960 ";
1961 let codebase = create_test_codebase(code);
1962
1963 let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("MySelf"));
1964 let input = TUnion::from_atomic(TAtomic::Alias(alias));
1965
1966 let options = options_with_self("Foo");
1967 let mut actual = input.clone();
1968 expand_union(&codebase, &mut actual, &options);
1969
1970 assert!(!actual.types.is_empty());
1971 }
1972
1973 #[test]
1974 fn test_expand_key_of_array() {
1975 let codebase = CodebaseMetadata::new();
1976
1977 let mut keyed = TKeyedArray::new();
1978 keyed.parameters = Some((Box::new(get_string()), Box::new(get_int())));
1979 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1980
1981 let key_of = TKeyOf::new(Box::new(array_type));
1982 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
1983
1984 let mut actual = input.clone();
1985 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1986
1987 assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_string));
1988 }
1989
1990 #[test]
1991 fn test_expand_key_of_with_self() {
1992 let code = r"<?php class Foo {}";
1993 let codebase = create_test_codebase(code);
1994
1995 let mut keyed = TKeyedArray::new();
1996 keyed.parameters = Some((Box::new(make_self_object()), Box::new(get_int())));
1997 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1998
1999 let key_of = TKeyOf::new(Box::new(array_type));
2000 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
2001
2002 let options = options_with_self("Foo");
2003 let mut actual = input.clone();
2004 expand_union(&codebase, &mut actual, &options);
2005
2006 assert!(!actual.types.is_empty());
2007 }
2008
2009 #[test]
2010 fn test_expand_value_of_array() {
2011 let codebase = CodebaseMetadata::new();
2012
2013 let mut keyed = TKeyedArray::new();
2014 keyed.parameters = Some((Box::new(get_string()), Box::new(get_int())));
2015 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2016
2017 let value_of = TValueOf::new(Box::new(array_type));
2018 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
2019
2020 let mut actual = input.clone();
2021 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2022
2023 assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_int));
2024 }
2025
2026 #[test]
2027 fn test_expand_value_of_enum() {
2028 let code = r"<?php
2029 enum Status: string {
2030 case Active = 'active';
2031 case Inactive = 'inactive';
2032 }
2033 ";
2034 let codebase = create_test_codebase(code);
2035
2036 let enum_type = TUnion::from_atomic(TAtomic::Object(TObject::Enum(TEnum::new(ascii_lowercase_atom("status")))));
2037
2038 let value_of = TValueOf::new(Box::new(enum_type));
2039 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
2040
2041 let mut actual = input.clone();
2042 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2043
2044 assert!(!actual.types.is_empty());
2045 }
2046
2047 #[test]
2048 fn test_expand_index_access() {
2049 let codebase = CodebaseMetadata::new();
2050
2051 use crate::ttype::atomic::array::key::ArrayKey;
2052 use std::collections::BTreeMap;
2053
2054 let mut keyed = TKeyedArray::new();
2055 let mut known_items = BTreeMap::new();
2056 known_items.insert(ArrayKey::String(atom("key")), (false, get_int()));
2057 keyed.known_items = Some(known_items);
2058 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2059
2060 use crate::ttype::get_literal_string;
2061 let index_type = get_literal_string(atom("key"));
2062
2063 let index_access = TIndexAccess::new(array_type, index_type);
2064 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2065
2066 let mut actual = input.clone();
2067 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2068
2069 assert!(!actual.types.is_empty());
2070 }
2071
2072 #[test]
2073 fn test_expand_index_access_with_self() {
2074 let code = r"<?php class Foo {}";
2075 let codebase = create_test_codebase(code);
2076
2077 use crate::ttype::atomic::array::key::ArrayKey;
2078 use std::collections::BTreeMap;
2079
2080 let mut keyed = TKeyedArray::new();
2081 let mut known_items = BTreeMap::new();
2082 known_items.insert(ArrayKey::String(atom("key")), (false, make_self_object()));
2083 keyed.known_items = Some(known_items);
2084 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2085
2086 use crate::ttype::get_literal_string;
2087 let index_type = get_literal_string(atom("key"));
2088
2089 let index_access = TIndexAccess::new(array_type, index_type);
2090 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2091
2092 let options = options_with_self("Foo");
2093 let mut actual = input.clone();
2094 expand_union(&codebase, &mut actual, &options);
2095
2096 assert!(!actual.types.is_empty());
2097 }
2098
2099 #[test]
2100 fn test_expand_iterable_key_type() {
2101 let code = r"<?php class Foo {}";
2102 let codebase = create_test_codebase(code);
2103
2104 let iterable = TIterable::new(Box::new(make_self_object()), Box::new(get_int()));
2105 let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2106
2107 let options = options_with_self("Foo");
2108 let mut actual = input.clone();
2109 expand_union(&codebase, &mut actual, &options);
2110
2111 if let TAtomic::Iterable(iter) = &actual.types[0] {
2112 assert!(iter.get_key_type().types.iter().any(|t| {
2113 if let TAtomic::Object(TObject::Named(named)) = t {
2114 named.name == ascii_lowercase_atom("foo")
2115 } else {
2116 false
2117 }
2118 }));
2119 }
2120 }
2121
2122 #[test]
2123 fn test_expand_iterable_value_type() {
2124 let code = r"<?php class Foo {}";
2125 let codebase = create_test_codebase(code);
2126
2127 let iterable = TIterable::new(Box::new(get_int()), Box::new(make_self_object()));
2128 let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2129
2130 let options = options_with_self("Foo");
2131 let mut actual = input.clone();
2132 expand_union(&codebase, &mut actual, &options);
2133
2134 if let TAtomic::Iterable(iter) = &actual.types[0] {
2135 assert!(iter.get_value_type().types.iter().any(|t| {
2136 if let TAtomic::Object(TObject::Named(named)) = t {
2137 named.name == ascii_lowercase_atom("foo")
2138 } else {
2139 false
2140 }
2141 }));
2142 }
2143 }
2144
2145 #[test]
2146 fn test_get_signature_of_function() {
2147 let code = r#"<?php
2148 function myFunc(int $a): string { return ""; }
2149 "#;
2150 let codebase = create_test_codebase(code);
2151
2152 let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc"));
2153
2154 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2155 assert!(sig.is_some());
2156
2157 let sig = sig.unwrap();
2158 assert_eq!(sig.get_parameters().len(), 1);
2159 assert!(sig.get_return_type().is_some());
2160 }
2161
2162 #[test]
2163 fn test_get_signature_of_method() {
2164 let code = r"<?php
2165 class Foo {
2166 public function bar(string $s): int { return 0; }
2167 }
2168 ";
2169 let codebase = create_test_codebase(code);
2170
2171 let id = FunctionLikeIdentifier::Method(ascii_lowercase_atom("foo"), ascii_lowercase_atom("bar"));
2172
2173 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2174 assert!(sig.is_some());
2175
2176 let sig = sig.unwrap();
2177 assert_eq!(sig.get_parameters().len(), 1);
2178 }
2179
2180 #[test]
2181 fn test_get_signature_of_closure() {
2182 let codebase = CodebaseMetadata::new();
2183
2184 let id = FunctionLikeIdentifier::Closure(FileId::new("test"), Position::new(0));
2185 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2186
2187 assert!(sig.is_none());
2188 }
2189
2190 #[test]
2191 fn test_get_atomic_of_function() {
2192 let code = r"<?php
2193 function myFunc(): void {}
2194 ";
2195 let codebase = create_test_codebase(code);
2196
2197 let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc"));
2198
2199 let atomic = get_atomic_of_function_like_identifier(&id, &codebase);
2200 assert!(atomic.is_some());
2201 assert!(matches!(atomic.unwrap(), TAtomic::Callable(TCallable::Signature(_))));
2202 }
2203
2204 #[test]
2205 fn test_get_signature_with_parameters() {
2206 let code = r"<?php
2207 function multiParam(int $a, string $b, ?float $c = null): bool { return true; }
2208 ";
2209 let codebase = create_test_codebase(code);
2210
2211 let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("multiparam"));
2212
2213 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2214 assert!(sig.is_some());
2215
2216 let sig = sig.unwrap();
2217 assert_eq!(sig.get_parameters().len(), 3);
2218
2219 let third_param = &sig.get_parameters()[2];
2220 assert!(third_param.has_default());
2221 }
2222
2223 #[test]
2224 fn test_expand_preserves_by_reference_flag() {
2225 let code = r"<?php class Foo {}";
2226 let codebase = create_test_codebase(code);
2227
2228 let mut input = make_self_object();
2229 input.flags.insert(UnionFlags::BY_REFERENCE);
2230
2231 let options = options_with_self("Foo");
2232 let mut actual = input.clone();
2233 expand_union(&codebase, &mut actual, &options);
2234
2235 assert!(actual.flags.contains(UnionFlags::BY_REFERENCE));
2236 }
2237
2238 #[test]
2239 fn test_expand_preserves_possibly_undefined_flag() {
2240 let code = r"<?php class Foo {}";
2241 let codebase = create_test_codebase(code);
2242
2243 let mut input = make_self_object();
2244 input.flags.insert(UnionFlags::POSSIBLY_UNDEFINED);
2245
2246 let options = options_with_self("Foo");
2247 let mut actual = input.clone();
2248 expand_union(&codebase, &mut actual, &options);
2249
2250 assert!(actual.flags.contains(UnionFlags::POSSIBLY_UNDEFINED));
2251 }
2252
2253 #[test]
2254 fn test_expand_multiple_self_in_union() {
2255 let code = r"<?php class Foo {}";
2256 let codebase = create_test_codebase(code);
2257
2258 let input = TUnion::from_vec(vec![
2259 TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))),
2260 TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))),
2261 ]);
2262
2263 let options = options_with_self("Foo");
2264 let mut actual = input.clone();
2265 expand_union(&codebase, &mut actual, &options);
2266
2267 assert!(actual.types.len() <= 2);
2268 }
2269
2270 #[test]
2271 fn test_expand_deeply_nested_types() {
2272 let code = r"<?php class Foo {}";
2273 let codebase = create_test_codebase(code);
2274
2275 let inner = TList::new(Box::new(make_self_object()));
2276 let middle = TList::new(Box::new(TUnion::from_atomic(TAtomic::Array(TArray::List(inner)))));
2277 let outer = TList::new(Box::new(TUnion::from_atomic(TAtomic::Array(TArray::List(middle)))));
2278 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(outer)));
2279
2280 let options = options_with_self("Foo");
2281 let mut actual = input.clone();
2282 expand_union(&codebase, &mut actual, &options);
2283
2284 if let TAtomic::Array(TArray::List(outer)) = &actual.types[0]
2285 && let TAtomic::Array(TArray::List(middle)) = &outer.element_type.types[0]
2286 && let TAtomic::Array(TArray::List(inner)) = &middle.element_type.types[0]
2287 {
2288 assert!(inner.element_type.types.iter().any(|t| {
2289 if let TAtomic::Object(TObject::Named(named)) = t {
2290 named.name == ascii_lowercase_atom("foo")
2291 } else {
2292 false
2293 }
2294 }));
2295 }
2296 }
2297
2298 #[test]
2299 fn test_expand_with_all_options_disabled() {
2300 let code = r"<?php class Foo {}";
2301 let codebase = create_test_codebase(code);
2302
2303 let input = make_self_object();
2304 let options = TypeExpansionOptions {
2305 self_class: None,
2306 static_class_type: StaticClassType::None,
2307 parent_class: None,
2308 evaluate_class_constants: false,
2309 evaluate_conditional_types: false,
2310 function_is_final: false,
2311 expand_generic: false,
2312 expand_templates: false,
2313 };
2314
2315 let mut actual = input.clone();
2316 expand_union(&codebase, &mut actual, &options);
2317
2318 assert!(actual.types.iter().any(|t| {
2319 if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("self") } else { false }
2320 }));
2321 }
2322
2323 #[test]
2324 fn test_expand_already_expanded_type() {
2325 let code = r"<?php class Foo {}";
2326 let codebase = create_test_codebase(code);
2327
2328 let input = make_named_object("Foo");
2329 let options = options_with_self("Foo");
2330
2331 let mut actual = input.clone();
2332 expand_union(&codebase, &mut actual, &options);
2333
2334 let mut actual2 = actual.clone();
2335 expand_union(&codebase, &mut actual2, &options);
2336
2337 assert_eq!(actual.types.as_ref(), actual2.types.as_ref());
2338 }
2339
2340 #[test]
2341 fn test_expand_complex_generic_class() {
2342 let code = r"<?php
2343 /**
2344 * @template T
2345 * @template U
2346 */
2347 class Container {}
2348 ";
2349 let codebase = create_test_codebase(code);
2350
2351 let named = TNamedObject::new_with_type_parameters(
2352 ascii_lowercase_atom("container"),
2353 Some(vec![make_self_object(), make_static_object()]),
2354 );
2355 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
2356
2357 let options = TypeExpansionOptions {
2358 self_class: Some(ascii_lowercase_atom("foo")),
2359 static_class_type: StaticClassType::Name(ascii_lowercase_atom("bar")),
2360 ..Default::default()
2361 };
2362
2363 let mut actual = input.clone();
2364 expand_union(&codebase, &mut actual, &options);
2365
2366 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
2367 && let Some(params) = &named.type_parameters
2368 {
2369 assert!(params[0].types.iter().any(|t| {
2370 if let TAtomic::Object(TObject::Named(named)) = t {
2371 named.name == ascii_lowercase_atom("foo")
2372 } else {
2373 false
2374 }
2375 }));
2376 assert!(params[1].types.iter().any(|t| {
2377 if let TAtomic::Object(TObject::Named(named)) = t {
2378 named.name == ascii_lowercase_atom("bar")
2379 } else {
2380 false
2381 }
2382 }));
2383 }
2384 }
2385}