1use std::borrow::Cow;
2use std::cell::RefCell;
3use std::sync::Arc;
4
5use std::collections::HashSet;
6
7use foldhash::fast::FixedState;
8use mago_word::Word;
9use mago_word::ascii_lowercase_word;
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<(Word, Word), FixedState>> = const { RefCell::new(HashSet::with_hasher(FixedState::with_seed(0))) };
49
50 static EXPANDING_OBJECT_PARAMS: RefCell<HashSet<Word, FixedState>> = const { RefCell::new(HashSet::with_hasher(FixedState::with_seed(0))) };
52
53 static EXPANDING_CONSTANTS: RefCell<HashSet<(Word, Word), 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: Word,
74 alias_name: Word,
75}
76
77impl AliasExpansionGuard {
78 #[must_use]
79 fn new(class_name: Word, alias_name: Word) -> 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: Word,
94}
95
96impl ObjectParamsExpansionGuard {
97 #[must_use]
98 fn try_new(object_name: Word) -> 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: Word,
125 constant_name: Word,
126}
127
128impl ConstantExpansionGuard {
129 #[must_use]
130 fn try_new(class_name: Word, constant_name: Word) -> 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(Word),
154 Object(TObject),
155}
156
157#[derive(Debug)]
158pub struct TypeExpansionOptions {
159 pub self_class: Option<Word>,
160 pub static_class_type: StaticClassType,
161 pub parent_class: Option<Word>,
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_word(class_like_name.as_bytes());
384 match name_lc.as_bytes() {
385 b"self" => options.self_class.unwrap_or(class_like_name),
386 b"static" | b"$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 b"parent" => {
394 if let Some(self_class) = options.self_class
395 && let Some(class_metadata) = codebase.get_class_like(self_class.as_bytes())
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.as_bytes()) 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: Word,
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.as_bytes().eq_ignore_ascii_case(b"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.as_bytes()) 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.as_bytes());
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: &[u8]) -> SpecialClassName {
586 match name.len() {
587 4 => {
588 if name.eq_ignore_ascii_case(b"self") {
589 SpecialClassName::SelfType
590 } else {
591 SpecialClassName::None
592 }
593 }
594 5 => {
595 if name == b"$this" || name.eq_ignore_ascii_case(b"$this") {
596 SpecialClassName::This
597 } else {
598 SpecialClassName::None
599 }
600 }
601 6 => {
602 if name.eq_ignore_ascii_case(b"static") {
603 SpecialClassName::Static
604 } else if name.eq_ignore_ascii_case(b"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_bytes());
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.as_bytes())
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.as_bytes(), named.name.as_bytes())) =>
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: &Word, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> bool {
709 if options.function_is_final {
710 return true;
711 }
712
713 codebase.get_class_like(class_name.as_bytes()).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.as_bytes(), named.name.as_bytes())
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.as_bytes(), named.name.as_bytes()))
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.as_bytes()) 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.as_bytes()) {
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.as_bytes())?;
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 =
802 codebase.get_declaring_method(classlike_name.as_bytes(), method_name.as_bytes())?;
803
804 get_signature_of_function_like_metadata(
805 function_like_identifier,
806 function_like_metadata,
807 codebase,
808 &TypeExpansionOptions {
809 self_class: Some(*classlike_name),
810 static_class_type: StaticClassType::Name(*classlike_name),
811 ..Default::default()
812 },
813 )
814 }
815 })
816}
817
818#[must_use]
819pub fn get_atomic_of_function_like_identifier(
820 function_like_identifier: &FunctionLikeIdentifier,
821 codebase: &CodebaseMetadata,
822) -> Option<TAtomic> {
823 let signature = get_signature_of_function_like_identifier(function_like_identifier, codebase)?;
824
825 Some(TAtomic::Callable(TCallable::Signature(signature)))
826}
827
828#[must_use]
829pub fn get_signature_of_function_like_metadata(
830 function_like_identifier: &FunctionLikeIdentifier,
831 function_like_metadata: &FunctionLikeMetadata,
832 codebase: &CodebaseMetadata,
833 options: &TypeExpansionOptions,
834) -> TCallableSignature {
835 let parameters: Vec<_> = function_like_metadata
836 .parameters
837 .iter()
838 .map(|parameter_metadata| {
839 let type_signature = if let Some(t) = parameter_metadata.get_type_metadata() {
840 let mut t = t.type_union.clone();
841 expand_union(codebase, &mut t, options);
842 Some(Arc::new(t))
843 } else {
844 None
845 };
846
847 TCallableParameter::new(
848 type_signature,
849 parameter_metadata.flags.is_by_reference(),
850 parameter_metadata.flags.is_variadic(),
851 parameter_metadata.flags.has_default(),
852 )
853 })
854 .collect();
855
856 let return_type = if let Some(type_metadata) = function_like_metadata.return_type_metadata.as_ref() {
857 let mut return_type = type_metadata.type_union.clone();
858 expand_union(codebase, &mut return_type, options);
859 Some(Arc::new(return_type))
860 } else {
861 None
862 };
863
864 let is_closure = matches!(function_like_identifier, FunctionLikeIdentifier::Closure(..));
865 let mut signature = TCallableSignature::new(function_like_metadata.flags.is_pure(), is_closure)
866 .with_parameters(parameters)
867 .with_return_type(return_type)
868 .with_source(Some(*function_like_identifier));
869
870 if let FunctionLikeIdentifier::Closure(file_id, closure_position) = function_like_identifier {
871 signature = signature.with_closure_location(Some((*file_id, *closure_position)));
872 }
873
874 signature
875}
876
877#[cold]
878fn expand_key_of(
879 return_type_key_of: &TKeyOf,
880 codebase: &CodebaseMetadata,
881 options: &TypeExpansionOptions,
882) -> Vec<TAtomic> {
883 let mut target_type = return_type_key_of.get_target_type().clone();
884 expand_union(codebase, &mut target_type, options);
885
886 let Some(new_return_types) = TKeyOf::get_key_of_targets(&target_type.types, codebase, false) else {
887 return vec![TAtomic::Derived(TDerived::KeyOf(return_type_key_of.clone()))];
888 };
889
890 new_return_types.types.into_owned()
891}
892
893#[cold]
894fn expand_value_of(
895 return_type_value_of: &TValueOf,
896 codebase: &CodebaseMetadata,
897 options: &TypeExpansionOptions,
898) -> Vec<TAtomic> {
899 let mut target_type = return_type_value_of.get_target_type().clone();
900 expand_union(codebase, &mut target_type, options);
901
902 let Some(new_return_types) = TValueOf::get_value_of_targets(&target_type.types, codebase, false) else {
903 return vec![TAtomic::Derived(TDerived::ValueOf(return_type_value_of.clone()))];
904 };
905
906 new_return_types.types.into_owned()
907}
908
909#[cold]
910fn expand_index_access(
911 return_type_index_access: &TIndexAccess,
912 codebase: &CodebaseMetadata,
913 options: &TypeExpansionOptions,
914) -> Vec<TAtomic> {
915 let mut target_type = return_type_index_access.get_target_type().clone();
916 expand_union(codebase, &mut target_type, options);
917
918 let mut index_type = return_type_index_access.get_index_type().clone();
919 expand_union(codebase, &mut index_type, options);
920
921 let Some(new_return_types) = TIndexAccess::get_indexed_access_result(&target_type.types, &index_type.types, false)
922 else {
923 return vec![TAtomic::Derived(TDerived::IndexAccess(return_type_index_access.clone()))];
924 };
925
926 new_return_types.types.into_owned()
927}
928
929#[cold]
930fn expand_new(new_type: &TNew, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
931 let mut target_type = new_type.get_target_type().clone();
932 expand_union(codebase, &mut target_type, options);
933
934 let Some(new_return_types) = TNew::get_new_targets(&target_type.types, codebase) else {
935 return vec![TAtomic::Derived(TDerived::New(new_type.clone()))];
936 };
937
938 new_return_types.types.into_owned()
939}
940
941#[cold]
942fn expand_template_type(
943 template_type: &TTemplateType,
944 codebase: &CodebaseMetadata,
945 options: &TypeExpansionOptions,
946) -> Vec<TAtomic> {
947 let mut expanded = template_type.clone();
948 expand_union(codebase, expanded.get_object_mut(), options);
949 expand_union(codebase, expanded.get_class_name_mut(), options);
950 expand_union(codebase, expanded.get_template_name_mut(), options);
951
952 let Some(resolved) = expanded.resolve(codebase) else {
953 return vec![TAtomic::Mixed(TMixed::new())];
954 };
955
956 resolved.types.into_owned()
957}
958
959#[cold]
960fn expand_int_mask(int_mask: &TIntMask, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
961 let mut literal_values = Vec::new();
962
963 for value in int_mask.get_values() {
964 let mut expanded = value.clone();
965 expand_union(codebase, &mut expanded, options);
966
967 if let Some(int_val) = expanded.get_single_literal_int_value() {
968 literal_values.push(int_val);
969 }
970 }
971
972 if literal_values.is_empty() {
973 return vec![TAtomic::Scalar(TScalar::int())];
974 }
975
976 let combinations = TIntMask::calculate_mask_combinations(&literal_values);
977 combinations.into_iter().map(|v| TAtomic::Scalar(TScalar::literal_int(v))).collect()
978}
979
980#[cold]
981fn expand_int_mask_of(
982 int_mask_of: &TIntMaskOf,
983 codebase: &CodebaseMetadata,
984 options: &TypeExpansionOptions,
985) -> Vec<TAtomic> {
986 let mut target = int_mask_of.get_target_type().clone();
987 expand_union(codebase, &mut target, options);
988
989 let mut literal_values = Vec::new();
990 for atomic in target.types.iter() {
991 if let Some(int_val) = atomic.get_literal_int_value() {
992 literal_values.push(int_val);
993 }
994 }
995
996 if literal_values.is_empty() {
997 return vec![TAtomic::Scalar(TScalar::int())];
998 }
999
1000 let combinations = TIntMask::calculate_mask_combinations(&literal_values);
1001 combinations.into_iter().map(|v| TAtomic::Scalar(TScalar::literal_int(v))).collect()
1002}
1003
1004#[cold]
1005fn expand_properties_of(
1006 properties_of: &TPropertiesOf,
1007 codebase: &CodebaseMetadata,
1008 options: &TypeExpansionOptions,
1009) -> Vec<TAtomic> {
1010 let mut target_type = properties_of.get_target_type().clone();
1011 expand_union(codebase, &mut target_type, options);
1012
1013 let Some(keyed_array) =
1014 TPropertiesOf::get_properties_of_targets(&target_type.types, codebase, properties_of.visibility(), false)
1015 else {
1016 return vec![TAtomic::Derived(TDerived::PropertiesOf(properties_of.clone()))];
1017 };
1018
1019 vec![keyed_array]
1020}
1021
1022#[cold]
1023fn expand_alias(alias: &TAlias, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
1024 let class_name = alias.get_class_name();
1025 let alias_name = alias.get_alias_name();
1026
1027 let is_cycle = EXPANDING_ALIASES.with(|set| set.borrow().contains(&(class_name, alias_name)));
1029
1030 if is_cycle {
1031 return vec![TAtomic::Alias(alias.clone())];
1032 }
1033
1034 let Some(mut expanded_union) = alias.resolve(codebase).cloned() else {
1035 return vec![TAtomic::Alias(alias.clone())];
1036 };
1037
1038 let _guard = AliasExpansionGuard::new(class_name, alias_name);
1039
1040 expand_union(codebase, &mut expanded_union, options);
1041
1042 expanded_union.types.into_owned()
1043}
1044
1045#[cfg(test)]
1046#[allow(clippy::unwrap_used, clippy::expect_used)]
1047mod tests {
1048 use super::*;
1049
1050 use std::borrow::Cow;
1051 use std::collections::HashSet;
1052 use std::sync::Arc;
1053
1054 use bumpalo::Bump;
1055
1056 use mago_database::Database;
1057 use mago_database::DatabaseReader;
1058 use mago_database::file::File;
1059 use mago_database::file::FileId;
1060 use mago_names::resolver::NameResolver;
1061 use mago_span::Position;
1062 use mago_syntax::parser::parse_file;
1063 use mago_word::WordSet;
1064 use mago_word::word;
1065
1066 use crate::metadata::CodebaseMetadata;
1067 use crate::misc::GenericParent;
1068 use crate::populator::populate_codebase;
1069 use crate::reference::SymbolReferences;
1070 use crate::scanner::scan_program;
1071 use crate::ttype::atomic::array::TArray;
1072 use crate::ttype::atomic::array::keyed::TKeyedArray;
1073 use crate::ttype::atomic::array::list::TList;
1074 use crate::ttype::atomic::callable::TCallable;
1075 use crate::ttype::atomic::callable::TCallableSignature;
1076 use crate::ttype::atomic::callable::parameter::TCallableParameter;
1077 use crate::ttype::atomic::conditional::TConditional;
1078 use crate::ttype::atomic::derived::TDerived;
1079 use crate::ttype::atomic::derived::index_access::TIndexAccess;
1080 use crate::ttype::atomic::derived::key_of::TKeyOf;
1081 use crate::ttype::atomic::derived::value_of::TValueOf;
1082 use crate::ttype::atomic::generic::TGenericParameter;
1083 use crate::ttype::atomic::iterable::TIterable;
1084 use crate::ttype::atomic::object::r#enum::TEnum;
1085 use crate::ttype::atomic::object::named::TNamedObject;
1086 use crate::ttype::atomic::reference::TReference;
1087 use crate::ttype::atomic::reference::TReferenceMemberSelector;
1088 use crate::ttype::atomic::scalar::TScalar;
1089 use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
1090 use crate::ttype::atomic::scalar::class_like_string::TClassLikeStringKind;
1091 use crate::ttype::flags::UnionFlags;
1092 use crate::ttype::get_int;
1093 use crate::ttype::get_mixed;
1094 use crate::ttype::get_never;
1095 use crate::ttype::get_null;
1096 use crate::ttype::get_string;
1097 use crate::ttype::get_void;
1098
1099 fn create_test_codebase(code: &'static str) -> CodebaseMetadata {
1100 let file = File::ephemeral(Cow::Borrowed(b"code.php"), Cow::Borrowed(code.as_bytes()));
1101 let config =
1102 mago_database::DatabaseConfiguration::new(std::path::Path::new("/"), vec![], vec![], vec![], vec![])
1103 .into_static();
1104 let database = Database::single(file, config);
1105
1106 let mut codebase = CodebaseMetadata::new();
1107 let arena = Bump::new();
1108 for file in database.files() {
1109 let program = parse_file(&arena, &file);
1110 assert!(!program.has_errors(), "Parse failed: {:?}", program.errors);
1111 let resolved_names = NameResolver::new(&arena).resolve(program);
1112 let program_codebase =
1113 scan_program(&arena, &file, program, &resolved_names, mago_php_version::PHPVersion::LATEST);
1114
1115 codebase.extend(program_codebase);
1116 }
1117
1118 populate_codebase(&mut codebase, &mut SymbolReferences::new(), WordSet::default(), HashSet::default());
1119
1120 codebase
1121 }
1122
1123 fn options_with_self(self_class: &str) -> TypeExpansionOptions {
1124 TypeExpansionOptions { self_class: Some(ascii_lowercase_word(self_class.as_bytes())), ..Default::default() }
1125 }
1126
1127 fn options_with_static(static_class: &str) -> TypeExpansionOptions {
1128 TypeExpansionOptions {
1129 self_class: Some(ascii_lowercase_word(static_class.as_bytes())),
1130 static_class_type: StaticClassType::Name(ascii_lowercase_word(static_class.as_bytes())),
1131 ..Default::default()
1132 }
1133 }
1134
1135 fn options_with_static_object(object: TObject) -> TypeExpansionOptions {
1136 TypeExpansionOptions {
1137 self_class: object.get_name(),
1138 static_class_type: StaticClassType::Object(object),
1139 ..Default::default()
1140 }
1141 }
1142
1143 macro_rules! assert_expands_to {
1144 ($codebase:expr, $input:expr, $expected:expr) => {
1145 assert_expands_to!($codebase, $input, $expected, &TypeExpansionOptions::default())
1146 };
1147 ($codebase:expr, $input:expr, $expected:expr, $options:expr) => {{
1148 let mut actual = $input.clone();
1149 expand_union($codebase, &mut actual, $options);
1150 assert_eq!(
1151 actual.types.as_ref(),
1152 $expected.types.as_ref(),
1153 "Type expansion mismatch.\nInput: {:?}\nExpected: {:?}\nActual: {:?}",
1154 $input,
1155 $expected,
1156 actual
1157 );
1158 }};
1159 }
1160
1161 fn make_self_object() -> TUnion {
1162 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(word("self")))))
1163 }
1164
1165 fn make_static_object() -> TUnion {
1166 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(word("static")))))
1167 }
1168
1169 fn make_parent_object() -> TUnion {
1170 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(word("parent")))))
1171 }
1172
1173 fn make_named_object(name: &str) -> TUnion {
1174 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_word(name.as_bytes())))))
1175 }
1176
1177 #[test]
1178 fn test_expand_null_type() {
1179 let codebase = CodebaseMetadata::new();
1180 let null_type = get_null();
1181 assert_expands_to!(&codebase, null_type, get_null());
1182 }
1183
1184 #[test]
1185 fn test_expand_void_type() {
1186 let codebase = CodebaseMetadata::new();
1187 let void_type = get_void();
1188 assert_expands_to!(&codebase, void_type, get_void());
1189 }
1190
1191 #[test]
1192 fn test_expand_never_type() {
1193 let codebase = CodebaseMetadata::new();
1194 let never_type = get_never();
1195 assert_expands_to!(&codebase, never_type, get_never());
1196 }
1197
1198 #[test]
1199 fn test_expand_int_type() {
1200 let codebase = CodebaseMetadata::new();
1201 let int_type = get_int();
1202 assert_expands_to!(&codebase, int_type, get_int());
1203 }
1204
1205 #[test]
1206 fn test_expand_mixed_type() {
1207 let codebase = CodebaseMetadata::new();
1208 let mixed_type = get_mixed();
1209 assert_expands_to!(&codebase, mixed_type, get_mixed());
1210 }
1211
1212 #[test]
1213 fn test_expand_keyed_array_with_self_key() {
1214 let code = "<?php class Foo {}";
1215 let codebase = create_test_codebase(code);
1216
1217 let mut keyed = TKeyedArray::new();
1218 keyed.parameters = Some((Arc::new(make_self_object()), Arc::new(get_int())));
1219 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1220
1221 let options = options_with_self("Foo");
1222 let mut actual = input;
1223 expand_union(&codebase, &mut actual, &options);
1224
1225 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1226 && let Some((key, _)) = &keyed.parameters
1227 {
1228 assert!(key.types.iter().any(|t| {
1229 if let TAtomic::Object(TObject::Named(named)) = t {
1230 named.name == ascii_lowercase_word(b"foo")
1231 } else {
1232 false
1233 }
1234 }));
1235 }
1236 }
1237
1238 #[test]
1239 fn test_expand_keyed_array_with_self_value() {
1240 let code = "<?php class Foo {}";
1241 let codebase = create_test_codebase(code);
1242
1243 let mut keyed = TKeyedArray::new();
1244 keyed.parameters = Some((Arc::new(get_string()), Arc::new(make_self_object())));
1245 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1246
1247 let options = options_with_self("Foo");
1248 let mut actual = input;
1249 expand_union(&codebase, &mut actual, &options);
1250
1251 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1252 && let Some((_, value)) = &keyed.parameters
1253 {
1254 assert!(value.types.iter().any(|t| {
1255 if let TAtomic::Object(TObject::Named(named)) = t {
1256 named.name == ascii_lowercase_word(b"foo")
1257 } else {
1258 false
1259 }
1260 }));
1261 }
1262 }
1263
1264 #[test]
1265 fn test_expand_keyed_array_known_items() {
1266 let code = "<?php class Foo {}";
1267 let codebase = create_test_codebase(code);
1268
1269 use crate::ttype::atomic::array::key::ArrayKey;
1270 use std::collections::BTreeMap;
1271
1272 let mut keyed = TKeyedArray::new();
1273 let mut known_items = BTreeMap::new();
1274 known_items.insert(ArrayKey::String(word("key")), (false, make_self_object()));
1275 keyed.known_items = Some(known_items);
1276 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1277
1278 let options = options_with_self("Foo");
1279 let mut actual = input;
1280 expand_union(&codebase, &mut actual, &options);
1281
1282 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1283 && let Some(items) = &keyed.known_items
1284 {
1285 let (_, item_type) = items.get(&ArrayKey::String(word("key"))).unwrap();
1286 assert!(item_type.types.iter().any(|t| {
1287 if let TAtomic::Object(TObject::Named(named)) = t {
1288 named.name == ascii_lowercase_word(b"foo")
1289 } else {
1290 false
1291 }
1292 }));
1293 }
1294 }
1295
1296 #[test]
1297 fn test_expand_list_with_self_element() {
1298 let code = "<?php class Foo {}";
1299 let codebase = create_test_codebase(code);
1300
1301 let list = TList::new(Arc::new(make_self_object()));
1302 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1303
1304 let options = options_with_self("Foo");
1305 let mut actual = input;
1306 expand_union(&codebase, &mut actual, &options);
1307
1308 if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1309 assert!(list.element_type.types.iter().any(|t| {
1310 if let TAtomic::Object(TObject::Named(named)) = t {
1311 named.name == ascii_lowercase_word(b"foo")
1312 } else {
1313 false
1314 }
1315 }));
1316 }
1317 }
1318
1319 #[test]
1320 fn test_expand_list_known_elements() {
1321 let code = "<?php class Foo {}";
1322 let codebase = create_test_codebase(code);
1323
1324 use std::collections::BTreeMap;
1325
1326 let mut list = TList::new(Arc::new(get_mixed()));
1327 let mut known_elements = BTreeMap::new();
1328 known_elements.insert(0, (false, make_self_object()));
1329 list.known_elements = Some(known_elements);
1330 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1331
1332 let options = options_with_self("Foo");
1333 let mut actual = input;
1334 expand_union(&codebase, &mut actual, &options);
1335
1336 if let TAtomic::Array(TArray::List(list)) = &actual.types[0]
1337 && let Some(elements) = &list.known_elements
1338 {
1339 let (_, element_type) = elements.get(&0).unwrap();
1340 assert!(element_type.types.iter().any(|t| {
1341 if let TAtomic::Object(TObject::Named(named)) = t {
1342 named.name == ascii_lowercase_word(b"foo")
1343 } else {
1344 false
1345 }
1346 }));
1347 }
1348 }
1349
1350 #[test]
1351 fn test_expand_nested_array() {
1352 let code = "<?php class Foo {}";
1353 let codebase = create_test_codebase(code);
1354
1355 let inner_list = TList::new(Arc::new(make_self_object()));
1356 let inner_array = TUnion::from_atomic(TAtomic::Array(TArray::List(inner_list)));
1357
1358 let mut outer = TKeyedArray::new();
1359 outer.parameters = Some((Arc::new(make_self_object()), Arc::new(inner_array)));
1360 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(outer)));
1361
1362 let options = options_with_self("Foo");
1363 let mut actual = input;
1364 expand_union(&codebase, &mut actual, &options);
1365
1366 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1367 && let Some((key, value)) = &keyed.parameters
1368 {
1369 assert!(key.types.iter().any(|t| {
1370 if let TAtomic::Object(TObject::Named(named)) = t {
1371 named.name == ascii_lowercase_word(b"foo")
1372 } else {
1373 false
1374 }
1375 }));
1376 if let TAtomic::Array(TArray::List(inner)) = &value.types[0] {
1377 assert!(inner.element_type.types.iter().any(|t| {
1378 if let TAtomic::Object(TObject::Named(named)) = t {
1379 named.name == ascii_lowercase_word(b"foo")
1380 } else {
1381 false
1382 }
1383 }));
1384 }
1385 }
1386 }
1387
1388 #[test]
1389 fn test_expand_empty_array() {
1390 let codebase = CodebaseMetadata::new();
1391 let keyed = TKeyedArray::new();
1392 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed.clone())));
1393 let expected = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1394 assert_expands_to!(&codebase, input, expected);
1395 }
1396
1397 #[test]
1398 fn test_expand_non_empty_list() {
1399 let code = "<?php class Foo {}";
1400 let codebase = create_test_codebase(code);
1401
1402 let mut list = TList::new(Arc::new(make_self_object()));
1403 list.non_empty = true;
1404 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1405
1406 let options = options_with_self("Foo");
1407 let mut actual = input;
1408 expand_union(&codebase, &mut actual, &options);
1409
1410 if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1411 assert!(list.non_empty);
1412 assert!(list.element_type.types.iter().any(|t| {
1413 if let TAtomic::Object(TObject::Named(named)) = t {
1414 named.name == ascii_lowercase_word(b"foo")
1415 } else {
1416 false
1417 }
1418 }));
1419 }
1420 }
1421
1422 #[test]
1423 fn test_expand_self_to_class_name() {
1424 let code = "<?php class Foo {}";
1425 let codebase = create_test_codebase(code);
1426
1427 let input = make_self_object();
1428 let options = options_with_self("Foo");
1429 let mut actual = input;
1430 expand_union(&codebase, &mut actual, &options);
1431
1432 assert!(actual.types.iter().any(|t| {
1433 if let TAtomic::Object(TObject::Named(named)) = t {
1434 named.name == ascii_lowercase_word(b"foo")
1435 } else {
1436 false
1437 }
1438 }));
1439 }
1440
1441 #[test]
1442 fn test_expand_static_to_class_name() {
1443 let code = "<?php class Foo {}";
1444 let codebase = create_test_codebase(code);
1445
1446 let input = make_static_object();
1447 let options = options_with_static("Foo");
1448 let mut actual = input;
1449 expand_union(&codebase, &mut actual, &options);
1450
1451 assert!(actual.types.iter().any(|t| {
1452 if let TAtomic::Object(TObject::Named(named)) = t {
1453 named.name == ascii_lowercase_word(b"foo")
1454 } else {
1455 false
1456 }
1457 }));
1458 }
1459
1460 #[test]
1461 fn test_expand_static_with_object_type() {
1462 let code = "<?php class Foo {}";
1463 let codebase = create_test_codebase(code);
1464
1465 let input = make_static_object();
1466 let static_obj = TObject::Named(TNamedObject::new(ascii_lowercase_word(b"foo")));
1467 let options = options_with_static_object(static_obj);
1468 let mut actual = input;
1469 expand_union(&codebase, &mut actual, &options);
1470
1471 assert!(actual.types.iter().any(|t| {
1472 if let TAtomic::Object(TObject::Named(named)) = t {
1473 named.name == ascii_lowercase_word(b"foo") && named.is_static && !named.is_this
1474 } else {
1475 false
1476 }
1477 }));
1478 }
1479
1480 #[test]
1481 fn test_expand_static_with_enum_type() {
1482 let code = "<?php enum Status { case Active; case Inactive; }";
1483 let codebase = create_test_codebase(code);
1484
1485 let input = make_static_object();
1486 let static_enum = TObject::Enum(TEnum::new(ascii_lowercase_word(b"status")));
1487 let options = options_with_static_object(static_enum);
1488 let mut actual = input;
1489 expand_union(&codebase, &mut actual, &options);
1490
1491 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1492 }
1493
1494 #[test]
1495 fn test_expand_parent_to_parent_class() {
1496 let code = "<?php
1497 class BaseClass {}
1498 class ChildClass extends BaseClass {}
1499 ";
1500 let codebase = create_test_codebase(code);
1501
1502 let input = make_parent_object();
1503 let options = options_with_self("ChildClass");
1504 let mut actual = input;
1505 expand_union(&codebase, &mut actual, &options);
1506
1507 assert!(actual.types.iter().any(|t| {
1508 if let TAtomic::Object(TObject::Named(named)) = t {
1509 named.name == ascii_lowercase_word(b"baseclass")
1510 } else {
1511 false
1512 }
1513 }));
1514 }
1515
1516 #[test]
1517 fn test_expand_parent_without_parent_class() {
1518 let code = "<?php class Foo {}";
1519 let codebase = create_test_codebase(code);
1520
1521 let input = make_parent_object();
1522 let options = options_with_self("Foo");
1523 let mut actual = input;
1524 expand_union(&codebase, &mut actual, &options);
1525
1526 assert!(actual.types.iter().any(|t| {
1527 if let TAtomic::Object(TObject::Named(named)) = t { named.name == word("parent") } else { false }
1528 }));
1529 }
1530
1531 #[test]
1532 fn test_expand_this_variable() {
1533 let code = "<?php class Foo {}";
1534 let codebase = create_test_codebase(code);
1535
1536 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new_this(word("$this")))));
1537 let options = options_with_static("Foo");
1538 let mut actual = input;
1539 expand_union(&codebase, &mut actual, &options);
1540
1541 assert!(actual.types.iter().any(|t| {
1542 if let TAtomic::Object(TObject::Named(named)) = t {
1543 named.name == ascii_lowercase_word(b"foo")
1544 } else {
1545 false
1546 }
1547 }));
1548 }
1549
1550 #[test]
1551 fn test_expand_this_with_final_function() {
1552 let code = "<?php class Foo {}";
1553 let codebase = create_test_codebase(code);
1554
1555 let input = make_static_object();
1556 let options = TypeExpansionOptions {
1557 self_class: Some(ascii_lowercase_word(b"foo")),
1558 static_class_type: StaticClassType::Name(ascii_lowercase_word(b"foo")),
1559 function_is_final: true,
1560 ..Default::default()
1561 };
1562 let mut actual = input;
1563 expand_union(&codebase, &mut actual, &options);
1564
1565 assert!(actual.types.iter().any(|t| {
1566 if let TAtomic::Object(TObject::Named(named)) = t {
1567 named.name == ascii_lowercase_word(b"foo") && !named.is_this
1568 } else {
1569 false
1570 }
1571 }));
1572 }
1573
1574 #[test]
1575 fn test_expand_object_with_type_parameters() {
1576 let code = "<?php class Container {}";
1577 let codebase = create_test_codebase(code);
1578
1579 let named =
1580 TNamedObject::new_with_type_parameters(ascii_lowercase_word(b"container"), Some(vec![make_self_object()]));
1581 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1582
1583 let options = options_with_self("Foo");
1584 let mut actual = input;
1585 expand_union(&codebase, &mut actual, &options);
1586
1587 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
1588 && let Some(params) = &named.type_parameters
1589 {
1590 assert!(params[0].types.iter().any(|t| {
1591 if let TAtomic::Object(TObject::Named(named)) = t {
1592 named.name == ascii_lowercase_word(b"foo")
1593 } else {
1594 false
1595 }
1596 }));
1597 }
1598 }
1599
1600 #[test]
1601 fn test_expand_object_gets_default_type_params() {
1602 let code = "<?php
1603 /** @template T */
1604 class Container {}
1605 ";
1606 let codebase = create_test_codebase(code);
1607
1608 let named = TNamedObject::new(ascii_lowercase_word(b"container"));
1609 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1610
1611 let mut actual = input;
1612 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1613
1614 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1615 assert!(named.type_parameters.is_some());
1616 }
1617 }
1618
1619 #[test]
1620 fn test_expand_object_intersection_from_static() {
1621 let code = "<?php
1622 interface Stringable {}
1623 class Foo implements Stringable {}
1624 ";
1625 let codebase = create_test_codebase(code);
1626
1627 let input = make_static_object();
1628
1629 let mut static_named = TNamedObject::new(ascii_lowercase_word(b"foo"));
1630 static_named.intersection_types =
1631 Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_word(b"stringable"))))]);
1632 let static_obj = TObject::Named(static_named);
1633 let options = options_with_static_object(static_obj);
1634
1635 let mut actual = input;
1636 expand_union(&codebase, &mut actual, &options);
1637
1638 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1639 assert!(named.intersection_types.is_some());
1640 }
1641 }
1642
1643 #[test]
1644 fn test_expand_self_without_self_class_option() {
1645 let codebase = CodebaseMetadata::new();
1646
1647 let input = make_self_object();
1648 let mut actual = input;
1649 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1650
1651 assert!(actual.types.iter().any(|t| {
1652 if let TAtomic::Object(TObject::Named(named)) = t { named.name == word("self") } else { false }
1653 }));
1654 }
1655
1656 #[test]
1657 fn test_expand_callable_return_type() {
1658 let code = "<?php class Foo {}";
1659 let codebase = create_test_codebase(code);
1660
1661 let sig = TCallableSignature::new(false, false).with_return_type(Some(Arc::new(make_self_object())));
1662 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1663
1664 let options = options_with_self("Foo");
1665 let mut actual = input;
1666 expand_union(&codebase, &mut actual, &options);
1667
1668 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1669 && let Some(ret) = sig.get_return_type()
1670 {
1671 assert!(ret.types.iter().any(|t| {
1672 if let TAtomic::Object(TObject::Named(named)) = t {
1673 named.name == ascii_lowercase_word(b"foo")
1674 } else {
1675 false
1676 }
1677 }));
1678 }
1679 }
1680
1681 #[test]
1682 fn test_expand_callable_parameter_types() {
1683 let code = "<?php class Foo {}";
1684 let codebase = create_test_codebase(code);
1685
1686 let param = TCallableParameter::new(Some(Arc::new(make_self_object())), false, false, false);
1687 let sig = TCallableSignature::new(false, false).with_parameters(vec![param]);
1688 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1689
1690 let options = options_with_self("Foo");
1691 let mut actual = input;
1692 expand_union(&codebase, &mut actual, &options);
1693
1694 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1695 && let Some(param) = sig.get_parameters().first()
1696 && let Some(param_type) = param.get_type_signature()
1697 {
1698 assert!(param_type.types.iter().any(|t| {
1699 if let TAtomic::Object(TObject::Named(named)) = t {
1700 named.name == ascii_lowercase_word(b"foo")
1701 } else {
1702 false
1703 }
1704 }));
1705 }
1706 }
1707
1708 #[test]
1709 fn test_expand_callable_alias_to_function() {
1710 let code = "<?php
1711 function myFunc(): int { return 1; }
1712 ";
1713 let codebase = create_test_codebase(code);
1714
1715 let alias = TCallable::Alias(FunctionLikeIdentifier::Function(ascii_lowercase_word(b"myfunc")));
1716 let input = TUnion::from_atomic(TAtomic::Callable(alias));
1717
1718 let mut actual = input;
1719 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1720
1721 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1722 }
1723
1724 #[test]
1725 fn test_expand_callable_alias_to_method() {
1726 let code = "<?php
1727 class Foo {
1728 public function bar(): int { return 1; }
1729 }
1730 ";
1731 let codebase = create_test_codebase(code);
1732
1733 let alias = TCallable::Alias(FunctionLikeIdentifier::Method(
1734 ascii_lowercase_word(b"foo"),
1735 ascii_lowercase_word(b"bar"),
1736 ));
1737 let input = TUnion::from_atomic(TAtomic::Callable(alias));
1738
1739 let mut actual = input;
1740 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1741
1742 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1743 }
1744
1745 #[test]
1746 fn test_expand_callable_alias_unknown() {
1747 let codebase = CodebaseMetadata::new();
1748
1749 let alias = TCallable::Alias(FunctionLikeIdentifier::Function(word("nonexistent")));
1750 let input = TUnion::from_atomic(TAtomic::Callable(alias));
1751
1752 let mut actual = input;
1753 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1754
1755 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Alias(_)))));
1756 }
1757
1758 #[test]
1759 fn test_expand_closure_signature() {
1760 let code = "<?php class Foo {}";
1761 let codebase = create_test_codebase(code);
1762
1763 let sig = TCallableSignature::new(false, true).with_return_type(Some(Arc::new(make_self_object())));
1764 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1765
1766 let options = options_with_self("Foo");
1767 let mut actual = input;
1768 expand_union(&codebase, &mut actual, &options);
1769
1770 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1771 && let Some(ret) = sig.get_return_type()
1772 {
1773 assert!(ret.types.iter().any(|t| {
1774 if let TAtomic::Object(TObject::Named(named)) = t {
1775 named.name == ascii_lowercase_word(b"foo")
1776 } else {
1777 false
1778 }
1779 }));
1780 }
1781 }
1782
1783 #[test]
1784 fn test_expand_generic_parameter_constraint() {
1785 let code = "<?php class Foo {}";
1786 let codebase = create_test_codebase(code);
1787
1788 let generic = TGenericParameter::new(
1789 word("T"),
1790 Arc::new(make_self_object()),
1791 GenericParent::ClassLike(ascii_lowercase_word(b"foo")),
1792 );
1793 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1794
1795 let options = options_with_self("Foo");
1796 let mut actual = input;
1797 expand_union(&codebase, &mut actual, &options);
1798
1799 if let TAtomic::GenericParameter(param) = &actual.types[0] {
1800 assert!(param.constraint.types.iter().any(|t| {
1801 if let TAtomic::Object(TObject::Named(named)) = t {
1802 named.name == ascii_lowercase_word(b"foo")
1803 } else {
1804 false
1805 }
1806 }));
1807 }
1808 }
1809
1810 #[test]
1811 fn test_expand_nested_generic_constraint() {
1812 let code = "<?php class Foo {} class Bar {}";
1813 let codebase = create_test_codebase(code);
1814
1815 let container =
1816 TNamedObject::new_with_type_parameters(ascii_lowercase_word(b"container"), Some(vec![make_self_object()]));
1817 let constraint = TUnion::from_atomic(TAtomic::Object(TObject::Named(container)));
1818
1819 let generic = TGenericParameter::new(
1820 word("T"),
1821 Arc::new(constraint),
1822 GenericParent::ClassLike(ascii_lowercase_word(b"bar")),
1823 );
1824 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1825
1826 let options = options_with_self("Foo");
1827 let mut actual = input;
1828 expand_union(&codebase, &mut actual, &options);
1829
1830 if let TAtomic::GenericParameter(param) = &actual.types[0]
1831 && let TAtomic::Object(TObject::Named(named)) = ¶m.constraint.types[0]
1832 && let Some(params) = &named.type_parameters
1833 {
1834 assert!(params[0].types.iter().any(|t| {
1835 if let TAtomic::Object(TObject::Named(named)) = t {
1836 named.name == ascii_lowercase_word(b"foo")
1837 } else {
1838 false
1839 }
1840 }));
1841 }
1842 }
1843
1844 #[test]
1845 fn test_expand_generic_with_intersection() {
1846 let code = "<?php
1847 interface Stringable {}
1848 class Foo {}
1849 ";
1850 let codebase = create_test_codebase(code);
1851
1852 let mut generic = TGenericParameter::new(
1853 word("T"),
1854 Arc::new(make_self_object()),
1855 GenericParent::ClassLike(ascii_lowercase_word(b"foo")),
1856 );
1857 generic.intersection_types =
1858 Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_word(b"stringable"))))]);
1859 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1860
1861 let options = options_with_self("Foo");
1862 let mut actual = input;
1863 expand_union(&codebase, &mut actual, &options);
1864
1865 if let TAtomic::GenericParameter(param) = &actual.types[0] {
1866 assert!(param.intersection_types.is_some());
1867 assert!(param.constraint.types.iter().any(|t| {
1868 if let TAtomic::Object(TObject::Named(named)) = t {
1869 named.name == ascii_lowercase_word(b"foo")
1870 } else {
1871 false
1872 }
1873 }));
1874 }
1875 }
1876
1877 #[test]
1878 fn test_expand_class_string_of_self() {
1879 let code = "<?php class Foo {}";
1880 let codebase = create_test_codebase(code);
1881
1882 let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(word("self")))));
1883 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1884 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1885
1886 let options = options_with_self("Foo");
1887 let mut actual = input;
1888 expand_union(&codebase, &mut actual, &options);
1889
1890 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1891 && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1892 {
1893 assert_eq!(named.name, ascii_lowercase_word(b"foo"));
1894 }
1895 }
1896
1897 #[test]
1898 fn test_expand_class_string_of_static() {
1899 let code = "<?php class Foo {}";
1900 let codebase = create_test_codebase(code);
1901
1902 let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(word("static")))));
1903 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1904 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1905
1906 let options = options_with_static("Foo");
1907 let mut actual = input;
1908 expand_union(&codebase, &mut actual, &options);
1909
1910 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1911 && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1912 {
1913 assert_eq!(named.name, ascii_lowercase_word(b"foo"));
1914 }
1915 }
1916
1917 #[test]
1918 fn test_expand_interface_string_of_type() {
1919 let code = "<?php interface MyInterface {}";
1920 let codebase = create_test_codebase(code);
1921
1922 let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(word("self")))));
1923 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Interface, constraint };
1924 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1925
1926 let options = options_with_self("MyInterface");
1927 let mut actual = input;
1928 expand_union(&codebase, &mut actual, &options);
1929
1930 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { kind, constraint })) =
1931 &actual.types[0]
1932 {
1933 assert!(matches!(kind, TClassLikeStringKind::Interface));
1934 if let TAtomic::Object(TObject::Named(named)) = constraint.as_ref() {
1935 assert_eq!(named.name, ascii_lowercase_word(b"myinterface"));
1936 }
1937 }
1938 }
1939
1940 #[test]
1941 fn test_expand_member_reference_wildcard_constants() {
1942 let code = "<?php
1943 class Foo {
1944 public const A = 1;
1945 public const B = 2;
1946 }
1947 ";
1948 let codebase = create_test_codebase(code);
1949
1950 let reference = TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::Wildcard);
1951 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1952
1953 let mut actual = input;
1954 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1955
1956 assert!(!actual.types.is_empty());
1957 }
1958
1959 #[test]
1960 fn test_expand_member_reference_wildcard_enum_cases() {
1961 let code = "<?php
1962 enum Status {
1963 case Active;
1964 case Inactive;
1965 }
1966 ";
1967 let codebase = create_test_codebase(code);
1968
1969 let reference = TReference::new_member(ascii_lowercase_word(b"status"), TReferenceMemberSelector::Wildcard);
1970 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1971
1972 let mut actual = input;
1973 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1974
1975 assert_eq!(actual.types.len(), 2);
1976 assert!(actual.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1977 }
1978
1979 #[test]
1980 fn test_expand_member_reference_starts_with() {
1981 let code = "<?php
1982 class Foo {
1983 public const STATUS_ACTIVE = 1;
1984 public const STATUS_INACTIVE = 2;
1985 public const OTHER = 3;
1986 }
1987 ";
1988 let codebase = create_test_codebase(code);
1989
1990 let reference =
1991 TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::StartsWith(word("STATUS_")));
1992 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1993
1994 let mut actual = input;
1995 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1996
1997 assert!(!actual.types.is_empty());
1998 }
1999
2000 #[test]
2001 fn test_expand_member_reference_ends_with() {
2002 let code = "<?php
2003 class Foo {
2004 public const READ_ERROR = 1;
2005 public const WRITE_ERROR = 2;
2006 public const SUCCESS = 0;
2007 }
2008 ";
2009 let codebase = create_test_codebase(code);
2010
2011 let reference =
2012 TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::EndsWith(word("_ERROR")));
2013 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2014
2015 let mut actual = input;
2016 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2017
2018 assert!(!actual.types.is_empty());
2019 }
2020
2021 #[test]
2022 fn test_expand_member_reference_identifier_constant() {
2023 let code = "<?php
2024 class Foo {
2025 public const BAR = 42;
2026 }
2027 ";
2028 let codebase = create_test_codebase(code);
2029
2030 let reference =
2031 TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::Identifier(word("BAR")));
2032 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2033
2034 let mut actual = input;
2035 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2036
2037 assert_eq!(actual.types.len(), 1);
2038 }
2039
2040 #[test]
2041 fn test_expand_member_reference_identifier_enum_case() {
2042 let code = "<?php
2043 enum Status {
2044 case Active;
2045 }
2046 ";
2047 let codebase = create_test_codebase(code);
2048
2049 let reference = TReference::new_member(
2050 ascii_lowercase_word(b"status"),
2051 TReferenceMemberSelector::Identifier(word("Active")),
2052 );
2053 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2054
2055 let mut actual = input;
2056 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2057
2058 assert_eq!(actual.types.len(), 1);
2059 assert!(matches!(&actual.types[0], TAtomic::Object(TObject::Enum(_))));
2060 }
2061
2062 #[test]
2063 fn test_expand_member_reference_unknown_class() {
2064 let codebase = CodebaseMetadata::new();
2065
2066 let reference = TReference::new_member(word("NonExistent"), TReferenceMemberSelector::Identifier(word("FOO")));
2067 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2068
2069 let mut actual = input;
2070 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2071
2072 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
2073 }
2074
2075 #[test]
2076 fn test_expand_member_reference_unknown_member() {
2077 let code = "<?php class Foo {}";
2078 let codebase = create_test_codebase(code);
2079
2080 let reference = TReference::new_member(
2081 ascii_lowercase_word(b"foo"),
2082 TReferenceMemberSelector::Identifier(word("NONEXISTENT")),
2083 );
2084 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2085
2086 let mut actual = input;
2087 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2088
2089 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
2090 }
2091
2092 #[test]
2093 fn test_expand_member_reference_constant_with_inferred_type() {
2094 let code = r#"<?php
2095 class Foo {
2096 public const VALUE = "hello";
2097 }
2098 "#;
2099 let codebase = create_test_codebase(code);
2100
2101 let reference =
2102 TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::Identifier(word("VALUE")));
2103 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2104
2105 let mut actual = input;
2106 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2107
2108 assert_eq!(actual.types.len(), 1);
2109 }
2110
2111 #[test]
2112 fn test_expand_member_reference_constant_with_type_metadata() {
2113 let code = "<?php
2114 class Foo {
2115 /** @var int */
2116 public const VALUE = 42;
2117 }
2118 ";
2119 let codebase = create_test_codebase(code);
2120
2121 let reference =
2122 TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::Identifier(word("VALUE")));
2123 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2124
2125 let mut actual = input;
2126 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2127
2128 assert_eq!(actual.types.len(), 1);
2129 }
2130
2131 #[test]
2132 fn test_expand_conditional_both_branches() {
2133 let code = "<?php class Foo {} class Bar {}";
2134 let codebase = create_test_codebase(code);
2135
2136 let conditional = TConditional::new(
2137 Arc::new(get_mixed()),
2138 Arc::new(get_string()),
2139 Arc::new(make_self_object()),
2140 Arc::new(make_self_object()),
2141 false,
2142 );
2143 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
2144
2145 let options = options_with_self("Foo");
2146 let mut actual = input;
2147 expand_union(&codebase, &mut actual, &options);
2148
2149 assert!(actual.types.iter().any(|t| {
2150 if let TAtomic::Object(TObject::Named(named)) = t {
2151 named.name == ascii_lowercase_word(b"foo")
2152 } else {
2153 false
2154 }
2155 }));
2156 }
2157
2158 #[test]
2159 fn test_expand_conditional_with_self_in_then() {
2160 let code = "<?php class Foo {}";
2161 let codebase = create_test_codebase(code);
2162
2163 let conditional = TConditional::new(
2164 Arc::new(get_mixed()),
2165 Arc::new(get_string()),
2166 Arc::new(make_self_object()),
2167 Arc::new(get_int()),
2168 false,
2169 );
2170 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
2171
2172 let options = options_with_self("Foo");
2173 let mut actual = input;
2174 expand_union(&codebase, &mut actual, &options);
2175
2176 assert!(!actual.types.is_empty());
2177 }
2178
2179 #[test]
2180 fn test_expand_conditional_with_self_in_otherwise() {
2181 let code = "<?php class Foo {}";
2182 let codebase = create_test_codebase(code);
2183
2184 let conditional = TConditional::new(
2185 Arc::new(get_mixed()),
2186 Arc::new(get_string()),
2187 Arc::new(get_int()),
2188 Arc::new(make_self_object()),
2189 false,
2190 );
2191 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
2192
2193 let options = options_with_self("Foo");
2194 let mut actual = input;
2195 expand_union(&codebase, &mut actual, &options);
2196
2197 assert!(!actual.types.is_empty());
2198 }
2199
2200 #[test]
2201 fn test_expand_simple_alias() {
2202 let code = "<?php
2203 class Foo {
2204 /** @phpstan-type MyInt = int */
2205 }
2206 ";
2207 let codebase = create_test_codebase(code);
2208
2209 let alias = TAlias::new(ascii_lowercase_word(b"foo"), word("MyInt"));
2210 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2211
2212 let mut actual = input;
2213 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2214
2215 assert!(!actual.types.is_empty());
2216 }
2217
2218 #[test]
2219 fn test_expand_nested_alias() {
2220 let code = "<?php
2221 class Foo {
2222 /** @phpstan-type Inner = int */
2223 /** @phpstan-type Outer = Inner */
2224 }
2225 ";
2226 let codebase = create_test_codebase(code);
2227
2228 let alias = TAlias::new(ascii_lowercase_word(b"foo"), word("Outer"));
2229 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2230
2231 let mut actual = input;
2232 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2233
2234 assert!(!actual.types.is_empty());
2235 }
2236
2237 #[test]
2238 fn test_expand_alias_cycle_detection() {
2239 let code = "<?php
2240 /** @phpstan-type SelfRef = int|array<int, SelfRef> */
2241 class Foo {}
2242 ";
2243 let codebase = create_test_codebase(code);
2244
2245 let alias = TAlias::new(ascii_lowercase_word(b"foo"), word("SelfRef"));
2246 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2247
2248 let mut actual = input;
2249 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2250
2251 assert!(!actual.types.is_empty());
2252 }
2253
2254 #[test]
2255 fn test_expand_alias_unknown() {
2256 let codebase = CodebaseMetadata::new();
2257
2258 let alias = TAlias::new(word("NonExistent"), word("Unknown"));
2259 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2260
2261 let mut actual = input;
2262 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2263
2264 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Alias(_))));
2265 }
2266
2267 #[test]
2268 fn test_expand_alias_with_self_inside() {
2269 let code = "<?php
2270 class Foo {
2271 /** @phpstan-type MySelf = self */
2272 }
2273 ";
2274 let codebase = create_test_codebase(code);
2275
2276 let alias = TAlias::new(ascii_lowercase_word(b"foo"), word("MySelf"));
2277 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2278
2279 let options = options_with_self("Foo");
2280 let mut actual = input;
2281 expand_union(&codebase, &mut actual, &options);
2282
2283 assert!(!actual.types.is_empty());
2284 }
2285
2286 #[test]
2287 fn test_expand_key_of_array() {
2288 let codebase = CodebaseMetadata::new();
2289
2290 let mut keyed = TKeyedArray::new();
2291 keyed.parameters = Some((Arc::new(get_string()), Arc::new(get_int())));
2292 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2293
2294 let key_of = TKeyOf::new(Arc::new(array_type));
2295 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
2296
2297 let mut actual = input;
2298 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2299
2300 assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_string));
2301 }
2302
2303 #[test]
2304 fn test_expand_key_of_with_self() {
2305 let code = "<?php class Foo {}";
2306 let codebase = create_test_codebase(code);
2307
2308 let mut keyed = TKeyedArray::new();
2309 keyed.parameters = Some((Arc::new(make_self_object()), Arc::new(get_int())));
2310 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2311
2312 let key_of = TKeyOf::new(Arc::new(array_type));
2313 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
2314
2315 let options = options_with_self("Foo");
2316 let mut actual = input;
2317 expand_union(&codebase, &mut actual, &options);
2318
2319 assert!(!actual.types.is_empty());
2320 }
2321
2322 #[test]
2323 fn test_expand_value_of_array() {
2324 let codebase = CodebaseMetadata::new();
2325
2326 let mut keyed = TKeyedArray::new();
2327 keyed.parameters = Some((Arc::new(get_string()), Arc::new(get_int())));
2328 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2329
2330 let value_of = TValueOf::new(Arc::new(array_type));
2331 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
2332
2333 let mut actual = input;
2334 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2335
2336 assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_int));
2337 }
2338
2339 #[test]
2340 fn test_expand_value_of_enum() {
2341 let code = "<?php
2342 enum Status: string {
2343 case Active = 'active';
2344 case Inactive = 'inactive';
2345 }
2346 ";
2347 let codebase = create_test_codebase(code);
2348
2349 let enum_type =
2350 TUnion::from_atomic(TAtomic::Object(TObject::Enum(TEnum::new(ascii_lowercase_word(b"status")))));
2351
2352 let value_of = TValueOf::new(Arc::new(enum_type));
2353 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
2354
2355 let mut actual = input;
2356 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2357
2358 assert!(!actual.types.is_empty());
2359 }
2360
2361 #[test]
2362 fn test_expand_index_access() {
2363 let codebase = CodebaseMetadata::new();
2364
2365 use crate::ttype::atomic::array::key::ArrayKey;
2366 use std::collections::BTreeMap;
2367
2368 let mut keyed = TKeyedArray::new();
2369 let mut known_items = BTreeMap::new();
2370 known_items.insert(ArrayKey::String(word("key")), (false, get_int()));
2371 keyed.known_items = Some(known_items);
2372 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2373
2374 use crate::ttype::get_literal_string;
2375 let index_type = get_literal_string(word("key"));
2376
2377 let index_access = TIndexAccess::new(array_type, index_type);
2378 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2379
2380 let mut actual = input;
2381 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2382
2383 assert!(!actual.types.is_empty());
2384 }
2385
2386 #[test]
2387 fn test_expand_index_access_with_self() {
2388 let code = "<?php class Foo {}";
2389 let codebase = create_test_codebase(code);
2390
2391 use crate::ttype::atomic::array::key::ArrayKey;
2392 use std::collections::BTreeMap;
2393
2394 let mut keyed = TKeyedArray::new();
2395 let mut known_items = BTreeMap::new();
2396 known_items.insert(ArrayKey::String(word("key")), (false, make_self_object()));
2397 keyed.known_items = Some(known_items);
2398 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2399
2400 use crate::ttype::get_literal_string;
2401 let index_type = get_literal_string(word("key"));
2402
2403 let index_access = TIndexAccess::new(array_type, index_type);
2404 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2405
2406 let options = options_with_self("Foo");
2407 let mut actual = input;
2408 expand_union(&codebase, &mut actual, &options);
2409
2410 assert!(!actual.types.is_empty());
2411 }
2412
2413 #[test]
2414 fn test_expand_iterable_key_type() {
2415 let code = "<?php class Foo {}";
2416 let codebase = create_test_codebase(code);
2417
2418 let iterable = TIterable::new(Arc::new(make_self_object()), Arc::new(get_int()));
2419 let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2420
2421 let options = options_with_self("Foo");
2422 let mut actual = input;
2423 expand_union(&codebase, &mut actual, &options);
2424
2425 if let TAtomic::Iterable(iter) = &actual.types[0] {
2426 assert!(iter.get_key_type().types.iter().any(|t| {
2427 if let TAtomic::Object(TObject::Named(named)) = t {
2428 named.name == ascii_lowercase_word(b"foo")
2429 } else {
2430 false
2431 }
2432 }));
2433 }
2434 }
2435
2436 #[test]
2437 fn test_expand_iterable_value_type() {
2438 let code = "<?php class Foo {}";
2439 let codebase = create_test_codebase(code);
2440
2441 let iterable = TIterable::new(Arc::new(get_int()), Arc::new(make_self_object()));
2442 let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2443
2444 let options = options_with_self("Foo");
2445 let mut actual = input;
2446 expand_union(&codebase, &mut actual, &options);
2447
2448 if let TAtomic::Iterable(iter) = &actual.types[0] {
2449 assert!(iter.get_value_type().types.iter().any(|t| {
2450 if let TAtomic::Object(TObject::Named(named)) = t {
2451 named.name == ascii_lowercase_word(b"foo")
2452 } else {
2453 false
2454 }
2455 }));
2456 }
2457 }
2458
2459 #[test]
2460 fn test_get_signature_of_function() {
2461 let code = r#"<?php
2462 function myFunc(int $a): string { return ""; }
2463 "#;
2464 let codebase = create_test_codebase(code);
2465
2466 let id = FunctionLikeIdentifier::Function(ascii_lowercase_word(b"myfunc"));
2467
2468 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2469 assert!(sig.is_some());
2470
2471 let sig = sig.unwrap();
2472 assert_eq!(sig.get_parameters().len(), 1);
2473 assert!(sig.get_return_type().is_some());
2474 }
2475
2476 #[test]
2477 fn test_get_signature_of_method() {
2478 let code = "<?php
2479 class Foo {
2480 public function bar(string $s): int { return 0; }
2481 }
2482 ";
2483 let codebase = create_test_codebase(code);
2484
2485 let id = FunctionLikeIdentifier::Method(ascii_lowercase_word(b"foo"), ascii_lowercase_word(b"bar"));
2486
2487 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2488 assert!(sig.is_some());
2489
2490 let sig = sig.unwrap();
2491 assert_eq!(sig.get_parameters().len(), 1);
2492 }
2493
2494 #[test]
2495 fn test_get_signature_of_closure() {
2496 let codebase = CodebaseMetadata::new();
2497
2498 let id = FunctionLikeIdentifier::Closure(FileId::new(b"test"), Position::new(0));
2499 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2500
2501 assert!(sig.is_none());
2502 }
2503
2504 #[test]
2505 fn test_get_atomic_of_function() {
2506 let code = "<?php
2507 function myFunc(): void {}
2508 ";
2509 let codebase = create_test_codebase(code);
2510
2511 let id = FunctionLikeIdentifier::Function(ascii_lowercase_word(b"myfunc"));
2512
2513 let atomic = get_atomic_of_function_like_identifier(&id, &codebase);
2514 assert!(atomic.is_some());
2515 assert!(matches!(atomic.unwrap(), TAtomic::Callable(TCallable::Signature(_))));
2516 }
2517
2518 #[test]
2519 fn test_get_signature_with_parameters() {
2520 let code = "<?php
2521 function multiParam(int $a, string $b, ?float $c = null): bool { return true; }
2522 ";
2523 let codebase = create_test_codebase(code);
2524
2525 let id = FunctionLikeIdentifier::Function(ascii_lowercase_word(b"multiparam"));
2526
2527 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2528 assert!(sig.is_some());
2529
2530 let sig = sig.unwrap();
2531 assert_eq!(sig.get_parameters().len(), 3);
2532
2533 let third_param = &sig.get_parameters()[2];
2534 assert!(third_param.has_default());
2535 }
2536
2537 #[test]
2538 fn test_expand_preserves_by_reference_flag() {
2539 let code = "<?php class Foo {}";
2540 let codebase = create_test_codebase(code);
2541
2542 let mut input = make_self_object();
2543 input.flags.insert(UnionFlags::BY_REFERENCE);
2544
2545 let options = options_with_self("Foo");
2546 let mut actual = input.clone();
2547 expand_union(&codebase, &mut actual, &options);
2548
2549 assert!(actual.flags.contains(UnionFlags::BY_REFERENCE));
2550 }
2551
2552 #[test]
2553 fn test_expand_preserves_possibly_undefined_flag() {
2554 let code = "<?php class Foo {}";
2555 let codebase = create_test_codebase(code);
2556
2557 let mut input = make_self_object();
2558 input.flags.insert(UnionFlags::POSSIBLY_UNDEFINED);
2559
2560 let options = options_with_self("Foo");
2561 let mut actual = input.clone();
2562 expand_union(&codebase, &mut actual, &options);
2563
2564 assert!(actual.flags.contains(UnionFlags::POSSIBLY_UNDEFINED));
2565 }
2566
2567 #[test]
2568 fn test_expand_multiple_self_in_union() {
2569 let code = "<?php class Foo {}";
2570 let codebase = create_test_codebase(code);
2571
2572 let input = TUnion::from_vec(vec![
2573 TAtomic::Object(TObject::Named(TNamedObject::new(word("self")))),
2574 TAtomic::Object(TObject::Named(TNamedObject::new(word("self")))),
2575 ]);
2576
2577 let options = options_with_self("Foo");
2578 let mut actual = input;
2579 expand_union(&codebase, &mut actual, &options);
2580
2581 assert!(actual.types.len() <= 2);
2582 }
2583
2584 #[test]
2585 fn test_expand_deeply_nested_types() {
2586 let code = "<?php class Foo {}";
2587 let codebase = create_test_codebase(code);
2588
2589 let inner = TList::new(Arc::new(make_self_object()));
2590 let middle = TList::new(Arc::new(TUnion::from_atomic(TAtomic::Array(TArray::List(inner)))));
2591 let outer = TList::new(Arc::new(TUnion::from_atomic(TAtomic::Array(TArray::List(middle)))));
2592 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(outer)));
2593
2594 let options = options_with_self("Foo");
2595 let mut actual = input;
2596 expand_union(&codebase, &mut actual, &options);
2597
2598 if let TAtomic::Array(TArray::List(outer)) = &actual.types[0]
2599 && let TAtomic::Array(TArray::List(middle)) = &outer.element_type.types[0]
2600 && let TAtomic::Array(TArray::List(inner)) = &middle.element_type.types[0]
2601 {
2602 assert!(inner.element_type.types.iter().any(|t| {
2603 if let TAtomic::Object(TObject::Named(named)) = t {
2604 named.name == ascii_lowercase_word(b"foo")
2605 } else {
2606 false
2607 }
2608 }));
2609 }
2610 }
2611
2612 #[test]
2613 fn test_expand_with_all_options_disabled() {
2614 let code = "<?php class Foo {}";
2615 let codebase = create_test_codebase(code);
2616
2617 let input = make_self_object();
2618 let options = TypeExpansionOptions {
2619 self_class: None,
2620 static_class_type: StaticClassType::None,
2621 parent_class: None,
2622 evaluate_class_constants: false,
2623 evaluate_conditional_types: false,
2624 function_is_final: false,
2625 expand_generic: false,
2626 expand_templates: false,
2627 };
2628
2629 let mut actual = input;
2630 expand_union(&codebase, &mut actual, &options);
2631
2632 assert!(actual.types.iter().any(|t| {
2633 if let TAtomic::Object(TObject::Named(named)) = t { named.name == word("self") } else { false }
2634 }));
2635 }
2636
2637 #[test]
2638 fn test_expand_already_expanded_type() {
2639 let code = "<?php class Foo {}";
2640 let codebase = create_test_codebase(code);
2641
2642 let input = make_named_object("Foo");
2643 let options = options_with_self("Foo");
2644
2645 let mut actual = input;
2646 expand_union(&codebase, &mut actual, &options);
2647
2648 let mut actual2 = actual.clone();
2649 expand_union(&codebase, &mut actual2, &options);
2650
2651 assert_eq!(actual.types.as_ref(), actual2.types.as_ref());
2652 }
2653
2654 #[test]
2655 fn test_expand_complex_generic_class() {
2656 let code = "<?php
2657 /**
2658 * @template T
2659 * @template U
2660 */
2661 class Container {}
2662 ";
2663 let codebase = create_test_codebase(code);
2664
2665 let named = TNamedObject::new_with_type_parameters(
2666 ascii_lowercase_word(b"container"),
2667 Some(vec![make_self_object(), make_static_object()]),
2668 );
2669 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
2670
2671 let options = TypeExpansionOptions {
2672 self_class: Some(ascii_lowercase_word(b"foo")),
2673 static_class_type: StaticClassType::Name(ascii_lowercase_word(b"bar")),
2674 ..Default::default()
2675 };
2676
2677 let mut actual = input;
2678 expand_union(&codebase, &mut actual, &options);
2679
2680 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
2681 && let Some(params) = &named.type_parameters
2682 {
2683 assert!(params[0].types.iter().any(|t| {
2684 if let TAtomic::Object(TObject::Named(named)) = t {
2685 named.name == ascii_lowercase_word(b"foo")
2686 } else {
2687 false
2688 }
2689 }));
2690 assert!(params[1].types.iter().any(|t| {
2691 if let TAtomic::Object(TObject::Named(named)) = t {
2692 named.name == ascii_lowercase_word(b"bar")
2693 } else {
2694 false
2695 }
2696 }));
2697 }
2698 }
2699}