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