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