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