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