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