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