1use std::borrow::Cow;
2use std::cell::RefCell;
3use std::sync::Arc;
4
5use std::collections::HashSet;
6
7use foldhash::fast::FixedState;
8use mago_word::Word;
9use mago_word::ascii_lowercase_word;
10
11use crate::identifier::function_like::FunctionLikeIdentifier;
12use crate::metadata::CodebaseMetadata;
13use crate::metadata::class_like::ClassLikeMetadata;
14use crate::metadata::function_like::FunctionLikeMetadata;
15use crate::ttype::TType;
16use crate::ttype::TypeRef;
17use crate::ttype::atomic::TAtomic;
18use crate::ttype::atomic::alias::TAlias;
19use crate::ttype::atomic::array::TArray;
20use crate::ttype::atomic::array::key::ArrayKey;
21use crate::ttype::atomic::callable::TCallable;
22use crate::ttype::atomic::callable::TCallableSignature;
23use crate::ttype::atomic::callable::parameter::TCallableParameter;
24use crate::ttype::atomic::derived::TDerived;
25use crate::ttype::atomic::derived::index_access::TIndexAccess;
26use crate::ttype::atomic::derived::int_mask::TIntMask;
27use crate::ttype::atomic::derived::int_mask_of::TIntMaskOf;
28use crate::ttype::atomic::derived::intersection::TDerivedIntersection;
29use crate::ttype::atomic::derived::key_of::TKeyOf;
30use crate::ttype::atomic::derived::new::TNew;
31use crate::ttype::atomic::derived::properties_of::TPropertiesOf;
32use crate::ttype::atomic::derived::template_type::TTemplateType;
33use crate::ttype::atomic::derived::value_of::TValueOf;
34use crate::ttype::atomic::generic::TGenericParameter;
35use crate::ttype::atomic::mixed::TMixed;
36use crate::ttype::atomic::object::TObject;
37use crate::ttype::atomic::object::named::TNamedObject;
38use crate::ttype::atomic::reference::TGlobalReferenceSelector;
39use crate::ttype::atomic::reference::TReference;
40use crate::ttype::atomic::reference::TReferenceMemberSelector;
41use crate::ttype::atomic::scalar::TScalar;
42use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
43use crate::ttype::atomic::scalar::int::TInteger;
44use crate::ttype::atomic::scalar::string::TString;
45use crate::ttype::atomic::scalar::string::TStringLiteral;
46use crate::ttype::combiner;
47use crate::ttype::union::TUnion;
48
49thread_local! {
50 pub(crate) static EXPANDING_ALIASES: RefCell<HashSet<(Word, Word), FixedState>> = const { RefCell::new(HashSet::with_hasher(FixedState::with_seed(0))) };
53
54 static EXPANDING_OBJECT_PARAMS: RefCell<HashSet<Word, FixedState>> = const { RefCell::new(HashSet::with_hasher(FixedState::with_seed(0))) };
56
57 static EXPANDING_CONSTANTS: RefCell<HashSet<(Word, Word), FixedState>> = const { RefCell::new(HashSet::with_hasher(FixedState::with_seed(0))) };
61}
62
63pub(crate) struct AliasExpansionGuard {
66 class_name: Word,
67 alias_name: Word,
68}
69
70impl AliasExpansionGuard {
71 #[must_use]
72 pub(crate) fn new(class_name: Word, alias_name: Word) -> Self {
73 EXPANDING_ALIASES.with(|set| set.borrow_mut().insert((class_name, alias_name)));
74 Self { class_name, alias_name }
75 }
76}
77
78impl Drop for AliasExpansionGuard {
79 fn drop(&mut self) {
80 EXPANDING_ALIASES.with(|set| set.borrow_mut().remove(&(self.class_name, self.alias_name)));
81 }
82}
83
84struct ObjectParamsExpansionGuard {
86 object_name: Word,
87}
88
89impl ObjectParamsExpansionGuard {
90 #[must_use]
91 fn try_new(object_name: Word) -> Option<Self> {
92 EXPANDING_OBJECT_PARAMS.with(|set| {
93 let mut set = set.borrow_mut();
94 if set.contains(&object_name) {
95 None
96 } else {
97 set.insert(object_name);
98 Some(Self { object_name })
99 }
100 })
101 }
102}
103
104impl Drop for ObjectParamsExpansionGuard {
105 fn drop(&mut self) {
106 EXPANDING_OBJECT_PARAMS.with(|set| set.borrow_mut().remove(&self.object_name));
107 }
108}
109
110struct ConstantExpansionGuard {
117 class_name: Word,
118 constant_name: Word,
119}
120
121impl ConstantExpansionGuard {
122 #[must_use]
123 fn try_new(class_name: Word, constant_name: Word) -> Option<Self> {
124 EXPANDING_CONSTANTS.with(|set| {
125 let mut set = set.borrow_mut();
126 if set.contains(&(class_name, constant_name)) {
127 None
128 } else {
129 set.insert((class_name, constant_name));
130 Some(Self { class_name, constant_name })
131 }
132 })
133 }
134}
135
136impl Drop for ConstantExpansionGuard {
137 fn drop(&mut self) {
138 EXPANDING_CONSTANTS.with(|set| set.borrow_mut().remove(&(self.class_name, self.constant_name)));
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
143pub enum StaticClassType {
144 #[default]
145 None,
146 Exact(Word),
148 Name(Word),
150 Object(TObject),
152 Generic(TGenericParameter),
153}
154
155#[derive(Debug, Default)]
156pub struct TypeExpansionOptions {
157 pub self_class: Option<Word>,
158 pub static_class_type: StaticClassType,
159 pub function_is_final: bool,
160 pub allow_mixin_static_rebind: bool,
165}
166
167pub fn expand_union(codebase: &CodebaseMetadata, return_type: &mut TUnion, options: &TypeExpansionOptions) {
170 if !return_type.is_expandable() {
171 return;
172 }
173
174 let mut types = std::mem::take(&mut return_type.types).into_owned();
175 let mut new_return_type_parts: Vec<TAtomic> = Vec::new();
176 let mut skip_mask: u64 = 0;
177
178 for (i, return_type_part) in types.iter_mut().enumerate() {
179 let mut skip_key = false;
180 expand_atomic(return_type_part, codebase, options, &mut skip_key, &mut new_return_type_parts);
181
182 if skip_key && i < 64 {
183 skip_mask |= 1u64 << i;
184 }
185 }
186
187 if skip_mask != 0 {
188 let mut idx = 0usize;
189 types.retain(|_| {
190 let retain = idx >= 64 || (skip_mask & (1u64 << idx)) == 0;
191 idx += 1;
192 retain
193 });
194
195 new_return_type_parts.append(&mut types);
196
197 if new_return_type_parts.is_empty() {
198 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
199 }
200
201 types = if new_return_type_parts.len() > 1 {
202 combiner::combine(new_return_type_parts, codebase, combiner::CombinerOptions::default())
203 } else {
204 new_return_type_parts
205 };
206 } else if types.len() > 1 {
207 types = combiner::combine(types, codebase, combiner::CombinerOptions::default());
208 }
209
210 return_type.types = Cow::Owned(types);
211}
212
213pub(crate) fn expand_atomic(
214 return_type_part: &mut TAtomic,
215 codebase: &CodebaseMetadata,
216 options: &TypeExpansionOptions,
217 skip_key: &mut bool,
218 new_return_type_parts: &mut Vec<TAtomic>,
219) {
220 match return_type_part {
221 TAtomic::Array(array_type) => match array_type {
222 TArray::Keyed(keyed_data) => {
223 if let Some((key_parameter, value_parameter)) = &mut keyed_data.parameters {
224 expand_union(codebase, Arc::make_mut(key_parameter), options);
225 expand_union(codebase, Arc::make_mut(value_parameter), options);
226 }
227
228 if let Some(known_items) = &mut keyed_data.known_items {
229 let needs_key_resolution = known_items.keys().any(|k| k.is_class_like_constant());
231
232 if needs_key_resolution {
233 let old_items = std::mem::take(known_items);
234 for (key, (is_optional, mut value_type)) in old_items {
235 expand_union(codebase, &mut value_type, options);
236 let resolved_key = resolve_array_key(key, codebase, options);
237 known_items.insert(resolved_key, (is_optional, value_type));
238 }
239 } else {
240 for (_, item_type) in known_items.values_mut() {
241 expand_union(codebase, item_type, options);
242 }
243 }
244 }
245 }
246 TArray::List(list_data) => {
247 expand_union(codebase, Arc::make_mut(&mut list_data.element_type), options);
248
249 if let Some(known_elements) = &mut list_data.known_elements {
250 for (_, element_type) in known_elements.values_mut() {
251 expand_union(codebase, element_type, options);
252 }
253 }
254 }
255 },
256 TAtomic::Object(object) => {
257 if let Some(parameter) = resolve_generic_static_type(object, codebase, options) {
258 *skip_key = true;
259 new_return_type_parts.push(TAtomic::GenericParameter(parameter));
260 } else {
261 expand_object(object, codebase, options);
262 }
263 }
264 TAtomic::Callable(TCallable::Signature(signature)) => {
265 if let Some(return_type) = signature.get_return_type_mut() {
266 expand_union(codebase, return_type, options);
267 }
268
269 for param in signature.get_parameters_mut() {
270 if let Some(param_type) = param.get_type_signature_mut() {
271 expand_union(codebase, param_type, options);
272 }
273 }
274
275 for constraint in &mut signature.constraints {
276 expand_union(codebase, Arc::make_mut(&mut constraint.input_type), options);
277 if !contains_parameter_variable(&constraint.parameter_type) {
278 expand_union(codebase, Arc::make_mut(&mut constraint.parameter_type), options);
279 }
280 }
281 }
282 TAtomic::GenericParameter(parameter) => {
283 expand_union(codebase, Arc::make_mut(&mut parameter.constraint), options);
284 }
285 TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) => {
286 let mut atomic_return_type_parts = vec![];
287 expand_atomic(Arc::make_mut(constraint), codebase, options, &mut false, &mut atomic_return_type_parts);
288
289 if !atomic_return_type_parts.is_empty() {
290 *Arc::make_mut(constraint) = atomic_return_type_parts.remove(0);
291 }
292 }
293 TAtomic::Reference(TReference::Member { class_like_name, member_selector }) => {
294 *skip_key = true;
295 expand_member_reference(*class_like_name, member_selector, codebase, options, new_return_type_parts);
296 }
297 TAtomic::Reference(TReference::Global { selector }) => {
298 *skip_key = true;
299 expand_global_reference(selector, codebase, options, new_return_type_parts);
300 }
301 TAtomic::Callable(TCallable::Alias(id)) => {
302 if let Some(value) = get_atomic_of_function_like_identifier(id, codebase) {
303 *skip_key = true;
304 new_return_type_parts.push(value);
305 }
306 }
307 TAtomic::Conditional(conditional) => {
308 *skip_key = true;
309
310 let mut then = (*conditional.then).clone();
311 let mut otherwise = (*conditional.otherwise).clone();
312
313 expand_union(codebase, &mut then, options);
314 expand_union(codebase, &mut otherwise, options);
315
316 new_return_type_parts.extend(then.types.into_owned());
317 new_return_type_parts.extend(otherwise.types.into_owned());
318 }
319 TAtomic::Alias(alias) => {
320 *skip_key = true;
321 new_return_type_parts.extend(expand_alias(alias, codebase, options));
322 }
323 TAtomic::Derived(derived) => {
324 *skip_key = true;
325 new_return_type_parts.extend(match derived {
326 TDerived::KeyOf(key_of) => expand_key_of(key_of, codebase, options),
327 TDerived::ValueOf(value_of) => expand_value_of(value_of, codebase, options),
328 TDerived::IndexAccess(index_access) => expand_index_access(index_access, codebase, options),
329 TDerived::IntMask(int_mask) => expand_int_mask(int_mask, codebase, options),
330 TDerived::IntMaskOf(int_mask_of) => expand_int_mask_of(int_mask_of, codebase, options),
331 TDerived::PropertiesOf(properties_of) => expand_properties_of(properties_of, codebase, options),
332 TDerived::New(new_type) => expand_new(new_type, codebase, options),
333 TDerived::TemplateType(template_type) => expand_template_type(template_type, codebase, options),
334 TDerived::Intersection(intersection) => expand_derived_intersection(intersection, codebase, options),
335 });
336 }
337 TAtomic::Iterable(iterable) => {
338 expand_union(codebase, Arc::make_mut(&mut iterable.key_type), options);
339 expand_union(codebase, Arc::make_mut(&mut iterable.value_type), options);
340 }
341 _ => {}
342 }
343}
344
345fn expand_derived_intersection(
346 intersection: &TDerivedIntersection,
347 codebase: &CodebaseMetadata,
348 options: &TypeExpansionOptions,
349) -> Vec<TAtomic> {
350 let mut base_type = intersection.get_base_type().clone();
351 expand_union(codebase, &mut base_type, options);
352 let mut results = base_type.types.into_owned();
353
354 for intersection_type in intersection.get_intersection_types().unwrap_or_default() {
355 let mut expanded_intersection = TUnion::from_atomic(intersection_type.clone());
356 expand_union(codebase, &mut expanded_intersection, options);
357
358 let mut next_results = Vec::with_capacity(results.len() * expanded_intersection.types.len());
359 for base in results {
360 for additional in expanded_intersection.types.as_ref() {
361 let mut result = base.clone();
362 if !result.add_intersection_type(additional.clone()) {
363 return vec![TAtomic::Derived(TDerived::Intersection(intersection.clone()))];
364 }
365 next_results.push(result);
366 }
367 }
368 results = next_results;
369 }
370
371 results
372}
373
374fn resolve_array_key(key: ArrayKey, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> ArrayKey {
381 let ArrayKey::ClassLikeConstant { class_like_name, constant_name } = key else {
382 return key;
383 };
384
385 let resolved_class_name = {
387 let name_lc = ascii_lowercase_word(class_like_name.as_bytes());
388 match name_lc.as_bytes() {
389 b"self" => options.self_class.unwrap_or(class_like_name),
390 b"static" | b"$this" => {
391 if let StaticClassType::Exact(name) | StaticClassType::Name(name) = &options.static_class_type {
392 *name
393 } else {
394 options.self_class.unwrap_or(class_like_name)
395 }
396 }
397 b"parent" => {
398 if let Some(self_class) = options.self_class
399 && let Some(class_metadata) = codebase.get_class_like(self_class.as_bytes())
400 && let Some(parent) = class_metadata.direct_parent_class
401 {
402 parent
403 } else {
404 class_like_name
405 }
406 }
407 _ => class_like_name,
408 }
409 };
410
411 let Some(class_like) = codebase.get_class_like(resolved_class_name.as_bytes()) else {
412 return ArrayKey::ClassLikeConstant { class_like_name, constant_name };
413 };
414
415 if let Some(constant) = class_like.constants.get(&constant_name)
417 && let Some(inferred) = &constant.inferred_type
418 {
419 match inferred {
420 TAtomic::Scalar(TScalar::Integer(TInteger::Literal(i))) => {
421 return ArrayKey::Integer(*i);
422 }
423 TAtomic::Scalar(TScalar::String(TString { literal: Some(TStringLiteral::Value(s)), .. })) => {
424 return ArrayKey::from_string(*s);
425 }
426 _ => {}
427 }
428 }
429
430 if let Some(enum_case) = class_like.enum_cases.get(&constant_name)
432 && let Some(value_type) = &enum_case.value_type
433 {
434 match value_type {
435 TAtomic::Scalar(TScalar::Integer(TInteger::Literal(i))) => {
436 return ArrayKey::Integer(*i);
437 }
438 TAtomic::Scalar(TScalar::String(TString { literal: Some(TStringLiteral::Value(s)), .. })) => {
439 return ArrayKey::from_string(*s);
440 }
441 _ => {}
442 }
443 }
444
445 ArrayKey::ClassLikeConstant { class_like_name, constant_name }
447}
448
449#[cold]
450fn expand_member_reference(
451 class_like_name: Word,
452 member_selector: &TReferenceMemberSelector,
453 codebase: &CodebaseMetadata,
454 options: &TypeExpansionOptions,
455 new_return_type_parts: &mut Vec<TAtomic>,
456) {
457 if let TReferenceMemberSelector::Identifier(member_name) = member_selector
458 && member_name.as_bytes().eq_ignore_ascii_case(b"class")
459 {
460 new_return_type_parts.push(TAtomic::Scalar(TScalar::literal_class_string(class_like_name)));
461 return;
462 }
463
464 let Some(class_like) = codebase.get_class_like(class_like_name.as_bytes()) else {
465 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
466 return;
467 };
468
469 for (constant_name, constant) in &class_like.constants {
470 if !member_selector.matches(*constant_name) {
471 continue;
472 }
473
474 if let Some(inferred_type) = constant.inferred_type.as_ref() {
475 let Some(_guard) = ConstantExpansionGuard::try_new(class_like_name, *constant_name) else {
476 new_return_type_parts.push(TAtomic::Never);
477 continue;
478 };
479
480 let mut inferred_type = inferred_type.clone();
481 let mut skip_inferred_type = false;
482 expand_atomic(&mut inferred_type, codebase, options, &mut skip_inferred_type, new_return_type_parts);
483
484 if !skip_inferred_type {
485 new_return_type_parts.push(inferred_type);
486 }
487 } else if let Some(type_metadata) = constant.type_metadata.as_ref() {
488 let mut constant_type = type_metadata.type_union.clone();
489 expand_union(codebase, &mut constant_type, options);
490 new_return_type_parts.extend(constant_type.types.into_owned());
491 } else {
492 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
493 }
494 }
495
496 for enum_case_name in class_like.enum_cases.keys() {
497 if !member_selector.matches(*enum_case_name) {
498 continue;
499 }
500 new_return_type_parts.push(TAtomic::Object(TObject::new_enum_case(class_like.original_name, *enum_case_name)));
501 }
502
503 if let TReferenceMemberSelector::Identifier(member_name) = member_selector
504 && let Some(type_alias) = class_like.type_aliases.get(member_name)
505 {
506 let mut alias_type = type_alias.type_union.clone();
507 expand_union(codebase, &mut alias_type, options);
508 new_return_type_parts.extend(alias_type.types.into_owned());
509 }
510
511 if new_return_type_parts.is_empty() {
512 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
513 }
514}
515
516fn expand_global_reference(
517 selector: &TGlobalReferenceSelector,
518 codebase: &CodebaseMetadata,
519 options: &TypeExpansionOptions,
520 new_return_type_parts: &mut Vec<TAtomic>,
521) {
522 for (constant_name, constant) in &codebase.constants {
523 if !selector.matches(*constant_name) {
524 continue;
525 }
526
527 if let Some(inferred_type) = constant.inferred_type.as_ref() {
528 let mut inferred_type = inferred_type.clone();
529 expand_union(codebase, &mut inferred_type, options);
530 new_return_type_parts.extend(inferred_type.types.into_owned());
531 } else if let Some(type_metadata) = constant.type_metadata.as_ref() {
532 let mut constant_type = type_metadata.type_union.clone();
533 expand_union(codebase, &mut constant_type, options);
534 new_return_type_parts.extend(constant_type.types.into_owned());
535 } else {
536 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
537 }
538 }
539
540 if new_return_type_parts.is_empty() {
541 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
542 }
543}
544
545fn expand_object(object: &mut TObject, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) {
546 resolve_special_class_names(object, codebase, options);
547
548 if let TObject::Named(named) = object
549 && named.intersection_types.is_none()
550 && let Some(class_metadata) = codebase.get_class_like(named.name.as_bytes())
551 && class_metadata.kind.is_enum()
552 {
553 *object = TObject::new_enum(class_metadata.original_name);
554 return;
555 }
556
557 let TObject::Named(named) = object else {
558 return;
559 };
560
561 let has_params = named.type_parameters.as_ref().is_some_and(|p| !p.is_empty());
562 let class_metadata = codebase.get_class_like(named.name.as_bytes());
563 let has_required_intersections =
564 class_metadata.map(|m| !m.require_extends.is_empty() || !m.require_implements.is_empty()).unwrap_or(false);
565 let needs_default_params = !has_params && class_metadata.map(|m| !m.template_types.is_empty()).unwrap_or(false);
566
567 if !has_params && !has_required_intersections && !needs_default_params {
568 return;
569 }
570
571 let Some(_guard) = ObjectParamsExpansionGuard::try_new(named.name) else {
572 return;
573 };
574
575 if has_required_intersections && let Some(class_metadata) = class_metadata {
576 for &required in class_metadata.require_extends.iter().chain(&class_metadata.require_implements) {
577 named.add_intersection_type(TAtomic::Object(TObject::Named(TNamedObject::new(required))));
578 }
579 }
580
581 expand_or_fill_type_parameters(named, codebase, options);
582}
583
584fn resolve_generic_static_type(
585 object: &TObject,
586 codebase: &CodebaseMetadata,
587 options: &TypeExpansionOptions,
588) -> Option<TGenericParameter> {
589 let StaticClassType::Generic(parameter) = &options.static_class_type else {
590 return None;
591 };
592 let TObject::Named(named) = object else {
593 return None;
594 };
595
596 let special = classify_special_class_name(named.name.as_bytes());
597 if matches!(special, SpecialClassName::None) && !named.is_static && !named.is_this {
598 return None;
599 }
600
601 if matches!(special, SpecialClassName::None)
602 && !generic_parameter_can_resolve_static(parameter, named.name, codebase)
603 {
604 return None;
605 }
606
607 let mut parameter = parameter.clone();
608 for intersection in named.intersection_types.iter().flatten() {
609 parameter.add_intersection_type(intersection.clone());
610 }
611
612 Some(parameter)
613}
614
615fn generic_parameter_can_resolve_static(
616 parameter: &TGenericParameter,
617 expected_class: Word,
618 codebase: &CodebaseMetadata,
619) -> bool {
620 parameter.constraint.types.iter().any(|atomic| match atomic {
621 TAtomic::Object(TObject::Named(object)) => {
622 codebase.is_instance_of(object.name.as_bytes(), expected_class.as_bytes())
623 }
624 TAtomic::Object(TObject::Enum(object)) => {
625 codebase.is_instance_of(object.name.as_bytes(), expected_class.as_bytes())
626 }
627 TAtomic::GenericParameter(parameter) => {
628 generic_parameter_can_resolve_static(parameter, expected_class, codebase)
629 }
630 _ => false,
631 })
632}
633
634#[derive(Copy, Clone, Eq, PartialEq)]
639enum SpecialClassName {
640 None,
641 SelfType,
642 Static,
643 Parent,
644 This,
645}
646
647#[inline]
648fn classify_special_class_name(name: &[u8]) -> SpecialClassName {
649 match name.len() {
650 4 => {
651 if name.eq_ignore_ascii_case(b"self") {
652 SpecialClassName::SelfType
653 } else {
654 SpecialClassName::None
655 }
656 }
657 5 => {
658 if name == b"$this" || name.eq_ignore_ascii_case(b"$this") {
659 SpecialClassName::This
660 } else {
661 SpecialClassName::None
662 }
663 }
664 6 => {
665 if name.eq_ignore_ascii_case(b"static") {
666 SpecialClassName::Static
667 } else if name.eq_ignore_ascii_case(b"parent") {
668 SpecialClassName::Parent
669 } else {
670 SpecialClassName::None
671 }
672 }
673 _ => SpecialClassName::None,
674 }
675}
676
677fn resolve_special_class_names(object: &mut TObject, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) {
679 let TObject::Named(named) = object else {
680 return;
681 };
682
683 let special = classify_special_class_name(named.name.as_bytes());
684 if matches!(special, SpecialClassName::None) && !named.is_static && !named.is_this {
685 return;
686 }
687
688 let needs_static_resolution = matches!(special, SpecialClassName::Static | SpecialClassName::This) || named.is_this;
689
690 if needs_static_resolution && let StaticClassType::Object(TObject::Enum(static_enum)) = &options.static_class_type {
691 *object = TObject::Enum(static_enum.clone());
692 return;
693 }
694
695 if matches!(special, SpecialClassName::None)
699 && named.is_static
700 && let StaticClassType::Object(TObject::Enum(static_enum)) = &options.static_class_type
701 && (codebase.is_instance_of(static_enum.name.as_bytes(), named.name.as_bytes())
702 || (options.allow_mixin_static_rebind && reaches_through_mixins(static_enum.name, named.name, codebase)))
703 {
704 *object = TObject::Enum(static_enum.clone());
705 return;
706 }
707
708 let TObject::Named(named) = object else {
709 return;
710 };
711
712 let was_this = named.is_this;
713 match special {
714 SpecialClassName::Static | SpecialClassName::This => {
715 resolve_static_type(named, was_this, false, codebase, options)
716 }
717 SpecialClassName::SelfType => {
718 if let Some(self_class) = options.self_class {
719 named.name = self_class;
720 }
721 }
722 SpecialClassName::Parent => {
723 if let Some(self_class) = options.self_class
724 && let Some(class_metadata) = codebase.get_class_like(self_class.as_bytes())
725 && let Some(parent) = class_metadata.direct_parent_class
726 {
727 named.name = parent;
728 }
729 }
730 SpecialClassName::None if named.is_static => resolve_static_type(named, was_this, true, codebase, options),
731 SpecialClassName::None => {}
732 }
733}
734
735fn resolve_static_type(
740 named: &mut TNamedObject,
741 is_this_type: bool,
742 check_compatibility: bool,
743 codebase: &CodebaseMetadata,
744 options: &TypeExpansionOptions,
745) {
746 match &options.static_class_type {
747 StaticClassType::Exact(static_class)
748 if !check_compatibility || codebase.is_instance_of(static_class.as_bytes(), named.name.as_bytes()) =>
749 {
750 named.name = *static_class;
751 named.is_static = false;
752 named.is_this = false;
753 }
754 StaticClassType::Object(TObject::Named(static_obj)) => {
755 let mut crosses_mixin = false;
759 if check_compatibility
760 && !codebase.is_instance_of(static_obj.name.as_bytes(), named.name.as_bytes())
761 && !intersection_object_names(static_obj)
762 .any(|name| codebase.is_instance_of(name.as_bytes(), named.name.as_bytes()))
763 {
764 crosses_mixin = options.allow_mixin_static_rebind
765 && (reaches_through_mixins(static_obj.name, named.name, codebase)
766 || intersection_object_names(static_obj)
767 .any(|name| reaches_through_mixins(name, named.name, codebase)));
768
769 if !crosses_mixin {
770 return;
771 }
772 }
773
774 if let Some(intersections) = &static_obj.intersection_types {
775 named.intersection_types.get_or_insert_with(Vec::new).extend(intersections.iter().cloned());
776 }
777
778 if crosses_mixin
782 || (static_obj.type_parameters.is_some() && should_use_static_type_params(named, static_obj, codebase))
783 {
784 named.type_parameters.clone_from(&static_obj.type_parameters);
785 }
786
787 named.name = static_obj.name;
788 let effectively_final = is_effectively_final(&static_obj.name, codebase, options);
789 named.is_static = !effectively_final;
790 named.is_this = !effectively_final && is_this_type;
791 }
792 StaticClassType::Name(static_class)
793 if (!check_compatibility || codebase.is_instance_of(static_class.as_bytes(), named.name.as_bytes())) =>
794 {
795 named.name = *static_class;
796 let effectively_final = is_effectively_final(static_class, codebase, options);
797 named.is_static = !effectively_final;
798 named.is_this = !effectively_final && is_this_type;
799 }
800 _ => {}
801 }
802}
803
804fn is_effectively_final(class_name: &Word, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> bool {
812 if options.function_is_final {
813 return true;
814 }
815
816 codebase.get_class_like(class_name.as_bytes()).is_some_and(|meta| meta.name_span.is_none() || meta.flags.is_final())
817}
818
819fn intersection_object_names(obj: &TNamedObject) -> impl Iterator<Item = Word> {
821 obj.intersection_types
822 .iter()
823 .flatten()
824 .filter_map(|t| if let TAtomic::Object(obj) = t { obj.get_name() } else { None })
825}
826
827fn reaches_through_mixins(class_name: Word, target_name: Word, codebase: &CodebaseMetadata) -> bool {
832 let Some(metadata) = codebase.get_class_like(class_name.as_bytes()) else {
833 return false;
834 };
835
836 let mut visited = HashSet::with_hasher(FixedState::with_seed(0));
839 let mut stack = Vec::new();
840 let mut current = metadata;
841 loop {
842 for (mixin_name, mixin_metadata) in direct_mixins(current, codebase) {
843 if codebase.is_instance_of(mixin_name.as_bytes(), target_name.as_bytes()) {
844 return true;
845 }
846
847 if let Some(mixin_metadata) = mixin_metadata
848 && !mixin_metadata.mixins.is_empty()
849 && visited.insert(mixin_name)
850 {
851 stack.push(mixin_metadata);
852 }
853 }
854
855 let Some(next) = stack.pop() else {
856 return false;
857 };
858 current = next;
859 }
860}
861
862fn direct_mixins<'ctx>(
866 metadata: &'ctx ClassLikeMetadata,
867 codebase: &'ctx CodebaseMetadata,
868) -> impl Iterator<Item = (Word, Option<&'ctx ClassLikeMetadata>)> {
869 metadata.mixins.iter().flat_map(|mixin| mixin.type_union.types.as_ref().iter()).flat_map(move |mixin_type| {
870 let atomics = match mixin_type {
871 TAtomic::GenericParameter(TGenericParameter { constraint, .. }) => constraint.types.as_ref(),
872 other => std::slice::from_ref(other),
873 };
874
875 atomics.iter().filter_map(move |atomic| {
876 let mixin_name = atomic.get_object_or_enum_name()?;
877 Some((mixin_name, codebase.get_class_like(mixin_name.as_bytes())))
878 })
879 })
880}
881
882fn should_use_static_type_params(named: &TNamedObject, static_obj: &TNamedObject, codebase: &CodebaseMetadata) -> bool {
885 let Some(current_params) = &named.type_parameters else {
886 return true;
887 };
888
889 let Some(class_metadata) = codebase.get_class_like(static_obj.name.as_bytes()) else {
890 return false;
891 };
892
893 let templates = &class_metadata.template_types;
894
895 current_params.len() == templates.len()
896 && current_params.iter().zip(templates.values()).all(|(current, template)| {
897 current.from_template_fallback()
898 || current == &template.constraint
899 || template.default.as_ref().is_some_and(|default| current == default)
900 })
901}
902
903fn expand_or_fill_type_parameters(
905 named: &mut TNamedObject,
906 codebase: &CodebaseMetadata,
907 options: &TypeExpansionOptions,
908) {
909 if let Some(class_metadata) = codebase.get_class_like(named.name.as_bytes()) {
910 let template_count = class_metadata.template_types.len();
911 let supplied_count = named.type_parameters.as_ref().map_or(0, Vec::len);
912
913 if supplied_count < template_count {
914 let mut params = named.type_parameters.take().unwrap_or_default();
915 params.extend(class_metadata.template_types.values().skip(supplied_count).map(|template| {
916 if let Some(default) = &template.default {
917 let mut default = default.clone();
918 default.set_from_template_default(true);
919 default
920 } else {
921 let mut constraint = template.constraint.clone();
922 constraint.set_from_unspecified_template(true);
923 constraint
924 }
925 }));
926 named.type_parameters = Some(params);
927 }
928 }
929
930 if let Some(params) = &mut named.type_parameters {
931 for param in params.iter_mut() {
932 expand_union(codebase, param, options);
933 }
934 }
935}
936
937#[must_use]
938pub fn get_signature_of_function_like_identifier(
939 function_like_identifier: &FunctionLikeIdentifier,
940 codebase: &CodebaseMetadata,
941) -> Option<TCallableSignature> {
942 get_signature_of_function_like_identifier_with_options(function_like_identifier, codebase, false)
943}
944
945#[must_use]
951pub fn get_parameter_dependent_signature_of_function_like_identifier(
952 function_like_identifier: &FunctionLikeIdentifier,
953 codebase: &CodebaseMetadata,
954) -> Option<TCallableSignature> {
955 get_signature_of_function_like_identifier_with_options(function_like_identifier, codebase, true)
956}
957
958fn get_signature_of_function_like_identifier_with_options(
959 function_like_identifier: &FunctionLikeIdentifier,
960 codebase: &CodebaseMetadata,
961 preserve_parameter_dependencies: bool,
962) -> Option<TCallableSignature> {
963 let (function_like_metadata, options) = match function_like_identifier {
964 FunctionLikeIdentifier::Function(name) => {
965 (codebase.get_function(name.as_bytes())?, TypeExpansionOptions::default())
966 }
967 FunctionLikeIdentifier::Closure(name) => (codebase.get_closure(name)?, TypeExpansionOptions::default()),
968 FunctionLikeIdentifier::Method(classlike_name, method_name) => (
969 codebase.get_declaring_method(classlike_name.as_bytes(), method_name.as_bytes())?,
970 TypeExpansionOptions {
971 self_class: Some(*classlike_name),
972 static_class_type: StaticClassType::Name(*classlike_name),
973 ..Default::default()
974 },
975 ),
976 };
977
978 Some(get_signature_of_function_like_metadata_with_options(
979 function_like_identifier,
980 function_like_metadata,
981 codebase,
982 &options,
983 preserve_parameter_dependencies,
984 ))
985}
986
987#[must_use]
988pub fn get_atomic_of_function_like_identifier(
989 function_like_identifier: &FunctionLikeIdentifier,
990 codebase: &CodebaseMetadata,
991) -> Option<TAtomic> {
992 let signature = get_signature_of_function_like_identifier(function_like_identifier, codebase)?;
993
994 Some(TAtomic::Callable(TCallable::Signature(signature)))
995}
996
997#[must_use]
998pub fn get_signature_of_function_like_metadata(
999 function_like_identifier: &FunctionLikeIdentifier,
1000 function_like_metadata: &FunctionLikeMetadata,
1001 codebase: &CodebaseMetadata,
1002 options: &TypeExpansionOptions,
1003) -> TCallableSignature {
1004 get_signature_of_function_like_metadata_with_options(
1005 function_like_identifier,
1006 function_like_metadata,
1007 codebase,
1008 options,
1009 false,
1010 )
1011}
1012
1013fn get_signature_of_function_like_metadata_with_options(
1014 function_like_identifier: &FunctionLikeIdentifier,
1015 function_like_metadata: &FunctionLikeMetadata,
1016 codebase: &CodebaseMetadata,
1017 options: &TypeExpansionOptions,
1018 preserve_parameter_dependencies: bool,
1019) -> TCallableSignature {
1020 let parameters: Vec<_> = function_like_metadata
1021 .parameters
1022 .iter()
1023 .map(|parameter_metadata| {
1024 let type_signature = if let Some(t) = parameter_metadata.get_type_metadata() {
1025 let mut t = t.type_union.clone();
1026 if !preserve_parameter_dependencies || !contains_parameter_variable(&t) {
1027 expand_union(codebase, &mut t, options);
1028 }
1029 Some(Arc::new(t))
1030 } else {
1031 None
1032 };
1033
1034 TCallableParameter::new(
1035 type_signature,
1036 parameter_metadata.flags.is_by_reference(),
1037 parameter_metadata.flags.is_variadic(),
1038 parameter_metadata.flags.has_default(),
1039 )
1040 .with_name(Some(*parameter_metadata.get_name()))
1041 })
1042 .collect();
1043
1044 let return_type = if let Some(type_metadata) = function_like_metadata.return_type_metadata.as_ref() {
1045 let mut return_type = type_metadata.type_union.clone();
1046 if !preserve_parameter_dependencies || !contains_parameter_variable(&return_type) {
1047 expand_union(codebase, &mut return_type, options);
1048 }
1049 Some(Arc::new(return_type))
1050 } else {
1051 None
1052 };
1053
1054 let is_closure = matches!(function_like_identifier, FunctionLikeIdentifier::Closure(_));
1055 TCallableSignature::new(function_like_metadata.flags.is_pure(), is_closure)
1056 .with_parameters(parameters)
1057 .with_return_type(return_type)
1058 .with_source(Some(*function_like_identifier))
1059}
1060
1061#[must_use]
1062pub fn contains_parameter_variable(union: &TUnion) -> bool {
1063 union.get_all_child_nodes().into_iter().any(|node| {
1064 matches!(
1065 node,
1066 TypeRef::Atomic(TAtomic::Variable(variable))
1067 if !variable.as_bytes().eq_ignore_ascii_case(b"$this")
1068 )
1069 })
1070}
1071
1072#[cold]
1073fn expand_key_of(
1074 return_type_key_of: &TKeyOf,
1075 codebase: &CodebaseMetadata,
1076 options: &TypeExpansionOptions,
1077) -> Vec<TAtomic> {
1078 let mut target_type = return_type_key_of.get_target_type().clone();
1079 expand_union(codebase, &mut target_type, options);
1080
1081 let Some(new_return_types) = TKeyOf::get_key_of_targets(&target_type.types, codebase, false) else {
1082 return vec![TAtomic::Derived(TDerived::KeyOf(return_type_key_of.clone()))];
1083 };
1084
1085 new_return_types.types.into_owned()
1086}
1087
1088#[cold]
1089fn expand_value_of(
1090 return_type_value_of: &TValueOf,
1091 codebase: &CodebaseMetadata,
1092 options: &TypeExpansionOptions,
1093) -> Vec<TAtomic> {
1094 let mut target_type = return_type_value_of.get_target_type().clone();
1095 expand_union(codebase, &mut target_type, options);
1096
1097 let Some(new_return_types) = TValueOf::get_value_of_targets(&target_type.types, codebase, false) else {
1098 return vec![TAtomic::Derived(TDerived::ValueOf(return_type_value_of.clone()))];
1099 };
1100
1101 new_return_types.types.into_owned()
1102}
1103
1104#[cold]
1105fn expand_index_access(
1106 return_type_index_access: &TIndexAccess,
1107 codebase: &CodebaseMetadata,
1108 options: &TypeExpansionOptions,
1109) -> Vec<TAtomic> {
1110 let mut target_type = return_type_index_access.get_target_type().clone();
1111 expand_union(codebase, &mut target_type, options);
1112
1113 let mut index_type = return_type_index_access.get_index_type().clone();
1114 expand_union(codebase, &mut index_type, options);
1115
1116 let Some(new_return_types) =
1117 TIndexAccess::get_indexed_access_result(&target_type.types, &index_type.types, codebase, false)
1118 else {
1119 return vec![TAtomic::Derived(TDerived::IndexAccess(return_type_index_access.clone()))];
1120 };
1121
1122 new_return_types.types.into_owned()
1123}
1124
1125#[cold]
1126fn expand_new(new_type: &TNew, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
1127 let mut target_type = new_type.get_target_type().clone();
1128 expand_union(codebase, &mut target_type, options);
1129
1130 let Some(new_return_types) = TNew::get_new_targets(&target_type.types, codebase) else {
1131 return vec![TAtomic::Derived(TDerived::New(new_type.clone()))];
1132 };
1133
1134 new_return_types.types.into_owned()
1135}
1136
1137#[cold]
1138fn expand_template_type(
1139 template_type: &TTemplateType,
1140 codebase: &CodebaseMetadata,
1141 options: &TypeExpansionOptions,
1142) -> Vec<TAtomic> {
1143 let mut expanded = template_type.clone();
1144 expand_union(codebase, expanded.get_object_mut(), options);
1145 expand_union(codebase, expanded.get_class_name_mut(), options);
1146 expand_union(codebase, expanded.get_template_name_mut(), options);
1147
1148 let Some(resolved) = expanded.resolve(codebase) else {
1149 return vec![TAtomic::Mixed(TMixed::new())];
1150 };
1151
1152 resolved.types.into_owned()
1153}
1154
1155#[cold]
1156fn expand_int_mask(int_mask: &TIntMask, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
1157 let mut literal_values = Vec::new();
1158
1159 for value in int_mask.get_values() {
1160 let mut expanded = value.clone();
1161 expand_union(codebase, &mut expanded, options);
1162
1163 if let Some(int_val) = expanded.get_single_literal_int_value() {
1164 literal_values.push(int_val);
1165 }
1166 }
1167
1168 if literal_values.is_empty() {
1169 return vec![TAtomic::Scalar(TScalar::int())];
1170 }
1171
1172 let combinations = TIntMask::calculate_mask_combinations(&literal_values);
1173 combinations.into_iter().map(|v| TAtomic::Scalar(TScalar::literal_int(v))).collect()
1174}
1175
1176#[cold]
1177fn expand_int_mask_of(
1178 int_mask_of: &TIntMaskOf,
1179 codebase: &CodebaseMetadata,
1180 options: &TypeExpansionOptions,
1181) -> Vec<TAtomic> {
1182 let mut target = int_mask_of.get_target_type().clone();
1183 expand_union(codebase, &mut target, options);
1184
1185 let mut literal_values = Vec::new();
1186 for atomic in target.types.iter() {
1187 if let Some(int_val) = atomic.get_literal_int_value() {
1188 literal_values.push(int_val);
1189 }
1190 }
1191
1192 if literal_values.is_empty() {
1193 return vec![TAtomic::Scalar(TScalar::int())];
1194 }
1195
1196 let combinations = TIntMask::calculate_mask_combinations(&literal_values);
1197 combinations.into_iter().map(|v| TAtomic::Scalar(TScalar::literal_int(v))).collect()
1198}
1199
1200#[cold]
1201fn expand_properties_of(
1202 properties_of: &TPropertiesOf,
1203 codebase: &CodebaseMetadata,
1204 options: &TypeExpansionOptions,
1205) -> Vec<TAtomic> {
1206 let mut target_type = properties_of.get_target_type().clone();
1207 expand_union(codebase, &mut target_type, options);
1208
1209 let Some(mut keyed_array) =
1210 TPropertiesOf::get_properties_of_targets(&target_type.types, codebase, properties_of.visibility(), false)
1211 else {
1212 return vec![TAtomic::Derived(TDerived::PropertiesOf(properties_of.clone()))];
1213 };
1214
1215 let mut skip_keyed_array = false;
1216 let mut expanded_parts = vec![];
1217 expand_atomic(&mut keyed_array, codebase, options, &mut skip_keyed_array, &mut expanded_parts);
1218 if skip_keyed_array {
1219 expanded_parts
1220 } else {
1221 expanded_parts.push(keyed_array);
1222 expanded_parts
1223 }
1224}
1225
1226#[cold]
1227fn expand_alias(alias: &TAlias, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
1228 let class_name = alias.get_class_name();
1229 let alias_name = alias.get_alias_name();
1230
1231 let is_cycle = EXPANDING_ALIASES.with(|set| set.borrow().contains(&(class_name, alias_name)));
1233
1234 if is_cycle {
1235 return vec![TAtomic::Alias(alias.clone())];
1236 }
1237
1238 let Some(mut expanded_union) = alias.resolve(codebase).cloned() else {
1239 return vec![TAtomic::Alias(alias.clone())];
1240 };
1241
1242 let _guard = AliasExpansionGuard::new(class_name, alias_name);
1243
1244 expand_union(codebase, &mut expanded_union, options);
1245
1246 expanded_union.types.into_owned()
1247}
1248
1249#[cfg(test)]
1250#[allow(clippy::unwrap_used, clippy::expect_used)]
1251mod tests {
1252 use super::*;
1253 use mago_allocator::LocalArena;
1254
1255 use std::borrow::Cow;
1256 use std::collections::HashSet;
1257 use std::sync::Arc;
1258
1259 use mago_database::Database;
1260 use mago_database::DatabaseReader;
1261 use mago_database::file::File;
1262
1263 use mago_names::resolver::NameResolver;
1264
1265 use mago_syntax::parser::parse_file;
1266 use mago_word::WordSet;
1267 use mago_word::word;
1268
1269 use crate::metadata::CodebaseMetadata;
1270 use crate::misc::GenericParent;
1271 use crate::populator::populate_codebase;
1272 use crate::reference::SymbolReferences;
1273 use crate::scanner::scan_program;
1274 use crate::ttype::atomic::array::TArray;
1275 use crate::ttype::atomic::array::keyed::TKeyedArray;
1276 use crate::ttype::atomic::array::list::TList;
1277 use crate::ttype::atomic::callable::TCallable;
1278 use crate::ttype::atomic::callable::TCallableSignature;
1279 use crate::ttype::atomic::callable::parameter::TCallableParameter;
1280 use crate::ttype::atomic::conditional::TConditional;
1281 use crate::ttype::atomic::derived::TDerived;
1282 use crate::ttype::atomic::derived::index_access::TIndexAccess;
1283 use crate::ttype::atomic::derived::key_of::TKeyOf;
1284 use crate::ttype::atomic::derived::value_of::TValueOf;
1285 use crate::ttype::atomic::generic::TGenericParameter;
1286 use crate::ttype::atomic::iterable::TIterable;
1287 use crate::ttype::atomic::object::r#enum::TEnum;
1288 use crate::ttype::atomic::object::named::TNamedObject;
1289 use crate::ttype::atomic::reference::TReference;
1290 use crate::ttype::atomic::reference::TReferenceMemberSelector;
1291 use crate::ttype::atomic::scalar::TScalar;
1292 use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
1293 use crate::ttype::atomic::scalar::class_like_string::TClassLikeStringKind;
1294 use crate::ttype::flags::UnionFlags;
1295 use crate::ttype::get_int;
1296 use crate::ttype::get_mixed;
1297 use crate::ttype::get_never;
1298 use crate::ttype::get_null;
1299 use crate::ttype::get_string;
1300 use crate::ttype::get_void;
1301
1302 fn create_test_codebase(code: &'static str) -> CodebaseMetadata {
1303 let file = File::ephemeral(Cow::Borrowed(b"code.php"), Cow::Borrowed(code.as_bytes()));
1304 let config =
1305 mago_database::DatabaseConfiguration::new(std::path::Path::new("/"), vec![], vec![], vec![], vec![])
1306 .into_static();
1307 let database = Database::single(file, config);
1308
1309 let mut codebase = CodebaseMetadata::new();
1310 let arena = LocalArena::new();
1311 for file in database.files() {
1312 let program = parse_file(&arena, &file);
1313 assert!(!program.has_errors(), "Parse failed: {:?}", program.errors);
1314 let resolved_names = NameResolver::new(&arena).resolve(program);
1315 let program_codebase =
1316 scan_program(&arena, &file, program, &resolved_names, mago_php_version::PHPVersion::LATEST);
1317
1318 codebase.extend(program_codebase);
1319 }
1320
1321 populate_codebase(&mut codebase, &mut SymbolReferences::new(), WordSet::default(), HashSet::default());
1322
1323 codebase
1324 }
1325
1326 fn options_with_self(self_class: &str) -> TypeExpansionOptions {
1327 TypeExpansionOptions { self_class: Some(ascii_lowercase_word(self_class.as_bytes())), ..Default::default() }
1328 }
1329
1330 fn options_with_static(static_class: &str) -> TypeExpansionOptions {
1331 TypeExpansionOptions {
1332 self_class: Some(ascii_lowercase_word(static_class.as_bytes())),
1333 static_class_type: StaticClassType::Name(ascii_lowercase_word(static_class.as_bytes())),
1334 ..Default::default()
1335 }
1336 }
1337
1338 fn options_with_static_object(object: TObject) -> TypeExpansionOptions {
1339 TypeExpansionOptions {
1340 self_class: object.get_name(),
1341 static_class_type: StaticClassType::Object(object),
1342 ..Default::default()
1343 }
1344 }
1345
1346 macro_rules! assert_expands_to {
1347 ($codebase:expr, $input:expr, $expected:expr) => {
1348 assert_expands_to!($codebase, $input, $expected, &TypeExpansionOptions::default())
1349 };
1350 ($codebase:expr, $input:expr, $expected:expr, $options:expr) => {{
1351 let mut actual = $input.clone();
1352 expand_union($codebase, &mut actual, $options);
1353 assert_eq!(
1354 actual.types.as_ref(),
1355 $expected.types.as_ref(),
1356 "Type expansion mismatch.\nInput: {:?}\nExpected: {:?}\nActual: {:?}",
1357 $input,
1358 $expected,
1359 actual
1360 );
1361 }};
1362 }
1363
1364 fn make_self_object() -> TUnion {
1365 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(word("self")))))
1366 }
1367
1368 fn make_static_object() -> TUnion {
1369 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(word("static")))))
1370 }
1371
1372 fn make_parent_object() -> TUnion {
1373 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(word("parent")))))
1374 }
1375
1376 fn make_named_object(name: &str) -> TUnion {
1377 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_word(name.as_bytes())))))
1378 }
1379
1380 #[test]
1381 fn test_expand_null_type() {
1382 let codebase = CodebaseMetadata::new();
1383 let null_type = get_null();
1384 assert_expands_to!(&codebase, null_type, get_null());
1385 }
1386
1387 #[test]
1388 fn test_expand_void_type() {
1389 let codebase = CodebaseMetadata::new();
1390 let void_type = get_void();
1391 assert_expands_to!(&codebase, void_type, get_void());
1392 }
1393
1394 #[test]
1395 fn test_expand_never_type() {
1396 let codebase = CodebaseMetadata::new();
1397 let never_type = get_never();
1398 assert_expands_to!(&codebase, never_type, get_never());
1399 }
1400
1401 #[test]
1402 fn test_expand_int_type() {
1403 let codebase = CodebaseMetadata::new();
1404 let int_type = get_int();
1405 assert_expands_to!(&codebase, int_type, get_int());
1406 }
1407
1408 #[test]
1409 fn test_expand_mixed_type() {
1410 let codebase = CodebaseMetadata::new();
1411 let mixed_type = get_mixed();
1412 assert_expands_to!(&codebase, mixed_type, get_mixed());
1413 }
1414
1415 #[test]
1416 fn test_expand_keyed_array_with_self_key() {
1417 let code = "<?php class Foo {}";
1418 let codebase = create_test_codebase(code);
1419
1420 let mut keyed = TKeyedArray::new();
1421 keyed.parameters = Some((Arc::new(make_self_object()), Arc::new(get_int())));
1422 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1423
1424 let options = options_with_self("Foo");
1425 let mut actual = input;
1426 expand_union(&codebase, &mut actual, &options);
1427
1428 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1429 && let Some((key, _)) = &keyed.parameters
1430 {
1431 assert!(key.types.iter().any(|t| {
1432 if let TAtomic::Object(TObject::Named(named)) = t {
1433 named.name == ascii_lowercase_word(b"foo")
1434 } else {
1435 false
1436 }
1437 }));
1438 }
1439 }
1440
1441 #[test]
1442 fn test_expand_keyed_array_with_self_value() {
1443 let code = "<?php class Foo {}";
1444 let codebase = create_test_codebase(code);
1445
1446 let mut keyed = TKeyedArray::new();
1447 keyed.parameters = Some((Arc::new(get_string()), Arc::new(make_self_object())));
1448 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1449
1450 let options = options_with_self("Foo");
1451 let mut actual = input;
1452 expand_union(&codebase, &mut actual, &options);
1453
1454 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1455 && let Some((_, value)) = &keyed.parameters
1456 {
1457 assert!(value.types.iter().any(|t| {
1458 if let TAtomic::Object(TObject::Named(named)) = t {
1459 named.name == ascii_lowercase_word(b"foo")
1460 } else {
1461 false
1462 }
1463 }));
1464 }
1465 }
1466
1467 #[test]
1468 fn test_expand_keyed_array_known_items() {
1469 let code = "<?php class Foo {}";
1470 let codebase = create_test_codebase(code);
1471
1472 use crate::ttype::atomic::array::key::ArrayKey;
1473 use std::collections::BTreeMap;
1474
1475 let mut keyed = TKeyedArray::new();
1476 let mut known_items = BTreeMap::new();
1477 known_items.insert(ArrayKey::String(word("key")), (false, make_self_object()));
1478 keyed.known_items = Some(known_items);
1479 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1480
1481 let options = options_with_self("Foo");
1482 let mut actual = input;
1483 expand_union(&codebase, &mut actual, &options);
1484
1485 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1486 && let Some(items) = &keyed.known_items
1487 {
1488 let (_, item_type) = items.get(&ArrayKey::String(word("key"))).unwrap();
1489 assert!(item_type.types.iter().any(|t| {
1490 if let TAtomic::Object(TObject::Named(named)) = t {
1491 named.name == ascii_lowercase_word(b"foo")
1492 } else {
1493 false
1494 }
1495 }));
1496 }
1497 }
1498
1499 #[test]
1500 fn test_expand_list_with_self_element() {
1501 let code = "<?php class Foo {}";
1502 let codebase = create_test_codebase(code);
1503
1504 let list = TList::new(Arc::new(make_self_object()));
1505 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1506
1507 let options = options_with_self("Foo");
1508 let mut actual = input;
1509 expand_union(&codebase, &mut actual, &options);
1510
1511 if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1512 assert!(list.element_type.types.iter().any(|t| {
1513 if let TAtomic::Object(TObject::Named(named)) = t {
1514 named.name == ascii_lowercase_word(b"foo")
1515 } else {
1516 false
1517 }
1518 }));
1519 }
1520 }
1521
1522 #[test]
1523 fn test_expand_list_known_elements() {
1524 let code = "<?php class Foo {}";
1525 let codebase = create_test_codebase(code);
1526
1527 use std::collections::BTreeMap;
1528
1529 let mut list = TList::new(Arc::new(get_mixed()));
1530 let mut known_elements = BTreeMap::new();
1531 known_elements.insert(0, (false, make_self_object()));
1532 list.known_elements = Some(known_elements);
1533 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1534
1535 let options = options_with_self("Foo");
1536 let mut actual = input;
1537 expand_union(&codebase, &mut actual, &options);
1538
1539 if let TAtomic::Array(TArray::List(list)) = &actual.types[0]
1540 && let Some(elements) = &list.known_elements
1541 {
1542 let (_, element_type) = elements.get(&0).unwrap();
1543 assert!(element_type.types.iter().any(|t| {
1544 if let TAtomic::Object(TObject::Named(named)) = t {
1545 named.name == ascii_lowercase_word(b"foo")
1546 } else {
1547 false
1548 }
1549 }));
1550 }
1551 }
1552
1553 #[test]
1554 fn test_expand_nested_array() {
1555 let code = "<?php class Foo {}";
1556 let codebase = create_test_codebase(code);
1557
1558 let inner_list = TList::new(Arc::new(make_self_object()));
1559 let inner_array = TUnion::from_atomic(TAtomic::Array(TArray::List(inner_list)));
1560
1561 let mut outer = TKeyedArray::new();
1562 outer.parameters = Some((Arc::new(make_self_object()), Arc::new(inner_array)));
1563 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(outer)));
1564
1565 let options = options_with_self("Foo");
1566 let mut actual = input;
1567 expand_union(&codebase, &mut actual, &options);
1568
1569 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1570 && let Some((key, value)) = &keyed.parameters
1571 {
1572 assert!(key.types.iter().any(|t| {
1573 if let TAtomic::Object(TObject::Named(named)) = t {
1574 named.name == ascii_lowercase_word(b"foo")
1575 } else {
1576 false
1577 }
1578 }));
1579 if let TAtomic::Array(TArray::List(inner)) = &value.types[0] {
1580 assert!(inner.element_type.types.iter().any(|t| {
1581 if let TAtomic::Object(TObject::Named(named)) = t {
1582 named.name == ascii_lowercase_word(b"foo")
1583 } else {
1584 false
1585 }
1586 }));
1587 }
1588 }
1589 }
1590
1591 #[test]
1592 fn test_expand_empty_array() {
1593 let codebase = CodebaseMetadata::new();
1594 let keyed = TKeyedArray::new();
1595 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed.clone())));
1596 let expected = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1597 assert_expands_to!(&codebase, input, expected);
1598 }
1599
1600 #[test]
1601 fn test_expand_non_empty_list() {
1602 let code = "<?php class Foo {}";
1603 let codebase = create_test_codebase(code);
1604
1605 let mut list = TList::new(Arc::new(make_self_object()));
1606 list.non_empty = true;
1607 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1608
1609 let options = options_with_self("Foo");
1610 let mut actual = input;
1611 expand_union(&codebase, &mut actual, &options);
1612
1613 if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1614 assert!(list.non_empty);
1615 assert!(list.element_type.types.iter().any(|t| {
1616 if let TAtomic::Object(TObject::Named(named)) = t {
1617 named.name == ascii_lowercase_word(b"foo")
1618 } else {
1619 false
1620 }
1621 }));
1622 }
1623 }
1624
1625 #[test]
1626 fn test_expand_self_to_class_name() {
1627 let code = "<?php class Foo {}";
1628 let codebase = create_test_codebase(code);
1629
1630 let input = make_self_object();
1631 let options = options_with_self("Foo");
1632 let mut actual = input;
1633 expand_union(&codebase, &mut actual, &options);
1634
1635 assert!(actual.types.iter().any(|t| {
1636 if let TAtomic::Object(TObject::Named(named)) = t {
1637 named.name == ascii_lowercase_word(b"foo")
1638 } else {
1639 false
1640 }
1641 }));
1642 }
1643
1644 #[test]
1645 fn test_expand_static_to_class_name() {
1646 let code = "<?php class Foo {}";
1647 let codebase = create_test_codebase(code);
1648
1649 let input = make_static_object();
1650 let options = options_with_static("Foo");
1651 let mut actual = input;
1652 expand_union(&codebase, &mut actual, &options);
1653
1654 assert!(actual.types.iter().any(|t| {
1655 if let TAtomic::Object(TObject::Named(named)) = t {
1656 named.name == ascii_lowercase_word(b"foo")
1657 } else {
1658 false
1659 }
1660 }));
1661 }
1662
1663 #[test]
1664 fn test_expand_static_with_object_type() {
1665 let code = "<?php class Foo {}";
1666 let codebase = create_test_codebase(code);
1667
1668 let input = make_static_object();
1669 let static_obj = TObject::Named(TNamedObject::new(ascii_lowercase_word(b"foo")));
1670 let options = options_with_static_object(static_obj);
1671 let mut actual = input;
1672 expand_union(&codebase, &mut actual, &options);
1673
1674 assert!(actual.types.iter().any(|t| {
1675 if let TAtomic::Object(TObject::Named(named)) = t {
1676 named.name == ascii_lowercase_word(b"foo") && named.is_static && !named.is_this
1677 } else {
1678 false
1679 }
1680 }));
1681 }
1682
1683 #[test]
1684 fn test_expand_static_replaces_defaulted_type_parameters_with_receiver_parameters() {
1685 let code = "<?php
1686 /**
1687 * @template TKey of array-key
1688 * @template TValue
1689 */
1690 final class Collection {}
1691 ";
1692 let codebase = create_test_codebase(code);
1693
1694 let mut input = TNamedObject::new_static(ascii_lowercase_word(b"collection"));
1695 let mut default_key = crate::ttype::get_arraykey();
1696 default_key.set_from_template_default(true);
1697 let mut default_value = crate::ttype::get_mixed();
1698 default_value.set_from_template_default(true);
1699 input.type_parameters = Some(vec![default_key, default_value]);
1700
1701 let receiver = TNamedObject::new_with_type_parameters(
1702 ascii_lowercase_word(b"collection"),
1703 Some(vec![crate::ttype::get_int(), crate::ttype::get_string()]),
1704 );
1705 let options = options_with_static_object(TObject::Named(receiver));
1706 let mut actual = TUnion::from_atomic(TAtomic::Object(TObject::Named(input)));
1707 expand_union(&codebase, &mut actual, &options);
1708
1709 let TAtomic::Object(TObject::Named(actual)) = &actual.types[0] else {
1710 panic!("expected a named object");
1711 };
1712 let parameters = actual.type_parameters.as_ref().expect("expected type parameters");
1713 assert_eq!(parameters, &[crate::ttype::get_int(), crate::ttype::get_string()]);
1714 }
1715
1716 #[test]
1717 fn test_expand_static_with_enum_type() {
1718 let code = "<?php enum Status { case Active; case Inactive; }";
1719 let codebase = create_test_codebase(code);
1720
1721 let input = make_static_object();
1722 let static_enum = TObject::Enum(TEnum::new(ascii_lowercase_word(b"status")));
1723 let options = options_with_static_object(static_enum);
1724 let mut actual = input;
1725 expand_union(&codebase, &mut actual, &options);
1726
1727 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1728 }
1729
1730 #[test]
1731 fn test_expand_parent_to_parent_class() {
1732 let code = "<?php
1733 class BaseClass {}
1734 class ChildClass extends BaseClass {}
1735 ";
1736 let codebase = create_test_codebase(code);
1737
1738 let input = make_parent_object();
1739 let options = options_with_self("ChildClass");
1740 let mut actual = input;
1741 expand_union(&codebase, &mut actual, &options);
1742
1743 assert!(actual.types.iter().any(|t| {
1744 if let TAtomic::Object(TObject::Named(named)) = t {
1745 named.name == ascii_lowercase_word(b"baseclass")
1746 } else {
1747 false
1748 }
1749 }));
1750 }
1751
1752 #[test]
1753 fn test_expand_parent_without_parent_class() {
1754 let code = "<?php class Foo {}";
1755 let codebase = create_test_codebase(code);
1756
1757 let input = make_parent_object();
1758 let options = options_with_self("Foo");
1759 let mut actual = input;
1760 expand_union(&codebase, &mut actual, &options);
1761
1762 assert!(actual.types.iter().any(|t| {
1763 if let TAtomic::Object(TObject::Named(named)) = t { named.name == word("parent") } else { false }
1764 }));
1765 }
1766
1767 #[test]
1768 fn test_expand_this_variable() {
1769 let code = "<?php class Foo {}";
1770 let codebase = create_test_codebase(code);
1771
1772 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new_this(word("$this")))));
1773 let options = options_with_static("Foo");
1774 let mut actual = input;
1775 expand_union(&codebase, &mut actual, &options);
1776
1777 assert!(actual.types.iter().any(|t| {
1778 if let TAtomic::Object(TObject::Named(named)) = t {
1779 named.name == ascii_lowercase_word(b"foo")
1780 } else {
1781 false
1782 }
1783 }));
1784 }
1785
1786 #[test]
1787 fn test_expand_this_with_final_function() {
1788 let code = "<?php class Foo {}";
1789 let codebase = create_test_codebase(code);
1790
1791 let input = make_static_object();
1792 let options = TypeExpansionOptions {
1793 self_class: Some(ascii_lowercase_word(b"foo")),
1794 static_class_type: StaticClassType::Name(ascii_lowercase_word(b"foo")),
1795 function_is_final: true,
1796 ..Default::default()
1797 };
1798 let mut actual = input;
1799 expand_union(&codebase, &mut actual, &options);
1800
1801 assert!(actual.types.iter().any(|t| {
1802 if let TAtomic::Object(TObject::Named(named)) = t {
1803 named.name == ascii_lowercase_word(b"foo") && !named.is_this
1804 } else {
1805 false
1806 }
1807 }));
1808 }
1809
1810 #[test]
1811 fn test_expand_static_to_generic_receiver() {
1812 let codebase = create_test_codebase("<?php class Base {}");
1813 let base = ascii_lowercase_word(b"base");
1814 let parameter = TGenericParameter::new(
1815 word("T"),
1816 Arc::new(TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(base))))),
1817 GenericParent::ClassLike(word("Consumer")),
1818 );
1819 let mut actual = TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new_static(base))));
1820
1821 expand_union(
1822 &codebase,
1823 &mut actual,
1824 &TypeExpansionOptions {
1825 self_class: Some(base),
1826 static_class_type: StaticClassType::Generic(parameter.clone()),
1827 ..Default::default()
1828 },
1829 );
1830
1831 assert_eq!(actual, TUnion::from_atomic(TAtomic::GenericParameter(parameter)));
1832 }
1833
1834 #[test]
1835 fn test_expand_object_with_type_parameters() {
1836 let code = "<?php class Container {}";
1837 let codebase = create_test_codebase(code);
1838
1839 let named =
1840 TNamedObject::new_with_type_parameters(ascii_lowercase_word(b"container"), Some(vec![make_self_object()]));
1841 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1842
1843 let options = options_with_self("Foo");
1844 let mut actual = input;
1845 expand_union(&codebase, &mut actual, &options);
1846
1847 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
1848 && let Some(params) = &named.type_parameters
1849 {
1850 assert!(params[0].types.iter().any(|t| {
1851 if let TAtomic::Object(TObject::Named(named)) = t {
1852 named.name == ascii_lowercase_word(b"foo")
1853 } else {
1854 false
1855 }
1856 }));
1857 }
1858 }
1859
1860 #[test]
1861 fn test_expand_object_marks_omitted_type_params_as_unspecified() {
1862 let code = "<?php
1863 /** @template T */
1864 class Container {}
1865 ";
1866 let codebase = create_test_codebase(code);
1867
1868 let named = TNamedObject::new(ascii_lowercase_word(b"container"));
1869 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1870
1871 let mut actual = input;
1872 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1873
1874 let TAtomic::Object(TObject::Named(named)) = &actual.types[0] else {
1875 panic!("Expected a named object");
1876 };
1877 let parameter = &named.type_parameters.as_ref().expect("Expected a filled type parameter")[0];
1878
1879 assert!(parameter.is_mixed());
1880 assert!(parameter.from_unspecified_template());
1881 assert!(!parameter.from_template_default());
1882 }
1883
1884 #[test]
1885 fn test_expand_object_keeps_declared_template_defaults_concrete() {
1886 let code = "<?php
1887 /** @template T = string */
1888 class Container {}
1889 ";
1890 let codebase = create_test_codebase(code);
1891
1892 let named = TNamedObject::new(ascii_lowercase_word(b"container"));
1893 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1894
1895 let mut actual = input;
1896 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1897
1898 let TAtomic::Object(TObject::Named(named)) = &actual.types[0] else {
1899 panic!("Expected a named object");
1900 };
1901 let parameter = &named.type_parameters.as_ref().expect("Expected a filled type parameter")[0];
1902
1903 assert!(parameter.is_string());
1904 assert!(parameter.from_template_default());
1905 assert!(!parameter.from_unspecified_template());
1906 }
1907
1908 #[test]
1909 fn test_expand_object_intersection_from_static() {
1910 let code = "<?php
1911 interface Stringable {}
1912 class Foo implements Stringable {}
1913 ";
1914 let codebase = create_test_codebase(code);
1915
1916 let input = make_static_object();
1917
1918 let mut static_named = TNamedObject::new(ascii_lowercase_word(b"foo"));
1919 static_named.intersection_types =
1920 Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_word(b"stringable"))))]);
1921 let static_obj = TObject::Named(static_named);
1922 let options = options_with_static_object(static_obj);
1923
1924 let mut actual = input;
1925 expand_union(&codebase, &mut actual, &options);
1926
1927 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1928 assert!(named.intersection_types.is_some());
1929 }
1930 }
1931
1932 #[test]
1933 fn test_expand_self_without_self_class_option() {
1934 let codebase = CodebaseMetadata::new();
1935
1936 let input = make_self_object();
1937 let mut actual = input;
1938 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1939
1940 assert!(actual.types.iter().any(|t| {
1941 if let TAtomic::Object(TObject::Named(named)) = t { named.name == word("self") } else { false }
1942 }));
1943 }
1944
1945 #[test]
1946 fn test_expand_callable_return_type() {
1947 let code = "<?php class Foo {}";
1948 let codebase = create_test_codebase(code);
1949
1950 let sig = TCallableSignature::new(false, false).with_return_type(Some(Arc::new(make_self_object())));
1951 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1952
1953 let options = options_with_self("Foo");
1954 let mut actual = input;
1955 expand_union(&codebase, &mut actual, &options);
1956
1957 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1958 && let Some(ret) = sig.get_return_type()
1959 {
1960 assert!(ret.types.iter().any(|t| {
1961 if let TAtomic::Object(TObject::Named(named)) = t {
1962 named.name == ascii_lowercase_word(b"foo")
1963 } else {
1964 false
1965 }
1966 }));
1967 }
1968 }
1969
1970 #[test]
1971 fn test_expand_callable_parameter_types() {
1972 let code = "<?php class Foo {}";
1973 let codebase = create_test_codebase(code);
1974
1975 let param = TCallableParameter::new(Some(Arc::new(make_self_object())), false, false, false);
1976 let sig = TCallableSignature::new(false, false).with_parameters(vec![param]);
1977 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1978
1979 let options = options_with_self("Foo");
1980 let mut actual = input;
1981 expand_union(&codebase, &mut actual, &options);
1982
1983 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1984 && let Some(param) = sig.get_parameters().first()
1985 && let Some(param_type) = param.get_type_signature()
1986 {
1987 assert!(param_type.types.iter().any(|t| {
1988 if let TAtomic::Object(TObject::Named(named)) = t {
1989 named.name == ascii_lowercase_word(b"foo")
1990 } else {
1991 false
1992 }
1993 }));
1994 }
1995 }
1996
1997 #[test]
1998 fn test_expand_callable_alias_to_function() {
1999 let code = "<?php
2000 function myFunc(): int { return 1; }
2001 ";
2002 let codebase = create_test_codebase(code);
2003
2004 let alias = TCallable::Alias(FunctionLikeIdentifier::Function(ascii_lowercase_word(b"myfunc")));
2005 let input = TUnion::from_atomic(TAtomic::Callable(alias));
2006
2007 let mut actual = input;
2008 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2009
2010 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
2011 }
2012
2013 #[test]
2014 fn test_expand_callable_alias_to_method() {
2015 let code = "<?php
2016 class Foo {
2017 public function bar(): int { return 1; }
2018 }
2019 ";
2020 let codebase = create_test_codebase(code);
2021
2022 let alias = TCallable::Alias(FunctionLikeIdentifier::Method(
2023 ascii_lowercase_word(b"foo"),
2024 ascii_lowercase_word(b"bar"),
2025 ));
2026 let input = TUnion::from_atomic(TAtomic::Callable(alias));
2027
2028 let mut actual = input;
2029 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2030
2031 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
2032 }
2033
2034 #[test]
2035 fn test_expand_callable_alias_unknown() {
2036 let codebase = CodebaseMetadata::new();
2037
2038 let alias = TCallable::Alias(FunctionLikeIdentifier::Function(word("nonexistent")));
2039 let input = TUnion::from_atomic(TAtomic::Callable(alias));
2040
2041 let mut actual = input;
2042 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2043
2044 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Alias(_)))));
2045 }
2046
2047 #[test]
2048 fn test_expand_closure_signature() {
2049 let code = "<?php class Foo {}";
2050 let codebase = create_test_codebase(code);
2051
2052 let sig = TCallableSignature::new(false, true).with_return_type(Some(Arc::new(make_self_object())));
2053 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
2054
2055 let options = options_with_self("Foo");
2056 let mut actual = input;
2057 expand_union(&codebase, &mut actual, &options);
2058
2059 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
2060 && let Some(ret) = sig.get_return_type()
2061 {
2062 assert!(ret.types.iter().any(|t| {
2063 if let TAtomic::Object(TObject::Named(named)) = t {
2064 named.name == ascii_lowercase_word(b"foo")
2065 } else {
2066 false
2067 }
2068 }));
2069 }
2070 }
2071
2072 #[test]
2073 fn test_expand_generic_parameter_constraint() {
2074 let code = "<?php class Foo {}";
2075 let codebase = create_test_codebase(code);
2076
2077 let generic = TGenericParameter::new(
2078 word("T"),
2079 Arc::new(make_self_object()),
2080 GenericParent::ClassLike(ascii_lowercase_word(b"foo")),
2081 );
2082 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
2083
2084 let options = options_with_self("Foo");
2085 let mut actual = input;
2086 expand_union(&codebase, &mut actual, &options);
2087
2088 if let TAtomic::GenericParameter(param) = &actual.types[0] {
2089 assert!(param.constraint.types.iter().any(|t| {
2090 if let TAtomic::Object(TObject::Named(named)) = t {
2091 named.name == ascii_lowercase_word(b"foo")
2092 } else {
2093 false
2094 }
2095 }));
2096 }
2097 }
2098
2099 #[test]
2100 fn test_expand_nested_generic_constraint() {
2101 let code = "<?php class Foo {} class Bar {}";
2102 let codebase = create_test_codebase(code);
2103
2104 let container =
2105 TNamedObject::new_with_type_parameters(ascii_lowercase_word(b"container"), Some(vec![make_self_object()]));
2106 let constraint = TUnion::from_atomic(TAtomic::Object(TObject::Named(container)));
2107
2108 let generic = TGenericParameter::new(
2109 word("T"),
2110 Arc::new(constraint),
2111 GenericParent::ClassLike(ascii_lowercase_word(b"bar")),
2112 );
2113 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
2114
2115 let options = options_with_self("Foo");
2116 let mut actual = input;
2117 expand_union(&codebase, &mut actual, &options);
2118
2119 if let TAtomic::GenericParameter(param) = &actual.types[0]
2120 && let TAtomic::Object(TObject::Named(named)) = ¶m.constraint.types[0]
2121 && let Some(params) = &named.type_parameters
2122 {
2123 assert!(params[0].types.iter().any(|t| {
2124 if let TAtomic::Object(TObject::Named(named)) = t {
2125 named.name == ascii_lowercase_word(b"foo")
2126 } else {
2127 false
2128 }
2129 }));
2130 }
2131 }
2132
2133 #[test]
2134 fn test_expand_generic_with_intersection() {
2135 let code = "<?php
2136 interface Stringable {}
2137 class Foo {}
2138 ";
2139 let codebase = create_test_codebase(code);
2140
2141 let mut generic = TGenericParameter::new(
2142 word("T"),
2143 Arc::new(make_self_object()),
2144 GenericParent::ClassLike(ascii_lowercase_word(b"foo")),
2145 );
2146 generic.intersection_types =
2147 Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_word(b"stringable"))))]);
2148 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
2149
2150 let options = options_with_self("Foo");
2151 let mut actual = input;
2152 expand_union(&codebase, &mut actual, &options);
2153
2154 if let TAtomic::GenericParameter(param) = &actual.types[0] {
2155 assert!(param.intersection_types.is_some());
2156 assert!(param.constraint.types.iter().any(|t| {
2157 if let TAtomic::Object(TObject::Named(named)) = t {
2158 named.name == ascii_lowercase_word(b"foo")
2159 } else {
2160 false
2161 }
2162 }));
2163 }
2164 }
2165
2166 #[test]
2167 fn test_expand_class_string_of_self() {
2168 let code = "<?php class Foo {}";
2169 let codebase = create_test_codebase(code);
2170
2171 let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(word("self")))));
2172 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
2173 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
2174
2175 let options = options_with_self("Foo");
2176 let mut actual = input;
2177 expand_union(&codebase, &mut actual, &options);
2178
2179 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
2180 && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
2181 {
2182 assert_eq!(named.name, ascii_lowercase_word(b"foo"));
2183 }
2184 }
2185
2186 #[test]
2187 fn test_expand_class_string_of_static() {
2188 let code = "<?php class Foo {}";
2189 let codebase = create_test_codebase(code);
2190
2191 let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(word("static")))));
2192 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
2193 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
2194
2195 let options = options_with_static("Foo");
2196 let mut actual = input;
2197 expand_union(&codebase, &mut actual, &options);
2198
2199 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
2200 && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
2201 {
2202 assert_eq!(named.name, ascii_lowercase_word(b"foo"));
2203 }
2204 }
2205
2206 #[test]
2207 fn test_expand_interface_string_of_type() {
2208 let code = "<?php interface MyInterface {}";
2209 let codebase = create_test_codebase(code);
2210
2211 let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(word("self")))));
2212 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Interface, constraint };
2213 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
2214
2215 let options = options_with_self("MyInterface");
2216 let mut actual = input;
2217 expand_union(&codebase, &mut actual, &options);
2218
2219 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { kind, constraint })) =
2220 &actual.types[0]
2221 {
2222 assert!(matches!(kind, TClassLikeStringKind::Interface));
2223 if let TAtomic::Object(TObject::Named(named)) = constraint.as_ref() {
2224 assert_eq!(named.name, ascii_lowercase_word(b"myinterface"));
2225 }
2226 }
2227 }
2228
2229 #[test]
2230 fn test_expand_member_reference_wildcard_constants() {
2231 let code = "<?php
2232 class Foo {
2233 public const A = 1;
2234 public const B = 2;
2235 }
2236 ";
2237 let codebase = create_test_codebase(code);
2238
2239 let reference = TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::Wildcard);
2240 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2241
2242 let mut actual = input;
2243 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2244
2245 assert!(!actual.types.is_empty());
2246 }
2247
2248 #[test]
2249 fn test_expand_member_reference_wildcard_enum_cases() {
2250 let code = "<?php
2251 enum Status {
2252 case Active;
2253 case Inactive;
2254 }
2255 ";
2256 let codebase = create_test_codebase(code);
2257
2258 let reference = TReference::new_member(ascii_lowercase_word(b"status"), TReferenceMemberSelector::Wildcard);
2259 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2260
2261 let mut actual = input;
2262 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2263
2264 assert_eq!(actual.types.len(), 2);
2265 assert!(actual.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
2266 }
2267
2268 #[test]
2269 fn test_expand_member_reference_starts_with() {
2270 let code = "<?php
2271 class Foo {
2272 public const STATUS_ACTIVE = 1;
2273 public const STATUS_INACTIVE = 2;
2274 public const OTHER = 3;
2275 }
2276 ";
2277 let codebase = create_test_codebase(code);
2278
2279 let reference =
2280 TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::StartsWith(word("STATUS_")));
2281 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2282
2283 let mut actual = input;
2284 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2285
2286 assert!(!actual.types.is_empty());
2287 }
2288
2289 #[test]
2290 fn test_expand_member_reference_ends_with() {
2291 let code = "<?php
2292 class Foo {
2293 public const READ_ERROR = 1;
2294 public const WRITE_ERROR = 2;
2295 public const SUCCESS = 0;
2296 }
2297 ";
2298 let codebase = create_test_codebase(code);
2299
2300 let reference =
2301 TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::EndsWith(word("_ERROR")));
2302 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2303
2304 let mut actual = input;
2305 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2306
2307 assert!(!actual.types.is_empty());
2308 }
2309
2310 #[test]
2311 fn test_expand_member_reference_identifier_constant() {
2312 let code = "<?php
2313 class Foo {
2314 public const BAR = 42;
2315 }
2316 ";
2317 let codebase = create_test_codebase(code);
2318
2319 let reference =
2320 TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::Identifier(word("BAR")));
2321 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2322
2323 let mut actual = input;
2324 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2325
2326 assert_eq!(actual.types.len(), 1);
2327 }
2328
2329 #[test]
2330 fn test_expand_member_reference_identifier_enum_case() {
2331 let code = "<?php
2332 enum Status {
2333 case Active;
2334 }
2335 ";
2336 let codebase = create_test_codebase(code);
2337
2338 let reference = TReference::new_member(
2339 ascii_lowercase_word(b"status"),
2340 TReferenceMemberSelector::Identifier(word("Active")),
2341 );
2342 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2343
2344 let mut actual = input;
2345 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2346
2347 assert_eq!(actual.types.len(), 1);
2348 assert!(matches!(&actual.types[0], TAtomic::Object(TObject::Enum(_))));
2349 }
2350
2351 #[test]
2352 fn test_expand_member_reference_unknown_class() {
2353 let codebase = CodebaseMetadata::new();
2354
2355 let reference = TReference::new_member(word("NonExistent"), TReferenceMemberSelector::Identifier(word("FOO")));
2356 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2357
2358 let mut actual = input;
2359 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2360
2361 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
2362 }
2363
2364 #[test]
2365 fn test_expand_member_reference_unknown_member() {
2366 let code = "<?php class Foo {}";
2367 let codebase = create_test_codebase(code);
2368
2369 let reference = TReference::new_member(
2370 ascii_lowercase_word(b"foo"),
2371 TReferenceMemberSelector::Identifier(word("NONEXISTENT")),
2372 );
2373 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2374
2375 let mut actual = input;
2376 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2377
2378 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
2379 }
2380
2381 #[test]
2382 fn test_expand_member_reference_constant_with_inferred_type() {
2383 let code = r#"<?php
2384 class Foo {
2385 public const VALUE = "hello";
2386 }
2387 "#;
2388 let codebase = create_test_codebase(code);
2389
2390 let reference =
2391 TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::Identifier(word("VALUE")));
2392 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2393
2394 let mut actual = input;
2395 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2396
2397 assert_eq!(actual.types.len(), 1);
2398 }
2399
2400 #[test]
2401 fn test_expand_member_reference_constant_with_type_metadata() {
2402 let code = "<?php
2403 class Foo {
2404 /** @var int */
2405 public const VALUE = 42;
2406 }
2407 ";
2408 let codebase = create_test_codebase(code);
2409
2410 let reference =
2411 TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::Identifier(word("VALUE")));
2412 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2413
2414 let mut actual = input;
2415 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2416
2417 assert_eq!(actual.types.len(), 1);
2418 }
2419
2420 #[test]
2421 fn test_expand_conditional_both_branches() {
2422 let code = "<?php class Foo {} class Bar {}";
2423 let codebase = create_test_codebase(code);
2424
2425 let conditional = TConditional::new(
2426 Arc::new(get_mixed()),
2427 Arc::new(get_string()),
2428 Arc::new(make_self_object()),
2429 Arc::new(make_self_object()),
2430 false,
2431 );
2432 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
2433
2434 let options = options_with_self("Foo");
2435 let mut actual = input;
2436 expand_union(&codebase, &mut actual, &options);
2437
2438 assert!(actual.types.iter().any(|t| {
2439 if let TAtomic::Object(TObject::Named(named)) = t {
2440 named.name == ascii_lowercase_word(b"foo")
2441 } else {
2442 false
2443 }
2444 }));
2445 }
2446
2447 #[test]
2448 fn test_expand_conditional_with_self_in_then() {
2449 let code = "<?php class Foo {}";
2450 let codebase = create_test_codebase(code);
2451
2452 let conditional = TConditional::new(
2453 Arc::new(get_mixed()),
2454 Arc::new(get_string()),
2455 Arc::new(make_self_object()),
2456 Arc::new(get_int()),
2457 false,
2458 );
2459 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
2460
2461 let options = options_with_self("Foo");
2462 let mut actual = input;
2463 expand_union(&codebase, &mut actual, &options);
2464
2465 assert!(!actual.types.is_empty());
2466 }
2467
2468 #[test]
2469 fn test_expand_conditional_with_self_in_otherwise() {
2470 let code = "<?php class Foo {}";
2471 let codebase = create_test_codebase(code);
2472
2473 let conditional = TConditional::new(
2474 Arc::new(get_mixed()),
2475 Arc::new(get_string()),
2476 Arc::new(get_int()),
2477 Arc::new(make_self_object()),
2478 false,
2479 );
2480 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
2481
2482 let options = options_with_self("Foo");
2483 let mut actual = input;
2484 expand_union(&codebase, &mut actual, &options);
2485
2486 assert!(!actual.types.is_empty());
2487 }
2488
2489 #[test]
2490 fn test_expand_simple_alias() {
2491 let code = "<?php
2492 class Foo {
2493 /** @phpstan-type MyInt = int */
2494 }
2495 ";
2496 let codebase = create_test_codebase(code);
2497
2498 let alias = TAlias::new(ascii_lowercase_word(b"foo"), word("MyInt"));
2499 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2500
2501 let mut actual = input;
2502 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2503
2504 assert!(!actual.types.is_empty());
2505 }
2506
2507 #[test]
2508 fn test_expand_nested_alias() {
2509 let code = "<?php
2510 class Foo {
2511 /** @phpstan-type Inner = int */
2512 /** @phpstan-type Outer = Inner */
2513 }
2514 ";
2515 let codebase = create_test_codebase(code);
2516
2517 let alias = TAlias::new(ascii_lowercase_word(b"foo"), word("Outer"));
2518 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2519
2520 let mut actual = input;
2521 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2522
2523 assert!(!actual.types.is_empty());
2524 }
2525
2526 #[test]
2527 fn test_expand_alias_cycle_detection() {
2528 let code = "<?php
2529 /** @phpstan-type SelfRef = int|array<int, SelfRef> */
2530 class Foo {}
2531 ";
2532 let codebase = create_test_codebase(code);
2533
2534 let alias = TAlias::new(ascii_lowercase_word(b"foo"), word("SelfRef"));
2535 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2536
2537 let mut actual = input;
2538 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2539
2540 assert!(!actual.types.is_empty());
2541 }
2542
2543 #[test]
2544 fn test_expand_alias_unknown() {
2545 let codebase = CodebaseMetadata::new();
2546
2547 let alias = TAlias::new(word("NonExistent"), word("Unknown"));
2548 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2549
2550 let mut actual = input;
2551 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2552
2553 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Alias(_))));
2554 }
2555
2556 #[test]
2557 fn test_expand_alias_direct_self_reference() {
2558 let code = "<?php
2559 /** @psalm-type SelfAlias = SelfAlias */
2560 class Foo {}
2561 ";
2562 let codebase = create_test_codebase(code);
2563
2564 let alias = TAlias::new(ascii_lowercase_word(b"foo"), word("SelfAlias"));
2565 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2566
2567 let options = TypeExpansionOptions::default();
2568 let mut actual = input;
2569 expand_union(&codebase, &mut actual, &options);
2570
2571 assert!(!actual.types.is_empty());
2572 }
2573
2574 #[test]
2575 fn test_expand_alias_with_self_inside() {
2576 let code = "<?php
2577 class Foo {
2578 /** @phpstan-type MySelf = self */
2579 }
2580 ";
2581 let codebase = create_test_codebase(code);
2582
2583 let alias = TAlias::new(ascii_lowercase_word(b"foo"), word("MySelf"));
2584 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2585
2586 let options = options_with_self("Foo");
2587 let mut actual = input;
2588 expand_union(&codebase, &mut actual, &options);
2589
2590 assert!(!actual.types.is_empty());
2591 }
2592
2593 #[test]
2594 fn test_expand_key_of_array() {
2595 let codebase = CodebaseMetadata::new();
2596
2597 let mut keyed = TKeyedArray::new();
2598 keyed.parameters = Some((Arc::new(get_string()), Arc::new(get_int())));
2599 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2600
2601 let key_of = TKeyOf::new(Arc::new(array_type));
2602 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
2603
2604 let mut actual = input;
2605 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2606
2607 assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_string));
2608 }
2609
2610 #[test]
2611 fn test_expand_key_of_with_self() {
2612 let code = "<?php class Foo {}";
2613 let codebase = create_test_codebase(code);
2614
2615 let mut keyed = TKeyedArray::new();
2616 keyed.parameters = Some((Arc::new(make_self_object()), Arc::new(get_int())));
2617 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2618
2619 let key_of = TKeyOf::new(Arc::new(array_type));
2620 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
2621
2622 let options = options_with_self("Foo");
2623 let mut actual = input;
2624 expand_union(&codebase, &mut actual, &options);
2625
2626 assert!(!actual.types.is_empty());
2627 }
2628
2629 #[test]
2630 fn test_expand_value_of_array() {
2631 let codebase = CodebaseMetadata::new();
2632
2633 let mut keyed = TKeyedArray::new();
2634 keyed.parameters = Some((Arc::new(get_string()), Arc::new(get_int())));
2635 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2636
2637 let value_of = TValueOf::new(Arc::new(array_type));
2638 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
2639
2640 let mut actual = input;
2641 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2642
2643 assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_int));
2644 }
2645
2646 #[test]
2647 fn test_expand_value_of_enum() {
2648 let code = "<?php
2649 enum Status: string {
2650 case Active = 'active';
2651 case Inactive = 'inactive';
2652 }
2653 ";
2654 let codebase = create_test_codebase(code);
2655
2656 let enum_type =
2657 TUnion::from_atomic(TAtomic::Object(TObject::Enum(TEnum::new(ascii_lowercase_word(b"status")))));
2658
2659 let value_of = TValueOf::new(Arc::new(enum_type));
2660 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
2661
2662 let mut actual = input;
2663 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2664
2665 assert!(!actual.types.is_empty());
2666 }
2667
2668 #[test]
2669 fn test_expand_index_access() {
2670 let codebase = CodebaseMetadata::new();
2671
2672 use crate::ttype::atomic::array::key::ArrayKey;
2673 use std::collections::BTreeMap;
2674
2675 let mut keyed = TKeyedArray::new();
2676 let mut known_items = BTreeMap::new();
2677 known_items.insert(ArrayKey::String(word("key")), (false, get_int()));
2678 keyed.known_items = Some(known_items);
2679 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2680
2681 use crate::ttype::get_literal_string;
2682 let index_type = get_literal_string(word("key"));
2683
2684 let index_access = TIndexAccess::new(array_type, index_type);
2685 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2686
2687 let mut actual = input;
2688 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2689
2690 assert!(!actual.types.is_empty());
2691 }
2692
2693 #[test]
2694 fn test_expand_index_access_with_self() {
2695 let code = "<?php class Foo {}";
2696 let codebase = create_test_codebase(code);
2697
2698 use crate::ttype::atomic::array::key::ArrayKey;
2699 use std::collections::BTreeMap;
2700
2701 let mut keyed = TKeyedArray::new();
2702 let mut known_items = BTreeMap::new();
2703 known_items.insert(ArrayKey::String(word("key")), (false, make_self_object()));
2704 keyed.known_items = Some(known_items);
2705 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2706
2707 use crate::ttype::get_literal_string;
2708 let index_type = get_literal_string(word("key"));
2709
2710 let index_access = TIndexAccess::new(array_type, index_type);
2711 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2712
2713 let options = options_with_self("Foo");
2714 let mut actual = input;
2715 expand_union(&codebase, &mut actual, &options);
2716
2717 assert!(!actual.types.is_empty());
2718 }
2719
2720 #[test]
2721 fn test_expand_iterable_key_type() {
2722 let code = "<?php class Foo {}";
2723 let codebase = create_test_codebase(code);
2724
2725 let iterable = TIterable::new(Arc::new(make_self_object()), Arc::new(get_int()));
2726 let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2727
2728 let options = options_with_self("Foo");
2729 let mut actual = input;
2730 expand_union(&codebase, &mut actual, &options);
2731
2732 if let TAtomic::Iterable(iter) = &actual.types[0] {
2733 assert!(iter.get_key_type().types.iter().any(|t| {
2734 if let TAtomic::Object(TObject::Named(named)) = t {
2735 named.name == ascii_lowercase_word(b"foo")
2736 } else {
2737 false
2738 }
2739 }));
2740 }
2741 }
2742
2743 #[test]
2744 fn test_expand_iterable_value_type() {
2745 let code = "<?php class Foo {}";
2746 let codebase = create_test_codebase(code);
2747
2748 let iterable = TIterable::new(Arc::new(get_int()), Arc::new(make_self_object()));
2749 let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2750
2751 let options = options_with_self("Foo");
2752 let mut actual = input;
2753 expand_union(&codebase, &mut actual, &options);
2754
2755 if let TAtomic::Iterable(iter) = &actual.types[0] {
2756 assert!(iter.get_value_type().types.iter().any(|t| {
2757 if let TAtomic::Object(TObject::Named(named)) = t {
2758 named.name == ascii_lowercase_word(b"foo")
2759 } else {
2760 false
2761 }
2762 }));
2763 }
2764 }
2765
2766 #[test]
2767 fn test_get_signature_of_function() {
2768 let code = r#"<?php
2769 function myFunc(int $a): string { return ""; }
2770 "#;
2771 let codebase = create_test_codebase(code);
2772
2773 let id = FunctionLikeIdentifier::Function(ascii_lowercase_word(b"myfunc"));
2774
2775 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2776 assert!(sig.is_some());
2777
2778 let sig = sig.unwrap();
2779 assert_eq!(sig.get_parameters().len(), 1);
2780 assert!(sig.get_return_type().is_some());
2781 }
2782
2783 #[test]
2784 fn test_get_signature_of_method() {
2785 let code = "<?php
2786 class Foo {
2787 public function bar(string $s): int { return 0; }
2788 }
2789 ";
2790 let codebase = create_test_codebase(code);
2791
2792 let id = FunctionLikeIdentifier::Method(ascii_lowercase_word(b"foo"), ascii_lowercase_word(b"bar"));
2793
2794 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2795 assert!(sig.is_some());
2796
2797 let sig = sig.unwrap();
2798 assert_eq!(sig.get_parameters().len(), 1);
2799 }
2800
2801 #[test]
2802 fn test_get_signature_of_closure() {
2803 let codebase = CodebaseMetadata::new();
2804
2805 let id = FunctionLikeIdentifier::Closure(word(b"{closure:test.php:1:1}"));
2806 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2807
2808 assert!(sig.is_none());
2809 }
2810
2811 #[test]
2812 fn test_get_atomic_of_function() {
2813 let code = "<?php
2814 function myFunc(): void {}
2815 ";
2816 let codebase = create_test_codebase(code);
2817
2818 let id = FunctionLikeIdentifier::Function(ascii_lowercase_word(b"myfunc"));
2819
2820 let atomic = get_atomic_of_function_like_identifier(&id, &codebase);
2821 assert!(atomic.is_some());
2822 assert!(matches!(atomic.unwrap(), TAtomic::Callable(TCallable::Signature(_))));
2823 }
2824
2825 #[test]
2826 fn test_get_signature_with_parameters() {
2827 let code = "<?php
2828 function multiParam(int $a, string $b, ?float $c = null): bool { return true; }
2829 ";
2830 let codebase = create_test_codebase(code);
2831
2832 let id = FunctionLikeIdentifier::Function(ascii_lowercase_word(b"multiparam"));
2833
2834 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2835 assert!(sig.is_some());
2836
2837 let sig = sig.unwrap();
2838 assert_eq!(sig.get_parameters().len(), 3);
2839
2840 let third_param = &sig.get_parameters()[2];
2841 assert!(third_param.has_default());
2842 }
2843
2844 #[test]
2845 fn test_expand_preserves_by_reference_flag() {
2846 let code = "<?php class Foo {}";
2847 let codebase = create_test_codebase(code);
2848
2849 let mut input = make_self_object();
2850 input.flags.insert(UnionFlags::BY_REFERENCE);
2851
2852 let options = options_with_self("Foo");
2853 let mut actual = input.clone();
2854 expand_union(&codebase, &mut actual, &options);
2855
2856 assert!(actual.flags.contains(UnionFlags::BY_REFERENCE));
2857 }
2858
2859 #[test]
2860 fn test_expand_preserves_possibly_undefined_flag() {
2861 let code = "<?php class Foo {}";
2862 let codebase = create_test_codebase(code);
2863
2864 let mut input = make_self_object();
2865 input.flags.insert(UnionFlags::POSSIBLY_UNDEFINED);
2866
2867 let options = options_with_self("Foo");
2868 let mut actual = input.clone();
2869 expand_union(&codebase, &mut actual, &options);
2870
2871 assert!(actual.flags.contains(UnionFlags::POSSIBLY_UNDEFINED));
2872 }
2873
2874 #[test]
2875 fn test_expand_multiple_self_in_union() {
2876 let code = "<?php class Foo {}";
2877 let codebase = create_test_codebase(code);
2878
2879 let input = TUnion::from_vec(vec![
2880 TAtomic::Object(TObject::Named(TNamedObject::new(word("self")))),
2881 TAtomic::Object(TObject::Named(TNamedObject::new(word("self")))),
2882 ]);
2883
2884 let options = options_with_self("Foo");
2885 let mut actual = input;
2886 expand_union(&codebase, &mut actual, &options);
2887
2888 assert!(actual.types.len() <= 2);
2889 }
2890
2891 #[test]
2892 fn test_expand_deeply_nested_types() {
2893 let code = "<?php class Foo {}";
2894 let codebase = create_test_codebase(code);
2895
2896 let inner = TList::new(Arc::new(make_self_object()));
2897 let middle = TList::new(Arc::new(TUnion::from_atomic(TAtomic::Array(TArray::List(inner)))));
2898 let outer = TList::new(Arc::new(TUnion::from_atomic(TAtomic::Array(TArray::List(middle)))));
2899 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(outer)));
2900
2901 let options = options_with_self("Foo");
2902 let mut actual = input;
2903 expand_union(&codebase, &mut actual, &options);
2904
2905 if let TAtomic::Array(TArray::List(outer)) = &actual.types[0]
2906 && let TAtomic::Array(TArray::List(middle)) = &outer.element_type.types[0]
2907 && let TAtomic::Array(TArray::List(inner)) = &middle.element_type.types[0]
2908 {
2909 assert!(inner.element_type.types.iter().any(|t| {
2910 if let TAtomic::Object(TObject::Named(named)) = t {
2911 named.name == ascii_lowercase_word(b"foo")
2912 } else {
2913 false
2914 }
2915 }));
2916 }
2917 }
2918
2919 #[test]
2920 fn test_expand_with_all_options_disabled() {
2921 let code = "<?php class Foo {}";
2922 let codebase = create_test_codebase(code);
2923
2924 let input = make_self_object();
2925 let options = TypeExpansionOptions {
2926 self_class: None,
2927 static_class_type: StaticClassType::None,
2928 function_is_final: false,
2929 allow_mixin_static_rebind: false,
2930 };
2931
2932 let mut actual = input;
2933 expand_union(&codebase, &mut actual, &options);
2934
2935 assert!(actual.types.iter().any(|t| {
2936 if let TAtomic::Object(TObject::Named(named)) = t { named.name == word("self") } else { false }
2937 }));
2938 }
2939
2940 #[test]
2941 fn test_expand_already_expanded_type() {
2942 let code = "<?php class Foo {}";
2943 let codebase = create_test_codebase(code);
2944
2945 let input = make_named_object("Foo");
2946 let options = options_with_self("Foo");
2947
2948 let mut actual = input;
2949 expand_union(&codebase, &mut actual, &options);
2950
2951 let mut actual2 = actual.clone();
2952 expand_union(&codebase, &mut actual2, &options);
2953
2954 assert_eq!(actual.types.as_ref(), actual2.types.as_ref());
2955 }
2956
2957 #[test]
2958 fn test_expand_complex_generic_class() {
2959 let code = "<?php
2960 /**
2961 * @template T
2962 * @template U
2963 */
2964 class Container {}
2965 ";
2966 let codebase = create_test_codebase(code);
2967
2968 let named = TNamedObject::new_with_type_parameters(
2969 ascii_lowercase_word(b"container"),
2970 Some(vec![make_self_object(), make_static_object()]),
2971 );
2972 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
2973
2974 let options = TypeExpansionOptions {
2975 self_class: Some(ascii_lowercase_word(b"foo")),
2976 static_class_type: StaticClassType::Name(ascii_lowercase_word(b"bar")),
2977 ..Default::default()
2978 };
2979
2980 let mut actual = input;
2981 expand_union(&codebase, &mut actual, &options);
2982
2983 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
2984 && let Some(params) = &named.type_parameters
2985 {
2986 assert!(params[0].types.iter().any(|t| {
2987 if let TAtomic::Object(TObject::Named(named)) = t {
2988 named.name == ascii_lowercase_word(b"foo")
2989 } else {
2990 false
2991 }
2992 }));
2993 assert!(params[1].types.iter().any(|t| {
2994 if let TAtomic::Object(TObject::Named(named)) = t {
2995 named.name == ascii_lowercase_word(b"bar")
2996 } else {
2997 false
2998 }
2999 }));
3000 }
3001 }
3002}