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