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