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