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 is_this = named.is_this;
485
486 match name_lc.as_str() {
487 "static" | "$this" => resolve_static_type(named, 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 is_this => resolve_static_type(named, true, codebase, options),
502 _ => {}
503 }
504}
505
506fn resolve_static_type(
509 named: &mut TNamedObject,
510 check_compatibility: bool,
511 codebase: &CodebaseMetadata,
512 options: &TypeExpansionOptions,
513) {
514 match &options.static_class_type {
515 StaticClassType::Object(TObject::Named(static_obj)) => {
516 if check_compatibility && !is_static_type_compatible(named, static_obj, codebase) {
517 return;
518 }
519
520 if let Some(intersections) = &static_obj.intersection_types {
521 named.intersection_types.get_or_insert_with(Vec::new).extend(intersections.iter().cloned());
522 }
523
524 if static_obj.type_parameters.is_some() && should_use_static_type_params(named, static_obj, codebase) {
525 named.type_parameters.clone_from(&static_obj.type_parameters);
526 }
527
528 named.name = static_obj.name;
529 named.is_this = !codebase.get_class_like(&static_obj.name).is_some_and(|meta| meta.flags.is_final());
531 }
532 StaticClassType::Name(static_class)
533 if (!check_compatibility || codebase.is_instance_of(static_class, &named.name)) =>
534 {
535 named.name = *static_class;
536 named.is_this = false;
537 }
538 _ => {}
539 }
540}
541
542fn is_static_type_compatible(named: &TNamedObject, static_obj: &TNamedObject, codebase: &CodebaseMetadata) -> bool {
544 codebase.is_instance_of(&static_obj.name, &named.name)
545 || static_obj
546 .intersection_types
547 .iter()
548 .flatten()
549 .filter_map(|t| if let TAtomic::Object(obj) = t { obj.get_name() } else { None })
550 .any(|name| codebase.is_instance_of(&name, &named.name))
551}
552
553fn should_use_static_type_params(named: &TNamedObject, static_obj: &TNamedObject, codebase: &CodebaseMetadata) -> bool {
556 let Some(current_params) = &named.type_parameters else {
557 return true;
558 };
559
560 let Some(class_metadata) = codebase.get_class_like(&static_obj.name) else {
561 return false;
562 };
563
564 let templates = &class_metadata.template_types;
565
566 current_params.len() == templates.len()
567 && current_params.iter().zip(templates.values()).all(|(current, template)| current == &template.constraint)
568}
569
570fn expand_or_fill_type_parameters(
572 named: &mut TNamedObject,
573 codebase: &CodebaseMetadata,
574 options: &TypeExpansionOptions,
575) {
576 if let Some(params) = &mut named.type_parameters
577 && !params.is_empty()
578 {
579 for param in params.iter_mut() {
580 expand_union(codebase, param, options);
581 }
582 return;
583 }
584
585 let Some(class_metadata) = codebase.get_class_like(&named.name) else {
586 return;
587 };
588
589 if class_metadata.template_types.is_empty() {
590 return;
591 }
592
593 let defaults: Vec<TUnion> =
594 class_metadata.template_types.values().map(|template| template.constraint.clone()).collect();
595
596 named.type_parameters = Some(defaults);
597}
598
599#[must_use]
600pub fn get_signature_of_function_like_identifier(
601 function_like_identifier: &FunctionLikeIdentifier,
602 codebase: &CodebaseMetadata,
603) -> Option<TCallableSignature> {
604 Some(match function_like_identifier {
605 FunctionLikeIdentifier::Function(name) => {
606 let function_like_metadata = codebase.get_function(name)?;
607
608 get_signature_of_function_like_metadata(
609 function_like_identifier,
610 function_like_metadata,
611 codebase,
612 &TypeExpansionOptions::default(),
613 )
614 }
615 FunctionLikeIdentifier::Closure(file_id, position) => {
616 let function_like_metadata = codebase.get_closure(file_id, position)?;
617
618 get_signature_of_function_like_metadata(
619 function_like_identifier,
620 function_like_metadata,
621 codebase,
622 &TypeExpansionOptions::default(),
623 )
624 }
625 FunctionLikeIdentifier::Method(classlike_name, method_name) => {
626 let function_like_metadata = codebase.get_declaring_method(classlike_name, method_name)?;
627
628 get_signature_of_function_like_metadata(
629 function_like_identifier,
630 function_like_metadata,
631 codebase,
632 &TypeExpansionOptions {
633 self_class: Some(*classlike_name),
634 static_class_type: StaticClassType::Name(*classlike_name),
635 ..Default::default()
636 },
637 )
638 }
639 })
640}
641
642#[must_use]
643pub fn get_atomic_of_function_like_identifier(
644 function_like_identifier: &FunctionLikeIdentifier,
645 codebase: &CodebaseMetadata,
646) -> Option<TAtomic> {
647 let signature = get_signature_of_function_like_identifier(function_like_identifier, codebase)?;
648
649 Some(TAtomic::Callable(TCallable::Signature(signature)))
650}
651
652#[must_use]
653pub fn get_signature_of_function_like_metadata(
654 function_like_identifier: &FunctionLikeIdentifier,
655 function_like_metadata: &FunctionLikeMetadata,
656 codebase: &CodebaseMetadata,
657 options: &TypeExpansionOptions,
658) -> TCallableSignature {
659 let parameters: Vec<_> = function_like_metadata
660 .parameters
661 .iter()
662 .map(|parameter_metadata| {
663 let type_signature = if let Some(t) = parameter_metadata.get_type_metadata() {
664 let mut t = t.type_union.clone();
665 expand_union(codebase, &mut t, options);
666 Some(Arc::new(t))
667 } else {
668 None
669 };
670
671 TCallableParameter::new(
672 type_signature,
673 parameter_metadata.flags.is_by_reference(),
674 parameter_metadata.flags.is_variadic(),
675 parameter_metadata.flags.has_default(),
676 )
677 })
678 .collect();
679
680 let return_type = if let Some(type_metadata) = function_like_metadata.return_type_metadata.as_ref() {
681 let mut return_type = type_metadata.type_union.clone();
682 expand_union(codebase, &mut return_type, options);
683 Some(Arc::new(return_type))
684 } else {
685 None
686 };
687
688 let mut signature = TCallableSignature::new(function_like_metadata.flags.is_pure(), true)
689 .with_parameters(parameters)
690 .with_return_type(return_type)
691 .with_source(Some(*function_like_identifier));
692
693 if let FunctionLikeIdentifier::Closure(file_id, closure_position) = function_like_identifier {
694 signature = signature.with_closure_location(Some((*file_id, *closure_position)));
695 }
696
697 signature
698}
699
700#[cold]
701fn expand_key_of(
702 return_type_key_of: &TKeyOf,
703 codebase: &CodebaseMetadata,
704 options: &TypeExpansionOptions,
705) -> Vec<TAtomic> {
706 let mut target_type = return_type_key_of.get_target_type().clone();
707 expand_union(codebase, &mut target_type, options);
708
709 let Some(new_return_types) = TKeyOf::get_key_of_targets(&target_type.types, codebase, false) else {
710 return vec![TAtomic::Derived(TDerived::KeyOf(return_type_key_of.clone()))];
711 };
712
713 new_return_types.types.into_owned()
714}
715
716#[cold]
717fn expand_value_of(
718 return_type_value_of: &TValueOf,
719 codebase: &CodebaseMetadata,
720 options: &TypeExpansionOptions,
721) -> Vec<TAtomic> {
722 let mut target_type = return_type_value_of.get_target_type().clone();
723 expand_union(codebase, &mut target_type, options);
724
725 let Some(new_return_types) = TValueOf::get_value_of_targets(&target_type.types, codebase, false) else {
726 return vec![TAtomic::Derived(TDerived::ValueOf(return_type_value_of.clone()))];
727 };
728
729 new_return_types.types.into_owned()
730}
731
732#[cold]
733fn expand_index_access(
734 return_type_index_access: &TIndexAccess,
735 codebase: &CodebaseMetadata,
736 options: &TypeExpansionOptions,
737) -> Vec<TAtomic> {
738 let mut target_type = return_type_index_access.get_target_type().clone();
739 expand_union(codebase, &mut target_type, options);
740
741 let mut index_type = return_type_index_access.get_index_type().clone();
742 expand_union(codebase, &mut index_type, options);
743
744 let Some(new_return_types) = TIndexAccess::get_indexed_access_result(&target_type.types, &index_type.types, false)
745 else {
746 return vec![TAtomic::Derived(TDerived::IndexAccess(return_type_index_access.clone()))];
747 };
748
749 new_return_types.types.into_owned()
750}
751
752#[cold]
753fn expand_int_mask(int_mask: &TIntMask, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
754 let mut literal_values = Vec::new();
755
756 for value in int_mask.get_values() {
757 let mut expanded = value.clone();
758 expand_union(codebase, &mut expanded, options);
759
760 if let Some(int_val) = expanded.get_single_literal_int_value() {
761 literal_values.push(int_val);
762 }
763 }
764
765 if literal_values.is_empty() {
766 return vec![TAtomic::Scalar(TScalar::int())];
767 }
768
769 let combinations = TIntMask::calculate_mask_combinations(&literal_values);
770 combinations.into_iter().map(|v| TAtomic::Scalar(TScalar::literal_int(v))).collect()
771}
772
773#[cold]
774fn expand_int_mask_of(
775 int_mask_of: &TIntMaskOf,
776 codebase: &CodebaseMetadata,
777 options: &TypeExpansionOptions,
778) -> Vec<TAtomic> {
779 let mut target = int_mask_of.get_target_type().clone();
780 expand_union(codebase, &mut target, options);
781
782 let mut literal_values = Vec::new();
783 for atomic in target.types.iter() {
784 if let Some(int_val) = atomic.get_literal_int_value() {
785 literal_values.push(int_val);
786 }
787 }
788
789 if literal_values.is_empty() {
790 return vec![TAtomic::Scalar(TScalar::int())];
791 }
792
793 let combinations = TIntMask::calculate_mask_combinations(&literal_values);
794 combinations.into_iter().map(|v| TAtomic::Scalar(TScalar::literal_int(v))).collect()
795}
796
797#[cold]
798fn expand_properties_of(
799 properties_of: &TPropertiesOf,
800 codebase: &CodebaseMetadata,
801 options: &TypeExpansionOptions,
802) -> Vec<TAtomic> {
803 let mut target_type = properties_of.get_target_type().clone();
804 expand_union(codebase, &mut target_type, options);
805
806 let Some(keyed_array) =
807 TPropertiesOf::get_properties_of_targets(&target_type.types, codebase, properties_of.visibility(), false)
808 else {
809 return vec![TAtomic::Derived(TDerived::PropertiesOf(properties_of.clone()))];
810 };
811
812 vec![keyed_array]
813}
814
815#[cold]
816fn expand_alias(alias: &TAlias, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
817 let class_name = alias.get_class_name();
818 let alias_name = alias.get_alias_name();
819
820 let is_cycle = EXPANDING_ALIASES.with(|set| set.borrow().contains(&(class_name, alias_name)));
822
823 if is_cycle {
824 return vec![TAtomic::Alias(alias.clone())];
825 }
826
827 let Some(mut expanded_union) = alias.resolve(codebase).cloned() else {
828 return vec![TAtomic::Alias(alias.clone())];
829 };
830
831 let _ = AliasExpansionGuard::new(class_name, alias_name);
832
833 expand_union(codebase, &mut expanded_union, options);
834
835 expanded_union.types.into_owned()
836}
837
838#[cfg(test)]
839mod tests {
840 use super::*;
841
842 use std::borrow::Cow;
843 use std::sync::Arc;
844
845 use bumpalo::Bump;
846
847 use mago_atom::atom;
848 use mago_database::Database;
849 use mago_database::DatabaseReader;
850 use mago_database::file::File;
851 use mago_database::file::FileId;
852 use mago_names::resolver::NameResolver;
853 use mago_span::Position;
854 use mago_syntax::parser::parse_file;
855
856 use crate::metadata::CodebaseMetadata;
857 use crate::misc::GenericParent;
858 use crate::populator::populate_codebase;
859 use crate::reference::SymbolReferences;
860 use crate::scanner::scan_program;
861 use crate::ttype::atomic::array::TArray;
862 use crate::ttype::atomic::array::keyed::TKeyedArray;
863 use crate::ttype::atomic::array::list::TList;
864 use crate::ttype::atomic::callable::TCallable;
865 use crate::ttype::atomic::callable::TCallableSignature;
866 use crate::ttype::atomic::callable::parameter::TCallableParameter;
867 use crate::ttype::atomic::conditional::TConditional;
868 use crate::ttype::atomic::derived::TDerived;
869 use crate::ttype::atomic::derived::index_access::TIndexAccess;
870 use crate::ttype::atomic::derived::key_of::TKeyOf;
871 use crate::ttype::atomic::derived::value_of::TValueOf;
872 use crate::ttype::atomic::generic::TGenericParameter;
873 use crate::ttype::atomic::iterable::TIterable;
874 use crate::ttype::atomic::object::r#enum::TEnum;
875 use crate::ttype::atomic::object::named::TNamedObject;
876 use crate::ttype::atomic::reference::TReference;
877 use crate::ttype::atomic::reference::TReferenceMemberSelector;
878 use crate::ttype::atomic::scalar::TScalar;
879 use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
880 use crate::ttype::atomic::scalar::class_like_string::TClassLikeStringKind;
881 use crate::ttype::flags::UnionFlags;
882 use crate::ttype::get_int;
883 use crate::ttype::get_mixed;
884 use crate::ttype::get_never;
885 use crate::ttype::get_null;
886 use crate::ttype::get_string;
887 use crate::ttype::get_void;
888
889 fn create_test_codebase(code: &'static str) -> CodebaseMetadata {
890 let file = File::ephemeral(Cow::Borrowed("code.php"), Cow::Borrowed(code));
891 let config =
892 mago_database::DatabaseConfiguration::new(std::path::Path::new("/"), vec![], vec![], vec![], vec![])
893 .into_static();
894 let database = Database::single(file, config);
895
896 let mut codebase = CodebaseMetadata::new();
897 let arena = Bump::new();
898 for file in database.files() {
899 let program = parse_file(&arena, &file);
900 assert!(!program.has_errors(), "Parse failed: {:?}", program.errors);
901 let resolved_names = NameResolver::new(&arena).resolve(program);
902 let program_codebase = scan_program(&arena, &file, program, &resolved_names);
903
904 codebase.extend(program_codebase);
905 }
906
907 populate_codebase(&mut codebase, &mut SymbolReferences::new(), Default::default(), Default::default());
908
909 codebase
910 }
911
912 fn options_with_self(self_class: &str) -> TypeExpansionOptions {
913 TypeExpansionOptions { self_class: Some(ascii_lowercase_atom(self_class)), ..Default::default() }
914 }
915
916 fn options_with_static(static_class: &str) -> TypeExpansionOptions {
917 TypeExpansionOptions {
918 self_class: Some(ascii_lowercase_atom(static_class)),
919 static_class_type: StaticClassType::Name(ascii_lowercase_atom(static_class)),
920 ..Default::default()
921 }
922 }
923
924 fn options_with_static_object(object: TObject) -> TypeExpansionOptions {
925 TypeExpansionOptions {
926 self_class: object.get_name(),
927 static_class_type: StaticClassType::Object(object),
928 ..Default::default()
929 }
930 }
931
932 macro_rules! assert_expands_to {
933 ($codebase:expr, $input:expr, $expected:expr) => {
934 assert_expands_to!($codebase, $input, $expected, &TypeExpansionOptions::default())
935 };
936 ($codebase:expr, $input:expr, $expected:expr, $options:expr) => {{
937 let mut actual = $input.clone();
938 expand_union($codebase, &mut actual, $options);
939 assert_eq!(
940 actual.types.as_ref(),
941 $expected.types.as_ref(),
942 "Type expansion mismatch.\nInput: {:?}\nExpected: {:?}\nActual: {:?}",
943 $input,
944 $expected,
945 actual
946 );
947 }};
948 }
949
950 fn make_self_object() -> TUnion {
951 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))))
952 }
953
954 fn make_static_object() -> TUnion {
955 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("static")))))
956 }
957
958 fn make_parent_object() -> TUnion {
959 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("parent")))))
960 }
961
962 fn make_named_object(name: &str) -> TUnion {
963 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom(name)))))
964 }
965
966 #[test]
967 fn test_expand_null_type() {
968 let codebase = CodebaseMetadata::new();
969 let null_type = get_null();
970 assert_expands_to!(&codebase, null_type, get_null());
971 }
972
973 #[test]
974 fn test_expand_void_type() {
975 let codebase = CodebaseMetadata::new();
976 let void_type = get_void();
977 assert_expands_to!(&codebase, void_type, get_void());
978 }
979
980 #[test]
981 fn test_expand_never_type() {
982 let codebase = CodebaseMetadata::new();
983 let never_type = get_never();
984 assert_expands_to!(&codebase, never_type, get_never());
985 }
986
987 #[test]
988 fn test_expand_int_type() {
989 let codebase = CodebaseMetadata::new();
990 let int_type = get_int();
991 assert_expands_to!(&codebase, int_type, get_int());
992 }
993
994 #[test]
995 fn test_expand_mixed_type() {
996 let codebase = CodebaseMetadata::new();
997 let mixed_type = get_mixed();
998 assert_expands_to!(&codebase, mixed_type, get_mixed());
999 }
1000
1001 #[test]
1002 fn test_expand_keyed_array_with_self_key() {
1003 let code = r"<?php class Foo {}";
1004 let codebase = create_test_codebase(code);
1005
1006 let mut keyed = TKeyedArray::new();
1007 keyed.parameters = Some((Arc::new(make_self_object()), Arc::new(get_int())));
1008 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1009
1010 let options = options_with_self("Foo");
1011 let mut actual = input.clone();
1012 expand_union(&codebase, &mut actual, &options);
1013
1014 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1015 && let Some((key, _)) = &keyed.parameters
1016 {
1017 assert!(key.types.iter().any(|t| {
1018 if let TAtomic::Object(TObject::Named(named)) = t {
1019 named.name == ascii_lowercase_atom("foo")
1020 } else {
1021 false
1022 }
1023 }));
1024 }
1025 }
1026
1027 #[test]
1028 fn test_expand_keyed_array_with_self_value() {
1029 let code = r"<?php class Foo {}";
1030 let codebase = create_test_codebase(code);
1031
1032 let mut keyed = TKeyedArray::new();
1033 keyed.parameters = Some((Arc::new(get_string()), Arc::new(make_self_object())));
1034 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1035
1036 let options = options_with_self("Foo");
1037 let mut actual = input.clone();
1038 expand_union(&codebase, &mut actual, &options);
1039
1040 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1041 && let Some((_, value)) = &keyed.parameters
1042 {
1043 assert!(value.types.iter().any(|t| {
1044 if let TAtomic::Object(TObject::Named(named)) = t {
1045 named.name == ascii_lowercase_atom("foo")
1046 } else {
1047 false
1048 }
1049 }));
1050 }
1051 }
1052
1053 #[test]
1054 fn test_expand_keyed_array_known_items() {
1055 let code = r"<?php class Foo {}";
1056 let codebase = create_test_codebase(code);
1057
1058 use crate::ttype::atomic::array::key::ArrayKey;
1059 use std::collections::BTreeMap;
1060
1061 let mut keyed = TKeyedArray::new();
1062 let mut known_items = BTreeMap::new();
1063 known_items.insert(ArrayKey::String(atom("key")), (false, make_self_object()));
1064 keyed.known_items = Some(known_items);
1065 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1066
1067 let options = options_with_self("Foo");
1068 let mut actual = input.clone();
1069 expand_union(&codebase, &mut actual, &options);
1070
1071 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1072 && let Some(items) = &keyed.known_items
1073 {
1074 let (_, item_type) = items.get(&ArrayKey::String(atom("key"))).unwrap();
1075 assert!(item_type.types.iter().any(|t| {
1076 if let TAtomic::Object(TObject::Named(named)) = t {
1077 named.name == ascii_lowercase_atom("foo")
1078 } else {
1079 false
1080 }
1081 }));
1082 }
1083 }
1084
1085 #[test]
1086 fn test_expand_list_with_self_element() {
1087 let code = r"<?php class Foo {}";
1088 let codebase = create_test_codebase(code);
1089
1090 let list = TList::new(Arc::new(make_self_object()));
1091 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1092
1093 let options = options_with_self("Foo");
1094 let mut actual = input.clone();
1095 expand_union(&codebase, &mut actual, &options);
1096
1097 if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1098 assert!(list.element_type.types.iter().any(|t| {
1099 if let TAtomic::Object(TObject::Named(named)) = t {
1100 named.name == ascii_lowercase_atom("foo")
1101 } else {
1102 false
1103 }
1104 }));
1105 }
1106 }
1107
1108 #[test]
1109 fn test_expand_list_known_elements() {
1110 let code = r"<?php class Foo {}";
1111 let codebase = create_test_codebase(code);
1112
1113 use std::collections::BTreeMap;
1114
1115 let mut list = TList::new(Arc::new(get_mixed()));
1116 let mut known_elements = BTreeMap::new();
1117 known_elements.insert(0, (false, make_self_object()));
1118 list.known_elements = Some(known_elements);
1119 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1120
1121 let options = options_with_self("Foo");
1122 let mut actual = input.clone();
1123 expand_union(&codebase, &mut actual, &options);
1124
1125 if let TAtomic::Array(TArray::List(list)) = &actual.types[0]
1126 && let Some(elements) = &list.known_elements
1127 {
1128 let (_, element_type) = elements.get(&0).unwrap();
1129 assert!(element_type.types.iter().any(|t| {
1130 if let TAtomic::Object(TObject::Named(named)) = t {
1131 named.name == ascii_lowercase_atom("foo")
1132 } else {
1133 false
1134 }
1135 }));
1136 }
1137 }
1138
1139 #[test]
1140 fn test_expand_nested_array() {
1141 let code = r"<?php class Foo {}";
1142 let codebase = create_test_codebase(code);
1143
1144 let inner_list = TList::new(Arc::new(make_self_object()));
1145 let inner_array = TUnion::from_atomic(TAtomic::Array(TArray::List(inner_list)));
1146
1147 let mut outer = TKeyedArray::new();
1148 outer.parameters = Some((Arc::new(make_self_object()), Arc::new(inner_array)));
1149 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(outer)));
1150
1151 let options = options_with_self("Foo");
1152 let mut actual = input.clone();
1153 expand_union(&codebase, &mut actual, &options);
1154
1155 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1156 && let Some((key, value)) = &keyed.parameters
1157 {
1158 assert!(key.types.iter().any(|t| {
1159 if let TAtomic::Object(TObject::Named(named)) = t {
1160 named.name == ascii_lowercase_atom("foo")
1161 } else {
1162 false
1163 }
1164 }));
1165 if let TAtomic::Array(TArray::List(inner)) = &value.types[0] {
1166 assert!(inner.element_type.types.iter().any(|t| {
1167 if let TAtomic::Object(TObject::Named(named)) = t {
1168 named.name == ascii_lowercase_atom("foo")
1169 } else {
1170 false
1171 }
1172 }));
1173 }
1174 }
1175 }
1176
1177 #[test]
1178 fn test_expand_empty_array() {
1179 let codebase = CodebaseMetadata::new();
1180 let keyed = TKeyedArray::new();
1181 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed.clone())));
1182 let expected = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1183 assert_expands_to!(&codebase, input, expected);
1184 }
1185
1186 #[test]
1187 fn test_expand_non_empty_list() {
1188 let code = r"<?php class Foo {}";
1189 let codebase = create_test_codebase(code);
1190
1191 let mut list = TList::new(Arc::new(make_self_object()));
1192 list.non_empty = true;
1193 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1194
1195 let options = options_with_self("Foo");
1196 let mut actual = input.clone();
1197 expand_union(&codebase, &mut actual, &options);
1198
1199 if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1200 assert!(list.non_empty);
1201 assert!(list.element_type.types.iter().any(|t| {
1202 if let TAtomic::Object(TObject::Named(named)) = t {
1203 named.name == ascii_lowercase_atom("foo")
1204 } else {
1205 false
1206 }
1207 }));
1208 }
1209 }
1210
1211 #[test]
1212 fn test_expand_self_to_class_name() {
1213 let code = r"<?php class Foo {}";
1214 let codebase = create_test_codebase(code);
1215
1216 let input = make_self_object();
1217 let options = options_with_self("Foo");
1218 let mut actual = input.clone();
1219 expand_union(&codebase, &mut actual, &options);
1220
1221 assert!(actual.types.iter().any(|t| {
1222 if let TAtomic::Object(TObject::Named(named)) = t {
1223 named.name == ascii_lowercase_atom("foo")
1224 } else {
1225 false
1226 }
1227 }));
1228 }
1229
1230 #[test]
1231 fn test_expand_static_to_class_name() {
1232 let code = r"<?php class Foo {}";
1233 let codebase = create_test_codebase(code);
1234
1235 let input = make_static_object();
1236 let options = options_with_static("Foo");
1237 let mut actual = input.clone();
1238 expand_union(&codebase, &mut actual, &options);
1239
1240 assert!(actual.types.iter().any(|t| {
1241 if let TAtomic::Object(TObject::Named(named)) = t {
1242 named.name == ascii_lowercase_atom("foo")
1243 } else {
1244 false
1245 }
1246 }));
1247 }
1248
1249 #[test]
1250 fn test_expand_static_with_object_type() {
1251 let code = r"<?php class Foo {}";
1252 let codebase = create_test_codebase(code);
1253
1254 let input = make_static_object();
1255 let static_obj = TObject::Named(TNamedObject::new(ascii_lowercase_atom("foo")));
1256 let options = options_with_static_object(static_obj);
1257 let mut actual = input.clone();
1258 expand_union(&codebase, &mut actual, &options);
1259
1260 assert!(actual.types.iter().any(|t| {
1261 if let TAtomic::Object(TObject::Named(named)) = t {
1262 named.name == ascii_lowercase_atom("foo") && named.is_this
1263 } else {
1264 false
1265 }
1266 }));
1267 }
1268
1269 #[test]
1270 fn test_expand_static_with_enum_type() {
1271 let code = r"<?php enum Status { case Active; case Inactive; }";
1272 let codebase = create_test_codebase(code);
1273
1274 let input = make_static_object();
1275 let static_enum = TObject::Enum(TEnum::new(ascii_lowercase_atom("status")));
1276 let options = options_with_static_object(static_enum);
1277 let mut actual = input.clone();
1278 expand_union(&codebase, &mut actual, &options);
1279
1280 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1281 }
1282
1283 #[test]
1284 fn test_expand_parent_to_parent_class() {
1285 let code = r"<?php
1286 class BaseClass {}
1287 class ChildClass extends BaseClass {}
1288 ";
1289 let codebase = create_test_codebase(code);
1290
1291 let input = make_parent_object();
1292 let options = options_with_self("ChildClass");
1293 let mut actual = input.clone();
1294 expand_union(&codebase, &mut actual, &options);
1295
1296 assert!(actual.types.iter().any(|t| {
1297 if let TAtomic::Object(TObject::Named(named)) = t {
1298 named.name == ascii_lowercase_atom("baseclass")
1299 } else {
1300 false
1301 }
1302 }));
1303 }
1304
1305 #[test]
1306 fn test_expand_parent_without_parent_class() {
1307 let code = r"<?php class Foo {}";
1308 let codebase = create_test_codebase(code);
1309
1310 let input = make_parent_object();
1311 let options = options_with_self("Foo");
1312 let mut actual = input.clone();
1313 expand_union(&codebase, &mut actual, &options);
1314
1315 assert!(actual.types.iter().any(|t| {
1316 if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("parent") } else { false }
1317 }));
1318 }
1319
1320 #[test]
1321 fn test_expand_this_variable() {
1322 let code = r"<?php class Foo {}";
1323 let codebase = create_test_codebase(code);
1324
1325 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new_this(atom("$this")))));
1326 let options = options_with_static("Foo");
1327 let mut actual = input.clone();
1328 expand_union(&codebase, &mut actual, &options);
1329
1330 assert!(actual.types.iter().any(|t| {
1331 if let TAtomic::Object(TObject::Named(named)) = t {
1332 named.name == ascii_lowercase_atom("foo")
1333 } else {
1334 false
1335 }
1336 }));
1337 }
1338
1339 #[test]
1340 fn test_expand_this_with_final_function() {
1341 let code = r"<?php class Foo {}";
1342 let codebase = create_test_codebase(code);
1343
1344 let input = make_static_object();
1345 let options = TypeExpansionOptions {
1346 self_class: Some(ascii_lowercase_atom("foo")),
1347 static_class_type: StaticClassType::Name(ascii_lowercase_atom("foo")),
1348 function_is_final: true,
1349 ..Default::default()
1350 };
1351 let mut actual = input.clone();
1352 expand_union(&codebase, &mut actual, &options);
1353
1354 assert!(actual.types.iter().any(|t| {
1355 if let TAtomic::Object(TObject::Named(named)) = t {
1356 named.name == ascii_lowercase_atom("foo") && !named.is_this
1357 } else {
1358 false
1359 }
1360 }));
1361 }
1362
1363 #[test]
1364 fn test_expand_object_with_type_parameters() {
1365 let code = r"<?php class Container {}";
1366 let codebase = create_test_codebase(code);
1367
1368 let named =
1369 TNamedObject::new_with_type_parameters(ascii_lowercase_atom("container"), Some(vec![make_self_object()]));
1370 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1371
1372 let options = options_with_self("Foo");
1373 let mut actual = input.clone();
1374 expand_union(&codebase, &mut actual, &options);
1375
1376 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
1377 && let Some(params) = &named.type_parameters
1378 {
1379 assert!(params[0].types.iter().any(|t| {
1380 if let TAtomic::Object(TObject::Named(named)) = t {
1381 named.name == ascii_lowercase_atom("foo")
1382 } else {
1383 false
1384 }
1385 }));
1386 }
1387 }
1388
1389 #[test]
1390 fn test_expand_object_gets_default_type_params() {
1391 let code = r"<?php
1392 /** @template T */
1393 class Container {}
1394 ";
1395 let codebase = create_test_codebase(code);
1396
1397 let named = TNamedObject::new(ascii_lowercase_atom("container"));
1398 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1399
1400 let mut actual = input.clone();
1401 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1402
1403 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1404 assert!(named.type_parameters.is_some());
1405 }
1406 }
1407
1408 #[test]
1409 fn test_expand_object_intersection_from_static() {
1410 let code = r"<?php
1411 interface Stringable {}
1412 class Foo implements Stringable {}
1413 ";
1414 let codebase = create_test_codebase(code);
1415
1416 let input = make_static_object();
1417
1418 let mut static_named = TNamedObject::new(ascii_lowercase_atom("foo"));
1419 static_named.intersection_types =
1420 Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom("stringable"))))]);
1421 let static_obj = TObject::Named(static_named);
1422 let options = options_with_static_object(static_obj);
1423
1424 let mut actual = input.clone();
1425 expand_union(&codebase, &mut actual, &options);
1426
1427 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1428 assert!(named.intersection_types.is_some());
1429 }
1430 }
1431
1432 #[test]
1433 fn test_expand_self_without_self_class_option() {
1434 let codebase = CodebaseMetadata::new();
1435
1436 let input = make_self_object();
1437 let mut actual = input.clone();
1438 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1439
1440 assert!(actual.types.iter().any(|t| {
1441 if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("self") } else { false }
1442 }));
1443 }
1444
1445 #[test]
1446 fn test_expand_callable_return_type() {
1447 let code = r"<?php class Foo {}";
1448 let codebase = create_test_codebase(code);
1449
1450 let sig = TCallableSignature::new(false, false).with_return_type(Some(Arc::new(make_self_object())));
1451 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1452
1453 let options = options_with_self("Foo");
1454 let mut actual = input.clone();
1455 expand_union(&codebase, &mut actual, &options);
1456
1457 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1458 && let Some(ret) = sig.get_return_type()
1459 {
1460 assert!(ret.types.iter().any(|t| {
1461 if let TAtomic::Object(TObject::Named(named)) = t {
1462 named.name == ascii_lowercase_atom("foo")
1463 } else {
1464 false
1465 }
1466 }));
1467 }
1468 }
1469
1470 #[test]
1471 fn test_expand_callable_parameter_types() {
1472 let code = r"<?php class Foo {}";
1473 let codebase = create_test_codebase(code);
1474
1475 let param = TCallableParameter::new(Some(Arc::new(make_self_object())), false, false, false);
1476 let sig = TCallableSignature::new(false, false).with_parameters(vec![param]);
1477 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1478
1479 let options = options_with_self("Foo");
1480 let mut actual = input.clone();
1481 expand_union(&codebase, &mut actual, &options);
1482
1483 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1484 && let Some(param) = sig.get_parameters().first()
1485 && let Some(param_type) = param.get_type_signature()
1486 {
1487 assert!(param_type.types.iter().any(|t| {
1488 if let TAtomic::Object(TObject::Named(named)) = t {
1489 named.name == ascii_lowercase_atom("foo")
1490 } else {
1491 false
1492 }
1493 }));
1494 }
1495 }
1496
1497 #[test]
1498 fn test_expand_callable_alias_to_function() {
1499 let code = r"<?php
1500 function myFunc(): int { return 1; }
1501 ";
1502 let codebase = create_test_codebase(code);
1503
1504 let alias = TCallable::Alias(FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc")));
1505 let input = TUnion::from_atomic(TAtomic::Callable(alias));
1506
1507 let mut actual = input.clone();
1508 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1509
1510 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1511 }
1512
1513 #[test]
1514 fn test_expand_callable_alias_to_method() {
1515 let code = r"<?php
1516 class Foo {
1517 public function bar(): int { return 1; }
1518 }
1519 ";
1520 let codebase = create_test_codebase(code);
1521
1522 let alias =
1523 TCallable::Alias(FunctionLikeIdentifier::Method(ascii_lowercase_atom("foo"), ascii_lowercase_atom("bar")));
1524 let input = TUnion::from_atomic(TAtomic::Callable(alias));
1525
1526 let mut actual = input.clone();
1527 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1528
1529 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1530 }
1531
1532 #[test]
1533 fn test_expand_callable_alias_unknown() {
1534 let codebase = CodebaseMetadata::new();
1535
1536 let alias = TCallable::Alias(FunctionLikeIdentifier::Function(atom("nonexistent")));
1537 let input = TUnion::from_atomic(TAtomic::Callable(alias.clone()));
1538
1539 let mut actual = input.clone();
1540 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1541
1542 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Alias(_)))));
1543 }
1544
1545 #[test]
1546 fn test_expand_closure_signature() {
1547 let code = r"<?php class Foo {}";
1548 let codebase = create_test_codebase(code);
1549
1550 let sig = TCallableSignature::new(false, true).with_return_type(Some(Arc::new(make_self_object())));
1551 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1552
1553 let options = options_with_self("Foo");
1554 let mut actual = input.clone();
1555 expand_union(&codebase, &mut actual, &options);
1556
1557 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1558 && let Some(ret) = sig.get_return_type()
1559 {
1560 assert!(ret.types.iter().any(|t| {
1561 if let TAtomic::Object(TObject::Named(named)) = t {
1562 named.name == ascii_lowercase_atom("foo")
1563 } else {
1564 false
1565 }
1566 }));
1567 }
1568 }
1569
1570 #[test]
1571 fn test_expand_generic_parameter_constraint() {
1572 let code = r"<?php class Foo {}";
1573 let codebase = create_test_codebase(code);
1574
1575 let generic = TGenericParameter::new(
1576 atom("T"),
1577 Arc::new(make_self_object()),
1578 GenericParent::ClassLike(ascii_lowercase_atom("foo")),
1579 );
1580 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1581
1582 let options = options_with_self("Foo");
1583 let mut actual = input.clone();
1584 expand_union(&codebase, &mut actual, &options);
1585
1586 if let TAtomic::GenericParameter(param) = &actual.types[0] {
1587 assert!(param.constraint.types.iter().any(|t| {
1588 if let TAtomic::Object(TObject::Named(named)) = t {
1589 named.name == ascii_lowercase_atom("foo")
1590 } else {
1591 false
1592 }
1593 }));
1594 }
1595 }
1596
1597 #[test]
1598 fn test_expand_nested_generic_constraint() {
1599 let code = r"<?php class Foo {} class Bar {}";
1600 let codebase = create_test_codebase(code);
1601
1602 let container =
1603 TNamedObject::new_with_type_parameters(ascii_lowercase_atom("container"), Some(vec![make_self_object()]));
1604 let constraint = TUnion::from_atomic(TAtomic::Object(TObject::Named(container)));
1605
1606 let generic = TGenericParameter::new(
1607 atom("T"),
1608 Arc::new(constraint),
1609 GenericParent::ClassLike(ascii_lowercase_atom("bar")),
1610 );
1611 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1612
1613 let options = options_with_self("Foo");
1614 let mut actual = input.clone();
1615 expand_union(&codebase, &mut actual, &options);
1616
1617 if let TAtomic::GenericParameter(param) = &actual.types[0]
1618 && let TAtomic::Object(TObject::Named(named)) = ¶m.constraint.types[0]
1619 && let Some(params) = &named.type_parameters
1620 {
1621 assert!(params[0].types.iter().any(|t| {
1622 if let TAtomic::Object(TObject::Named(named)) = t {
1623 named.name == ascii_lowercase_atom("foo")
1624 } else {
1625 false
1626 }
1627 }));
1628 }
1629 }
1630
1631 #[test]
1632 fn test_expand_generic_with_intersection() {
1633 let code = r"<?php
1634 interface Stringable {}
1635 class Foo {}
1636 ";
1637 let codebase = create_test_codebase(code);
1638
1639 let mut generic = TGenericParameter::new(
1640 atom("T"),
1641 Arc::new(make_self_object()),
1642 GenericParent::ClassLike(ascii_lowercase_atom("foo")),
1643 );
1644 generic.intersection_types =
1645 Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom("stringable"))))]);
1646 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1647
1648 let options = options_with_self("Foo");
1649 let mut actual = input.clone();
1650 expand_union(&codebase, &mut actual, &options);
1651
1652 if let TAtomic::GenericParameter(param) = &actual.types[0] {
1653 assert!(param.intersection_types.is_some());
1654 assert!(param.constraint.types.iter().any(|t| {
1655 if let TAtomic::Object(TObject::Named(named)) = t {
1656 named.name == ascii_lowercase_atom("foo")
1657 } else {
1658 false
1659 }
1660 }));
1661 }
1662 }
1663
1664 #[test]
1665 fn test_expand_class_string_of_self() {
1666 let code = r"<?php class Foo {}";
1667 let codebase = create_test_codebase(code);
1668
1669 let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))));
1670 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1671 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1672
1673 let options = options_with_self("Foo");
1674 let mut actual = input.clone();
1675 expand_union(&codebase, &mut actual, &options);
1676
1677 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1678 && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1679 {
1680 assert_eq!(named.name, ascii_lowercase_atom("foo"));
1681 }
1682 }
1683
1684 #[test]
1685 fn test_expand_class_string_of_static() {
1686 let code = r"<?php class Foo {}";
1687 let codebase = create_test_codebase(code);
1688
1689 let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("static")))));
1690 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1691 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1692
1693 let options = options_with_static("Foo");
1694 let mut actual = input.clone();
1695 expand_union(&codebase, &mut actual, &options);
1696
1697 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1698 && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1699 {
1700 assert_eq!(named.name, ascii_lowercase_atom("foo"));
1701 }
1702 }
1703
1704 #[test]
1705 fn test_expand_interface_string_of_type() {
1706 let code = r"<?php interface MyInterface {}";
1707 let codebase = create_test_codebase(code);
1708
1709 let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))));
1710 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Interface, constraint };
1711 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1712
1713 let options = options_with_self("MyInterface");
1714 let mut actual = input.clone();
1715 expand_union(&codebase, &mut actual, &options);
1716
1717 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { kind, constraint })) =
1718 &actual.types[0]
1719 {
1720 assert!(matches!(kind, TClassLikeStringKind::Interface));
1721 if let TAtomic::Object(TObject::Named(named)) = constraint.as_ref() {
1722 assert_eq!(named.name, ascii_lowercase_atom("myinterface"));
1723 }
1724 }
1725 }
1726
1727 #[test]
1728 fn test_expand_member_reference_wildcard_constants() {
1729 let code = r"<?php
1730 class Foo {
1731 public const A = 1;
1732 public const B = 2;
1733 }
1734 ";
1735 let codebase = create_test_codebase(code);
1736
1737 let reference = TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Wildcard);
1738 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1739
1740 let mut actual = input.clone();
1741 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1742
1743 assert!(!actual.types.is_empty());
1744 }
1745
1746 #[test]
1747 fn test_expand_member_reference_wildcard_enum_cases() {
1748 let code = r"<?php
1749 enum Status {
1750 case Active;
1751 case Inactive;
1752 }
1753 ";
1754 let codebase = create_test_codebase(code);
1755
1756 let reference = TReference::new_member(ascii_lowercase_atom("status"), TReferenceMemberSelector::Wildcard);
1757 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1758
1759 let mut actual = input.clone();
1760 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1761
1762 assert_eq!(actual.types.len(), 2);
1763 assert!(actual.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1764 }
1765
1766 #[test]
1767 fn test_expand_member_reference_starts_with() {
1768 let code = r"<?php
1769 class Foo {
1770 public const STATUS_ACTIVE = 1;
1771 public const STATUS_INACTIVE = 2;
1772 public const OTHER = 3;
1773 }
1774 ";
1775 let codebase = create_test_codebase(code);
1776
1777 let reference =
1778 TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::StartsWith(atom("STATUS_")));
1779 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1780
1781 let mut actual = input.clone();
1782 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1783
1784 assert!(!actual.types.is_empty());
1785 }
1786
1787 #[test]
1788 fn test_expand_member_reference_ends_with() {
1789 let code = r"<?php
1790 class Foo {
1791 public const READ_ERROR = 1;
1792 public const WRITE_ERROR = 2;
1793 public const SUCCESS = 0;
1794 }
1795 ";
1796 let codebase = create_test_codebase(code);
1797
1798 let reference =
1799 TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::EndsWith(atom("_ERROR")));
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_identifier_constant() {
1810 let code = r"<?php
1811 class Foo {
1812 public const BAR = 42;
1813 }
1814 ";
1815 let codebase = create_test_codebase(code);
1816
1817 let reference =
1818 TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("BAR")));
1819 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1820
1821 let mut actual = input.clone();
1822 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1823
1824 assert_eq!(actual.types.len(), 1);
1825 }
1826
1827 #[test]
1828 fn test_expand_member_reference_identifier_enum_case() {
1829 let code = r"<?php
1830 enum Status {
1831 case Active;
1832 }
1833 ";
1834 let codebase = create_test_codebase(code);
1835
1836 let reference = TReference::new_member(
1837 ascii_lowercase_atom("status"),
1838 TReferenceMemberSelector::Identifier(atom("Active")),
1839 );
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 assert!(matches!(&actual.types[0], TAtomic::Object(TObject::Enum(_))));
1847 }
1848
1849 #[test]
1850 fn test_expand_member_reference_unknown_class() {
1851 let codebase = CodebaseMetadata::new();
1852
1853 let reference = TReference::new_member(atom("NonExistent"), TReferenceMemberSelector::Identifier(atom("FOO")));
1854 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1855
1856 let mut actual = input.clone();
1857 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1858
1859 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
1860 }
1861
1862 #[test]
1863 fn test_expand_member_reference_unknown_member() {
1864 let code = r"<?php class Foo {}";
1865 let codebase = create_test_codebase(code);
1866
1867 let reference = TReference::new_member(
1868 ascii_lowercase_atom("foo"),
1869 TReferenceMemberSelector::Identifier(atom("NONEXISTENT")),
1870 );
1871 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1872
1873 let mut actual = input.clone();
1874 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1875
1876 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
1877 }
1878
1879 #[test]
1880 fn test_expand_member_reference_constant_with_inferred_type() {
1881 let code = r#"<?php
1882 class Foo {
1883 public const VALUE = "hello";
1884 }
1885 "#;
1886 let codebase = create_test_codebase(code);
1887
1888 let reference =
1889 TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("VALUE")));
1890 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1891
1892 let mut actual = input.clone();
1893 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1894
1895 assert_eq!(actual.types.len(), 1);
1896 }
1897
1898 #[test]
1899 fn test_expand_member_reference_constant_with_type_metadata() {
1900 let code = r"<?php
1901 class Foo {
1902 /** @var int */
1903 public const VALUE = 42;
1904 }
1905 ";
1906 let codebase = create_test_codebase(code);
1907
1908 let reference =
1909 TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("VALUE")));
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_eq!(actual.types.len(), 1);
1916 }
1917
1918 #[test]
1919 fn test_expand_conditional_both_branches() {
1920 let code = r"<?php class Foo {} class Bar {}";
1921 let codebase = create_test_codebase(code);
1922
1923 let conditional = TConditional::new(
1924 Arc::new(get_mixed()),
1925 Arc::new(get_string()),
1926 Arc::new(make_self_object()),
1927 Arc::new(make_self_object()),
1928 false,
1929 );
1930 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
1931
1932 let options = options_with_self("Foo");
1933 let mut actual = input.clone();
1934 expand_union(&codebase, &mut actual, &options);
1935
1936 assert!(actual.types.iter().any(|t| {
1937 if let TAtomic::Object(TObject::Named(named)) = t {
1938 named.name == ascii_lowercase_atom("foo")
1939 } else {
1940 false
1941 }
1942 }));
1943 }
1944
1945 #[test]
1946 fn test_expand_conditional_with_self_in_then() {
1947 let code = r"<?php class Foo {}";
1948 let codebase = create_test_codebase(code);
1949
1950 let conditional = TConditional::new(
1951 Arc::new(get_mixed()),
1952 Arc::new(get_string()),
1953 Arc::new(make_self_object()),
1954 Arc::new(get_int()),
1955 false,
1956 );
1957 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
1958
1959 let options = options_with_self("Foo");
1960 let mut actual = input.clone();
1961 expand_union(&codebase, &mut actual, &options);
1962
1963 assert!(!actual.types.is_empty());
1964 }
1965
1966 #[test]
1967 fn test_expand_conditional_with_self_in_otherwise() {
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(get_int()),
1975 Arc::new(make_self_object()),
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_simple_alias() {
1989 let code = r"<?php
1990 class Foo {
1991 /** @phpstan-type MyInt = int */
1992 }
1993 ";
1994 let codebase = create_test_codebase(code);
1995
1996 let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("MyInt"));
1997 let input = TUnion::from_atomic(TAtomic::Alias(alias));
1998
1999 let mut actual = input.clone();
2000 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2001
2002 assert!(!actual.types.is_empty());
2003 }
2004
2005 #[test]
2006 fn test_expand_nested_alias() {
2007 let code = r"<?php
2008 class Foo {
2009 /** @phpstan-type Inner = int */
2010 /** @phpstan-type Outer = Inner */
2011 }
2012 ";
2013 let codebase = create_test_codebase(code);
2014
2015 let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("Outer"));
2016 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2017
2018 let mut actual = input.clone();
2019 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2020
2021 assert!(!actual.types.is_empty());
2022 }
2023
2024 #[test]
2025 fn test_expand_alias_cycle_detection() {
2026 let codebase = CodebaseMetadata::new();
2027
2028 let alias = TAlias::new(atom("Foo"), atom("SelfRef"));
2029 let input = TUnion::from_atomic(TAtomic::Alias(alias.clone()));
2030
2031 let mut actual = input.clone();
2032 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2033
2034 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Alias(_))));
2035 }
2036
2037 #[test]
2038 fn test_expand_alias_unknown() {
2039 let codebase = CodebaseMetadata::new();
2040
2041 let alias = TAlias::new(atom("NonExistent"), atom("Unknown"));
2042 let input = TUnion::from_atomic(TAtomic::Alias(alias.clone()));
2043
2044 let mut actual = input.clone();
2045 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2046
2047 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Alias(_))));
2048 }
2049
2050 #[test]
2051 fn test_expand_alias_with_self_inside() {
2052 let code = r"<?php
2053 class Foo {
2054 /** @phpstan-type MySelf = self */
2055 }
2056 ";
2057 let codebase = create_test_codebase(code);
2058
2059 let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("MySelf"));
2060 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2061
2062 let options = options_with_self("Foo");
2063 let mut actual = input.clone();
2064 expand_union(&codebase, &mut actual, &options);
2065
2066 assert!(!actual.types.is_empty());
2067 }
2068
2069 #[test]
2070 fn test_expand_key_of_array() {
2071 let codebase = CodebaseMetadata::new();
2072
2073 let mut keyed = TKeyedArray::new();
2074 keyed.parameters = Some((Arc::new(get_string()), Arc::new(get_int())));
2075 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2076
2077 let key_of = TKeyOf::new(Arc::new(array_type));
2078 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
2079
2080 let mut actual = input.clone();
2081 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2082
2083 assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_string));
2084 }
2085
2086 #[test]
2087 fn test_expand_key_of_with_self() {
2088 let code = r"<?php class Foo {}";
2089 let codebase = create_test_codebase(code);
2090
2091 let mut keyed = TKeyedArray::new();
2092 keyed.parameters = Some((Arc::new(make_self_object()), Arc::new(get_int())));
2093 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2094
2095 let key_of = TKeyOf::new(Arc::new(array_type));
2096 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
2097
2098 let options = options_with_self("Foo");
2099 let mut actual = input.clone();
2100 expand_union(&codebase, &mut actual, &options);
2101
2102 assert!(!actual.types.is_empty());
2103 }
2104
2105 #[test]
2106 fn test_expand_value_of_array() {
2107 let codebase = CodebaseMetadata::new();
2108
2109 let mut keyed = TKeyedArray::new();
2110 keyed.parameters = Some((Arc::new(get_string()), Arc::new(get_int())));
2111 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2112
2113 let value_of = TValueOf::new(Arc::new(array_type));
2114 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
2115
2116 let mut actual = input.clone();
2117 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2118
2119 assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_int));
2120 }
2121
2122 #[test]
2123 fn test_expand_value_of_enum() {
2124 let code = r"<?php
2125 enum Status: string {
2126 case Active = 'active';
2127 case Inactive = 'inactive';
2128 }
2129 ";
2130 let codebase = create_test_codebase(code);
2131
2132 let enum_type = TUnion::from_atomic(TAtomic::Object(TObject::Enum(TEnum::new(ascii_lowercase_atom("status")))));
2133
2134 let value_of = TValueOf::new(Arc::new(enum_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.is_empty());
2141 }
2142
2143 #[test]
2144 fn test_expand_index_access() {
2145 let codebase = CodebaseMetadata::new();
2146
2147 use crate::ttype::atomic::array::key::ArrayKey;
2148 use std::collections::BTreeMap;
2149
2150 let mut keyed = TKeyedArray::new();
2151 let mut known_items = BTreeMap::new();
2152 known_items.insert(ArrayKey::String(atom("key")), (false, get_int()));
2153 keyed.known_items = Some(known_items);
2154 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2155
2156 use crate::ttype::get_literal_string;
2157 let index_type = get_literal_string(atom("key"));
2158
2159 let index_access = TIndexAccess::new(array_type, index_type);
2160 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2161
2162 let mut actual = input.clone();
2163 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2164
2165 assert!(!actual.types.is_empty());
2166 }
2167
2168 #[test]
2169 fn test_expand_index_access_with_self() {
2170 let code = r"<?php class Foo {}";
2171 let codebase = create_test_codebase(code);
2172
2173 use crate::ttype::atomic::array::key::ArrayKey;
2174 use std::collections::BTreeMap;
2175
2176 let mut keyed = TKeyedArray::new();
2177 let mut known_items = BTreeMap::new();
2178 known_items.insert(ArrayKey::String(atom("key")), (false, make_self_object()));
2179 keyed.known_items = Some(known_items);
2180 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2181
2182 use crate::ttype::get_literal_string;
2183 let index_type = get_literal_string(atom("key"));
2184
2185 let index_access = TIndexAccess::new(array_type, index_type);
2186 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2187
2188 let options = options_with_self("Foo");
2189 let mut actual = input.clone();
2190 expand_union(&codebase, &mut actual, &options);
2191
2192 assert!(!actual.types.is_empty());
2193 }
2194
2195 #[test]
2196 fn test_expand_iterable_key_type() {
2197 let code = r"<?php class Foo {}";
2198 let codebase = create_test_codebase(code);
2199
2200 let iterable = TIterable::new(Arc::new(make_self_object()), Arc::new(get_int()));
2201 let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2202
2203 let options = options_with_self("Foo");
2204 let mut actual = input.clone();
2205 expand_union(&codebase, &mut actual, &options);
2206
2207 if let TAtomic::Iterable(iter) = &actual.types[0] {
2208 assert!(iter.get_key_type().types.iter().any(|t| {
2209 if let TAtomic::Object(TObject::Named(named)) = t {
2210 named.name == ascii_lowercase_atom("foo")
2211 } else {
2212 false
2213 }
2214 }));
2215 }
2216 }
2217
2218 #[test]
2219 fn test_expand_iterable_value_type() {
2220 let code = r"<?php class Foo {}";
2221 let codebase = create_test_codebase(code);
2222
2223 let iterable = TIterable::new(Arc::new(get_int()), Arc::new(make_self_object()));
2224 let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2225
2226 let options = options_with_self("Foo");
2227 let mut actual = input.clone();
2228 expand_union(&codebase, &mut actual, &options);
2229
2230 if let TAtomic::Iterable(iter) = &actual.types[0] {
2231 assert!(iter.get_value_type().types.iter().any(|t| {
2232 if let TAtomic::Object(TObject::Named(named)) = t {
2233 named.name == ascii_lowercase_atom("foo")
2234 } else {
2235 false
2236 }
2237 }));
2238 }
2239 }
2240
2241 #[test]
2242 fn test_get_signature_of_function() {
2243 let code = r#"<?php
2244 function myFunc(int $a): string { return ""; }
2245 "#;
2246 let codebase = create_test_codebase(code);
2247
2248 let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc"));
2249
2250 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2251 assert!(sig.is_some());
2252
2253 let sig = sig.unwrap();
2254 assert_eq!(sig.get_parameters().len(), 1);
2255 assert!(sig.get_return_type().is_some());
2256 }
2257
2258 #[test]
2259 fn test_get_signature_of_method() {
2260 let code = r"<?php
2261 class Foo {
2262 public function bar(string $s): int { return 0; }
2263 }
2264 ";
2265 let codebase = create_test_codebase(code);
2266
2267 let id = FunctionLikeIdentifier::Method(ascii_lowercase_atom("foo"), ascii_lowercase_atom("bar"));
2268
2269 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2270 assert!(sig.is_some());
2271
2272 let sig = sig.unwrap();
2273 assert_eq!(sig.get_parameters().len(), 1);
2274 }
2275
2276 #[test]
2277 fn test_get_signature_of_closure() {
2278 let codebase = CodebaseMetadata::new();
2279
2280 let id = FunctionLikeIdentifier::Closure(FileId::new("test"), Position::new(0));
2281 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2282
2283 assert!(sig.is_none());
2284 }
2285
2286 #[test]
2287 fn test_get_atomic_of_function() {
2288 let code = r"<?php
2289 function myFunc(): void {}
2290 ";
2291 let codebase = create_test_codebase(code);
2292
2293 let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc"));
2294
2295 let atomic = get_atomic_of_function_like_identifier(&id, &codebase);
2296 assert!(atomic.is_some());
2297 assert!(matches!(atomic.unwrap(), TAtomic::Callable(TCallable::Signature(_))));
2298 }
2299
2300 #[test]
2301 fn test_get_signature_with_parameters() {
2302 let code = r"<?php
2303 function multiParam(int $a, string $b, ?float $c = null): bool { return true; }
2304 ";
2305 let codebase = create_test_codebase(code);
2306
2307 let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("multiparam"));
2308
2309 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2310 assert!(sig.is_some());
2311
2312 let sig = sig.unwrap();
2313 assert_eq!(sig.get_parameters().len(), 3);
2314
2315 let third_param = &sig.get_parameters()[2];
2316 assert!(third_param.has_default());
2317 }
2318
2319 #[test]
2320 fn test_expand_preserves_by_reference_flag() {
2321 let code = r"<?php class Foo {}";
2322 let codebase = create_test_codebase(code);
2323
2324 let mut input = make_self_object();
2325 input.flags.insert(UnionFlags::BY_REFERENCE);
2326
2327 let options = options_with_self("Foo");
2328 let mut actual = input.clone();
2329 expand_union(&codebase, &mut actual, &options);
2330
2331 assert!(actual.flags.contains(UnionFlags::BY_REFERENCE));
2332 }
2333
2334 #[test]
2335 fn test_expand_preserves_possibly_undefined_flag() {
2336 let code = r"<?php class Foo {}";
2337 let codebase = create_test_codebase(code);
2338
2339 let mut input = make_self_object();
2340 input.flags.insert(UnionFlags::POSSIBLY_UNDEFINED);
2341
2342 let options = options_with_self("Foo");
2343 let mut actual = input.clone();
2344 expand_union(&codebase, &mut actual, &options);
2345
2346 assert!(actual.flags.contains(UnionFlags::POSSIBLY_UNDEFINED));
2347 }
2348
2349 #[test]
2350 fn test_expand_multiple_self_in_union() {
2351 let code = r"<?php class Foo {}";
2352 let codebase = create_test_codebase(code);
2353
2354 let input = TUnion::from_vec(vec![
2355 TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))),
2356 TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))),
2357 ]);
2358
2359 let options = options_with_self("Foo");
2360 let mut actual = input.clone();
2361 expand_union(&codebase, &mut actual, &options);
2362
2363 assert!(actual.types.len() <= 2);
2364 }
2365
2366 #[test]
2367 fn test_expand_deeply_nested_types() {
2368 let code = r"<?php class Foo {}";
2369 let codebase = create_test_codebase(code);
2370
2371 let inner = TList::new(Arc::new(make_self_object()));
2372 let middle = TList::new(Arc::new(TUnion::from_atomic(TAtomic::Array(TArray::List(inner)))));
2373 let outer = TList::new(Arc::new(TUnion::from_atomic(TAtomic::Array(TArray::List(middle)))));
2374 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(outer)));
2375
2376 let options = options_with_self("Foo");
2377 let mut actual = input.clone();
2378 expand_union(&codebase, &mut actual, &options);
2379
2380 if let TAtomic::Array(TArray::List(outer)) = &actual.types[0]
2381 && let TAtomic::Array(TArray::List(middle)) = &outer.element_type.types[0]
2382 && let TAtomic::Array(TArray::List(inner)) = &middle.element_type.types[0]
2383 {
2384 assert!(inner.element_type.types.iter().any(|t| {
2385 if let TAtomic::Object(TObject::Named(named)) = t {
2386 named.name == ascii_lowercase_atom("foo")
2387 } else {
2388 false
2389 }
2390 }));
2391 }
2392 }
2393
2394 #[test]
2395 fn test_expand_with_all_options_disabled() {
2396 let code = r"<?php class Foo {}";
2397 let codebase = create_test_codebase(code);
2398
2399 let input = make_self_object();
2400 let options = TypeExpansionOptions {
2401 self_class: None,
2402 static_class_type: StaticClassType::None,
2403 parent_class: None,
2404 evaluate_class_constants: false,
2405 evaluate_conditional_types: false,
2406 function_is_final: false,
2407 expand_generic: false,
2408 expand_templates: false,
2409 };
2410
2411 let mut actual = input.clone();
2412 expand_union(&codebase, &mut actual, &options);
2413
2414 assert!(actual.types.iter().any(|t| {
2415 if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("self") } else { false }
2416 }));
2417 }
2418
2419 #[test]
2420 fn test_expand_already_expanded_type() {
2421 let code = r"<?php class Foo {}";
2422 let codebase = create_test_codebase(code);
2423
2424 let input = make_named_object("Foo");
2425 let options = options_with_self("Foo");
2426
2427 let mut actual = input.clone();
2428 expand_union(&codebase, &mut actual, &options);
2429
2430 let mut actual2 = actual.clone();
2431 expand_union(&codebase, &mut actual2, &options);
2432
2433 assert_eq!(actual.types.as_ref(), actual2.types.as_ref());
2434 }
2435
2436 #[test]
2437 fn test_expand_complex_generic_class() {
2438 let code = r"<?php
2439 /**
2440 * @template T
2441 * @template U
2442 */
2443 class Container {}
2444 ";
2445 let codebase = create_test_codebase(code);
2446
2447 let named = TNamedObject::new_with_type_parameters(
2448 ascii_lowercase_atom("container"),
2449 Some(vec![make_self_object(), make_static_object()]),
2450 );
2451 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
2452
2453 let options = TypeExpansionOptions {
2454 self_class: Some(ascii_lowercase_atom("foo")),
2455 static_class_type: StaticClassType::Name(ascii_lowercase_atom("bar")),
2456 ..Default::default()
2457 };
2458
2459 let mut actual = input.clone();
2460 expand_union(&codebase, &mut actual, &options);
2461
2462 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
2463 && let Some(params) = &named.type_parameters
2464 {
2465 assert!(params[0].types.iter().any(|t| {
2466 if let TAtomic::Object(TObject::Named(named)) = t {
2467 named.name == ascii_lowercase_atom("foo")
2468 } else {
2469 false
2470 }
2471 }));
2472 assert!(params[1].types.iter().any(|t| {
2473 if let TAtomic::Object(TObject::Named(named)) = t {
2474 named.name == ascii_lowercase_atom("bar")
2475 } else {
2476 false
2477 }
2478 }));
2479 }
2480 }
2481}