Skip to main content

mago_codex/scanner/inference/
mod.rs

1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::sync::Arc;
4use std::sync::LazyLock;
5
6use mago_atom::Atom;
7use mago_atom::AtomMap;
8use mago_atom::ascii_lowercase_constant_name_atom;
9use mago_atom::atom;
10use mago_atom::concat_atom;
11use mago_names::ResolvedNames;
12use mago_names::scope::NamespaceScope;
13use mago_span::HasPosition;
14use mago_span::HasSpan;
15use mago_syntax::ast::Access;
16use mago_syntax::ast::Array;
17use mago_syntax::ast::ArrayElement;
18use mago_syntax::ast::Binary;
19use mago_syntax::ast::BinaryOperator;
20use mago_syntax::ast::ClassConstantAccess;
21use mago_syntax::ast::ClassLikeConstantSelector;
22use mago_syntax::ast::Construct;
23use mago_syntax::ast::Expression;
24use mago_syntax::ast::Identifier;
25use mago_syntax::ast::LegacyArray;
26use mago_syntax::ast::Literal;
27use mago_syntax::ast::MagicConstant;
28use mago_syntax::ast::StringPart;
29use mago_syntax::ast::UnaryPrefix;
30use mago_syntax::ast::UnaryPrefixOperator;
31
32use crate::flags::attribute::AttributeFlags;
33use crate::identifier::function_like::FunctionLikeIdentifier;
34use crate::metadata::constant::ConstantMetadata;
35use crate::scanner::Context;
36use crate::ttype::atomic::TAtomic;
37use crate::ttype::atomic::array::TArray;
38use crate::ttype::atomic::array::keyed::TKeyedArray;
39use crate::ttype::atomic::array::list::TList;
40use crate::ttype::atomic::callable::TCallable;
41use crate::ttype::atomic::reference::TReference;
42use crate::ttype::atomic::reference::TReferenceMemberSelector;
43use crate::ttype::atomic::scalar::TScalar;
44use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
45use crate::ttype::atomic::scalar::float::TFloat;
46use crate::ttype::atomic::scalar::int::TInteger;
47use crate::ttype::atomic::scalar::string::TString;
48use crate::ttype::atomic::scalar::string::TStringCasing;
49use crate::ttype::atomic::scalar::string::TStringLiteral;
50use crate::ttype::get_arraykey;
51use crate::ttype::get_bool;
52use crate::ttype::get_empty_string;
53use crate::ttype::get_false;
54use crate::ttype::get_float;
55use crate::ttype::get_int;
56use crate::ttype::get_int_or_float;
57use crate::ttype::get_literal_int;
58use crate::ttype::get_literal_string;
59use crate::ttype::get_mixed;
60use crate::ttype::get_mixed_keyed_array;
61use crate::ttype::get_never;
62use crate::ttype::get_non_empty_string;
63use crate::ttype::get_non_negative_int;
64use crate::ttype::get_null;
65use crate::ttype::get_object;
66use crate::ttype::get_open_resource;
67use crate::ttype::get_positive_int;
68use crate::ttype::get_string;
69use crate::ttype::get_true;
70use crate::ttype::get_void;
71use crate::ttype::union::TUnion;
72use crate::ttype::wrap_atomic;
73use crate::utils::str_is_numeric;
74
75/// Returns the type for a predefined literal constant, if known.
76///
77/// These constants (`true`, `false`, `null`) are parsed as `Literal` nodes when bare,
78/// but become `ConstantAccess` nodes when accessed via FQN (e.g. `\true`).
79#[inline]
80pub fn get_literal_constant_type(name: &str) -> Option<TUnion> {
81    let name = name.strip_prefix('\\').unwrap_or(name);
82
83    if name.eq_ignore_ascii_case("true") {
84        Some(get_true())
85    } else if name.eq_ignore_ascii_case("false") {
86        Some(get_false())
87    } else if name.eq_ignore_ascii_case("null") {
88        Some(get_null())
89    } else {
90        None
91    }
92}
93
94/// Returns the platform-aware type for a predefined constant, if known.
95///
96/// These constants have values that vary across platforms (e.g. 32-bit vs 64-bit),
97/// so their types should be ranges or unions rather than host-specific literals.
98#[inline]
99pub fn get_platform_constant_type(name: &str) -> Option<TUnion> {
100    static DIR_SEPARATOR_SLICE: LazyLock<[TAtomic; 2]> = LazyLock::new(|| {
101        [
102            TAtomic::Scalar(TScalar::String(TString {
103                literal: Some(TStringLiteral::Value(atom("/"))),
104                is_numeric: false,
105                is_truthy: true,
106                is_non_empty: true,
107                is_callable: false,
108                casing: TStringCasing::Lowercase,
109            })),
110            TAtomic::Scalar(TScalar::String(TString {
111                literal: Some(TStringLiteral::Value(atom("\\"))),
112                is_numeric: false,
113                is_truthy: true,
114                is_non_empty: true,
115                is_callable: false,
116                casing: TStringCasing::Lowercase,
117            })),
118        ]
119    });
120
121    const PHP_INT_MAX_SLICE: &[TAtomic] = &[
122        TAtomic::Scalar(TScalar::Integer(TInteger::Literal(9_223_372_036_854_775_807))),
123        TAtomic::Scalar(TScalar::Integer(TInteger::Literal(2_147_483_647))),
124    ];
125
126    const PHP_INT_MIN_SLICE: &[TAtomic] = &[
127        TAtomic::Scalar(TScalar::Integer(TInteger::Literal(-9_223_372_036_854_775_808))),
128        TAtomic::Scalar(TScalar::Integer(TInteger::Literal(-2_147_483_648))),
129    ];
130
131    const PHP_MAJOR_VERSION_ATOMIC: &TAtomic = &TAtomic::Scalar(TScalar::Integer(TInteger::Range(8, 9)));
132    const PHP_ZTS_ATOMIC: &TAtomic = &TAtomic::Scalar(TScalar::Integer(TInteger::Range(0, 1)));
133    const PHP_DEBUG_ATOMIC: &TAtomic = &TAtomic::Scalar(TScalar::Integer(TInteger::Range(0, 1)));
134    const PHP_INT_SIZE_ATOMIC: &TAtomic = &TAtomic::Scalar(TScalar::Integer(TInteger::Range(4, 8)));
135    const PHP_WINDOWS_VERSION_MAJOR_ATOMIC: &TAtomic = &TAtomic::Scalar(TScalar::Integer(TInteger::Range(4, 6)));
136    const PHP_WINDOWS_VERSION_MINOR_SLICE: &[TAtomic] = &[
137        TAtomic::Scalar(TScalar::Integer(TInteger::Literal(0))),
138        TAtomic::Scalar(TScalar::Integer(TInteger::Literal(1))),
139        TAtomic::Scalar(TScalar::Integer(TInteger::Literal(2))),
140        TAtomic::Scalar(TScalar::Integer(TInteger::Literal(10))),
141        TAtomic::Scalar(TScalar::Integer(TInteger::Literal(90))),
142    ];
143
144    let name = name.strip_prefix('\\').unwrap_or(name);
145
146    match name {
147        "PHP_MAXPATHLEN"
148        | "PHP_WINDOWS_VERSION_BUILD"
149        | "LIBXML_VERSION"
150        | "OPENSSL_VERSION_NUMBER"
151        | "PHP_FLOAT_DIG" => Some(get_int()),
152        "PHP_EXTRA_VERSION" => Some(get_string()),
153        "PHP_BUILD_DATE"
154        | "PEAR_EXTENSION_DIR"
155        | "PEAR_INSTALL_DIR"
156        | "PHP_BINARY"
157        | "PHP_BINDIR"
158        | "PHP_CONFIG_FILE_PATH"
159        | "PHP_CONFIG_FILE_SCAN_DIR"
160        | "PHP_DATADIR"
161        | "PHP_EXTENSION_DIR"
162        | "PHP_LIBDIR"
163        | "PHP_LOCALSTATEDIR"
164        | "PHP_MANDIR"
165        | "PHP_OS"
166        | "PHP_OS_FAMILY"
167        | "PHP_PREFIX"
168        | "PHP_EOL"
169        | "PATH_SEPARATOR"
170        | "PHP_VERSION"
171        | "PHP_SAPI"
172        | "PHP_SYSCONFDIR"
173        | "ICONV_IMPL"
174        | "LIBXML_DOTTED_VERSION"
175        | "PCRE_VERSION" => Some(get_non_empty_string()),
176        "STDIN" | "STDOUT" | "STDERR" => Some(get_open_resource()),
177        "NAN" | "PHP_FLOAT_EPSILON" | "INF" => Some(get_float()),
178        "PHP_VERSION_ID" => Some(get_positive_int()),
179        "PHP_RELEASE_VERSION" | "PHP_MINOR_VERSION" => Some(get_non_negative_int()),
180        "PHP_MAJOR_VERSION" => Some(TUnion::from_single(Cow::Borrowed(PHP_MAJOR_VERSION_ATOMIC))),
181        "PHP_ZTS" => Some(TUnion::from_single(Cow::Borrowed(PHP_ZTS_ATOMIC))),
182        "PHP_DEBUG" => Some(TUnion::from_single(Cow::Borrowed(PHP_DEBUG_ATOMIC))),
183        "PHP_INT_SIZE" => Some(TUnion::from_single(Cow::Borrowed(PHP_INT_SIZE_ATOMIC))),
184        "PHP_WINDOWS_VERSION_MAJOR" => Some(TUnion::from_single(Cow::Borrowed(PHP_WINDOWS_VERSION_MAJOR_ATOMIC))),
185        "DIRECTORY_SEPARATOR" => Some(TUnion::new(Cow::Borrowed(DIR_SEPARATOR_SLICE.as_slice()))),
186        "PHP_INT_MAX" => Some(TUnion::new(Cow::Borrowed(PHP_INT_MAX_SLICE))),
187        "PHP_INT_MIN" => Some(TUnion::new(Cow::Borrowed(PHP_INT_MIN_SLICE))),
188        "PHP_WINDOWS_VERSION_MINOR" => Some(TUnion::new(Cow::Borrowed(PHP_WINDOWS_VERSION_MINOR_SLICE))),
189        _ => None,
190    }
191}
192
193#[inline]
194pub(super) fn infer<'arena>(
195    context: &Context<'_, 'arena>,
196    scope: &NamespaceScope,
197    expression: &'arena Expression<'arena>,
198    enclosing_class: Option<Atom>,
199) -> Option<TUnion> {
200    infer_with_constants(context, scope, expression, enclosing_class, None)
201}
202
203#[inline]
204pub(super) fn infer_with_constants<'arena>(
205    context: &Context<'_, 'arena>,
206    scope: &NamespaceScope,
207    expression: &'arena Expression<'arena>,
208    enclosing_class: Option<Atom>,
209    constants: Option<&AtomMap<ConstantMetadata>>,
210) -> Option<TUnion> {
211    match expression {
212        Expression::MagicConstant(magic_constant) => Some(match magic_constant {
213            MagicConstant::Line(_) => {
214                get_literal_int(i64::from(context.file.line_number(magic_constant.start_position().offset())) + 1)
215            }
216            MagicConstant::File(_) => {
217                if let Some(path) = context.file.path.as_deref().and_then(|p| p.to_str()) {
218                    get_literal_string(atom(path))
219                } else {
220                    get_non_empty_string()
221                }
222            }
223            MagicConstant::Directory(_) => {
224                if let Some(path) = context.file.path.as_deref().and_then(|p| p.parent()).and_then(|p| p.to_str()) {
225                    get_literal_string(atom(path))
226                } else {
227                    get_non_empty_string()
228                }
229            }
230            MagicConstant::Namespace(_) => {
231                if let Some(namespace_name) = scope.namespace_name() {
232                    get_literal_string(atom(namespace_name))
233                } else {
234                    get_empty_string()
235                }
236            }
237            MagicConstant::Trait(_) => get_string(),
238            MagicConstant::Class(_) => get_string(),
239            MagicConstant::Function(_) | MagicConstant::Method(_) => get_string(),
240            MagicConstant::Property(_) => get_string(),
241        }),
242        Expression::Literal(literal) => match literal {
243            Literal::String(literal_string) => {
244                Some(match literal_string.value {
245                    Some(value) => {
246                        if value.is_empty() {
247                            get_empty_string()
248                        } else if value.len() < 1000 {
249                            wrap_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal(atom(value)))))
250                        } else {
251                            wrap_atomic(TAtomic::Scalar(TScalar::String(TString::unspecified_literal_with_props(
252                                str_is_numeric(value),
253                                true,  // truthy
254                                true,  // not empty
255                                false, // callable, we can't tell here.
256                                if value.chars().all(char::is_lowercase) {
257                                    TStringCasing::Lowercase
258                                } else if value.chars().all(char::is_uppercase) {
259                                    TStringCasing::Uppercase
260                                } else {
261                                    TStringCasing::Unspecified
262                                },
263                            ))))
264                        }
265                    }
266                    None => get_string(),
267                })
268            }
269            Literal::Integer(literal_integer) => Some(match literal_integer.value {
270                Some(value) => get_literal_int(value as i64),
271                None => get_int_or_float(),
272            }),
273            Literal::Float(_) => Some(get_float()),
274            Literal::True(_) => Some(get_true()),
275            Literal::False(_) => Some(get_false()),
276            Literal::Null(_) => Some(get_null()),
277        },
278        Expression::CompositeString(composite_string) => {
279            let mut contains_content = false;
280            for part in composite_string.parts() {
281                if let StringPart::Literal(literal_string_part) = part
282                    && !literal_string_part.value.is_empty()
283                {
284                    contains_content = true;
285                    break;
286                }
287            }
288
289            if contains_content { Some(get_non_empty_string()) } else { Some(get_string()) }
290        }
291        Expression::UnaryPrefix(UnaryPrefix { operator, operand }) => {
292            let operand_type = infer_with_constants(context, scope, operand, enclosing_class, constants)?;
293
294            match operator {
295                UnaryPrefixOperator::Plus(_) => {
296                    Some(if let Some(operand_value) = operand_type.get_single_literal_int_value() {
297                        get_literal_int(operand_value)
298                    } else if let Some(operand_value) = operand_type.get_single_literal_float_value() {
299                        TUnion::from_single(Cow::Owned(TAtomic::Scalar(TScalar::Float(TFloat::literal(operand_value)))))
300                    } else {
301                        operand_type
302                    })
303                }
304                UnaryPrefixOperator::Negation(_) => {
305                    Some(if let Some(operand_value) = operand_type.get_single_literal_int_value() {
306                        get_literal_int(operand_value.wrapping_neg())
307                    } else if let Some(operand_value) = operand_type.get_single_literal_float_value() {
308                        TUnion::from_single(Cow::Owned(TAtomic::Scalar(TScalar::Float(TFloat::literal(
309                            -operand_value,
310                        )))))
311                    } else {
312                        operand_type
313                    })
314                }
315                UnaryPrefixOperator::ArrayCast(_, _) => Some(get_mixed_keyed_array()),
316                UnaryPrefixOperator::BoolCast(_, _) => Some(get_bool()),
317                UnaryPrefixOperator::BooleanCast(_, _) => Some(get_bool()),
318                UnaryPrefixOperator::DoubleCast(_, _) => Some(get_float()),
319                UnaryPrefixOperator::RealCast(_, _) => Some(get_float()),
320                UnaryPrefixOperator::FloatCast(_, _) => Some(get_float()),
321                UnaryPrefixOperator::IntCast(_, _) => Some(get_int()),
322                UnaryPrefixOperator::IntegerCast(_, _) => Some(get_int()),
323                UnaryPrefixOperator::ObjectCast(_, _) => Some(get_object()),
324                UnaryPrefixOperator::UnsetCast(_, _) => Some(get_null()),
325                UnaryPrefixOperator::StringCast(_, _) => Some(get_string()),
326                UnaryPrefixOperator::BinaryCast(_, _) => Some(get_string()),
327                UnaryPrefixOperator::VoidCast(_, _) => Some(get_void()),
328                UnaryPrefixOperator::Not(_) => Some(get_bool()),
329                _ => None,
330            }
331        }
332        Expression::Binary(Binary { operator: BinaryOperator::StringConcat(_), lhs, rhs }) => {
333            let Some(lhs_type) = infer_with_constants(context, scope, lhs, enclosing_class, constants) else {
334                return Some(get_string());
335            };
336            let Some(rhs_type) = infer_with_constants(context, scope, rhs, enclosing_class, constants) else {
337                return Some(get_string());
338            };
339
340            let lhs_string = match lhs_type.get_single_owned() {
341                TAtomic::Scalar(TScalar::String(s)) => s,
342                _ => return Some(get_string()),
343            };
344
345            let rhs_string = match rhs_type.get_single_owned() {
346                TAtomic::Scalar(TScalar::String(s)) => s,
347                _ => return Some(get_string()),
348            };
349
350            if let (Some(left_val), Some(right_val)) =
351                (lhs_string.get_known_literal_value(), rhs_string.get_known_literal_value())
352            {
353                return Some(wrap_atomic(TAtomic::Scalar(TScalar::String(TString::known_literal(concat_atom!(
354                    left_val, right_val
355                ))))));
356            }
357
358            let is_non_empty = lhs_string.is_non_empty() || rhs_string.is_non_empty();
359            let is_truthy = lhs_string.is_truthy() || rhs_string.is_truthy();
360            let is_literal_origin = lhs_string.is_literal_origin() && rhs_string.is_literal_origin();
361            let casing = match (lhs_string.casing, rhs_string.casing) {
362                (TStringCasing::Lowercase, TStringCasing::Lowercase) => TStringCasing::Lowercase,
363                (TStringCasing::Uppercase, TStringCasing::Uppercase) => TStringCasing::Uppercase,
364                _ => TStringCasing::Unspecified,
365            };
366
367            let final_string_type = if is_literal_origin {
368                TString::unspecified_literal_with_props(false, is_truthy, is_non_empty, false, casing)
369            } else {
370                TString::general_with_props(false, is_truthy, is_non_empty, false, casing)
371            };
372
373            Some(wrap_atomic(TAtomic::Scalar(TScalar::String(final_string_type))))
374        }
375        Expression::Binary(Binary { operator, lhs, rhs }) if operator.is_bitwise() => {
376            let lhs = infer_with_constants(context, scope, lhs, enclosing_class, constants);
377            let rhs = infer_with_constants(context, scope, rhs, enclosing_class, constants);
378
379            Some(wrap_atomic(
380                match (
381                    lhs.and_then(|v| v.get_single_literal_int_value()),
382                    rhs.and_then(|v| v.get_single_literal_int_value()),
383                ) {
384                    (Some(lhs), Some(rhs)) => {
385                        let value = match operator {
386                            BinaryOperator::BitwiseAnd(_) => lhs & rhs,
387                            BinaryOperator::BitwiseOr(_) => lhs | rhs,
388                            BinaryOperator::BitwiseXor(_) => lhs ^ rhs,
389                            BinaryOperator::LeftShift(_) => {
390                                if rhs < 0 {
391                                    return Some(get_int());
392                                }
393
394                                u32::try_from(rhs).ok().and_then(|s| lhs.checked_shl(s)).unwrap_or_default()
395                            }
396                            BinaryOperator::RightShift(_) => {
397                                if rhs < 0 {
398                                    return Some(get_int());
399                                }
400
401                                match u32::try_from(rhs).ok().and_then(|s| lhs.checked_shr(s)) {
402                                    Some(v) => v,
403                                    None => {
404                                        if lhs >= 0 {
405                                            0
406                                        } else {
407                                            -1
408                                        }
409                                    }
410                                }
411                            }
412                            _ => {
413                                unreachable!("unexpected bitwise operator: {:?}", operator);
414                            }
415                        };
416
417                        TAtomic::Scalar(TScalar::literal_int(value))
418                    }
419                    _ => TAtomic::Scalar(TScalar::int()),
420                },
421            ))
422        }
423        Expression::Binary(Binary { operator, lhs, rhs }) if operator.is_arithmetic() => {
424            let lhs = infer_with_constants(context, scope, lhs, enclosing_class, constants);
425            let rhs = infer_with_constants(context, scope, rhs, enclosing_class, constants);
426
427            match (
428                lhs.and_then(|v| v.get_single_literal_int_value()),
429                rhs.and_then(|v| v.get_single_literal_int_value()),
430            ) {
431                (Some(lhs_val), Some(rhs_val)) => {
432                    let result = match operator {
433                        BinaryOperator::Addition(_) => lhs_val.checked_add(rhs_val),
434                        BinaryOperator::Subtraction(_) => lhs_val.checked_sub(rhs_val),
435                        BinaryOperator::Multiplication(_) => lhs_val.checked_mul(rhs_val),
436                        BinaryOperator::Modulo(_) if rhs_val != 0 => Some(lhs_val % rhs_val),
437                        BinaryOperator::Exponentiation(_) if rhs_val >= 0 => lhs_val.checked_pow(rhs_val as u32),
438                        BinaryOperator::Division(_) if rhs_val != 0 && lhs_val % rhs_val == 0 => {
439                            Some(lhs_val / rhs_val)
440                        }
441                        _ => None,
442                    };
443
444                    match result {
445                        Some(v) => Some(get_literal_int(v)),
446                        None => Some(get_int_or_float()),
447                    }
448                }
449                // Can't compute - return int|float
450                _ => Some(get_int_or_float()),
451            }
452        }
453        Expression::Construct(construct) => match construct {
454            Construct::Isset(_) => Some(get_bool()),
455            Construct::Empty(_) => Some(get_bool()),
456            Construct::Print(_) => Some(get_literal_int(1)),
457            _ => None,
458        },
459        Expression::ConstantAccess(access) => infer_constant(context.resolved_names, &access.name, constants),
460        Expression::Access(Access::ClassConstant(ClassConstantAccess {
461            class,
462            constant: ClassLikeConstantSelector::Identifier(identifier),
463            ..
464        })) => {
465            let class_name_str = if let Expression::Identifier(identifier) = class {
466                context.resolved_names.get(identifier)
467            } else if matches!(class, Expression::Self_(_) | Expression::Static(_)) {
468                enclosing_class.as_ref().map(Atom::as_str)?
469            } else {
470                return None;
471            };
472
473            Some(wrap_atomic(if identifier.value.eq_ignore_ascii_case("class") {
474                TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::literal(atom(class_name_str))))
475            } else if class_name_str.eq_ignore_ascii_case("Attribute") {
476                let bits = match identifier.value {
477                    "TARGET_CLASS" => Some(AttributeFlags::TARGET_CLASS.bits()),
478                    "TARGET_FUNCTION" => Some(AttributeFlags::TARGET_FUNCTION.bits()),
479                    "TARGET_METHOD" => Some(AttributeFlags::TARGET_METHOD.bits()),
480                    "TARGET_PROPERTY" => Some(AttributeFlags::TARGET_PROPERTY.bits()),
481                    "TARGET_CLASS_CONSTANT" => Some(AttributeFlags::TARGET_CLASS_CONSTANT.bits()),
482                    "TARGET_PARAMETER" => Some(AttributeFlags::TARGET_PARAMETER.bits()),
483                    "TARGET_CONSTANT" => Some(AttributeFlags::TARGET_CONSTANT.bits()),
484                    "TARGET_ALL" => Some(AttributeFlags::TARGET_ALL.bits()),
485                    "IS_REPEATABLE" => Some(AttributeFlags::IS_REPEATABLE.bits()),
486                    _ => None,
487                };
488
489                match bits {
490                    Some(bits) => return Some(get_literal_int(i64::from(bits))),
491                    None => TAtomic::Reference(TReference::Member {
492                        class_like_name: atom(class_name_str),
493                        member_selector: TReferenceMemberSelector::Identifier(atom(identifier.value)),
494                    }),
495                }
496            } else {
497                TAtomic::Reference(TReference::Member {
498                    class_like_name: atom(class_name_str),
499                    member_selector: TReferenceMemberSelector::Identifier(atom(identifier.value)),
500                })
501            }))
502        }
503        Expression::Array(Array { elements, .. }) | Expression::LegacyArray(LegacyArray { elements, .. })
504            if is_list_array_expression(expression) =>
505        {
506            let mut entries = BTreeMap::new();
507
508            for (i, element) in elements.iter().enumerate() {
509                let ArrayElement::Value(element) = element else {
510                    return None;
511                };
512
513                let value_type = infer_with_constants(context, scope, element.value, enclosing_class, constants)
514                    .unwrap_or_else(get_mixed);
515
516                entries.insert(i, (false, value_type));
517            }
518
519            Some(wrap_atomic(TAtomic::Array(TArray::List(TList {
520                known_count: Some(entries.len()),
521                known_elements: Some(entries),
522                element_type: Arc::new(get_never()),
523                non_empty: !elements.is_empty(),
524            }))))
525        }
526        Expression::Array(Array { elements, .. }) | Expression::LegacyArray(LegacyArray { elements, .. })
527            if is_keyed_array_expression(expression) =>
528        {
529            let mut known_items = BTreeMap::new();
530            let mut unknown_key_values = Vec::new();
531            for element in elements {
532                let ArrayElement::KeyValue(element) = element else {
533                    return None;
534                };
535
536                let value_type = infer_with_constants(context, scope, element.value, enclosing_class, constants)
537                    .unwrap_or_else(get_mixed);
538
539                let Some(key_type) = infer_with_constants(context, scope, element.key, enclosing_class, constants)
540                    .and_then(|v| v.get_single_array_key())
541                else {
542                    unknown_key_values.push(value_type);
543                    continue;
544                };
545
546                known_items.insert(key_type, (false, value_type));
547
548                if known_items.len() > 100 {
549                    return None;
550                }
551            }
552
553            let mut keyed_array = TKeyedArray::new();
554            keyed_array.non_empty = !known_items.is_empty();
555            if !known_items.is_empty() {
556                keyed_array.known_items = Some(known_items);
557            }
558
559            if !unknown_key_values.is_empty() {
560                let mut value_parameter_types = vec![];
561                for value_type in unknown_key_values {
562                    value_parameter_types.extend(value_type.types.into_owned());
563                }
564
565                keyed_array.parameters =
566                    Some((Arc::new(get_arraykey()), Arc::new(TUnion::from_vec(value_parameter_types))))
567            }
568
569            Some(TUnion::from_single(Cow::Owned(TAtomic::Array(TArray::Keyed(keyed_array)))))
570        }
571        Expression::Closure(closure) => {
572            let span = closure.span();
573
574            Some(wrap_atomic(TAtomic::Callable(TCallable::Alias(FunctionLikeIdentifier::Closure(
575                span.file_id,
576                span.start,
577            )))))
578        }
579        Expression::ArrowFunction(arrow_func) => {
580            let span = arrow_func.span();
581
582            Some(wrap_atomic(TAtomic::Callable(TCallable::Alias(FunctionLikeIdentifier::Closure(
583                span.file_id,
584                span.start,
585            )))))
586        }
587        _ => None,
588    }
589}
590
591#[inline]
592fn infer_constant<'ctx, 'arena>(
593    names: &'ctx ResolvedNames<'arena>,
594    constant: &'ctx Identifier<'arena>,
595    constants_map: Option<&AtomMap<ConstantMetadata>>,
596) -> Option<TUnion> {
597    let (short_name, fqn) = if names.is_imported(constant) {
598        (names.get(constant), names.get(constant))
599    } else if let Some(stripped) = constant.value().strip_prefix('\\') {
600        (stripped, names.get(constant))
601    } else {
602        (constant.value(), names.get(constant))
603    };
604
605    if let Some(t) = get_literal_constant_type(short_name) {
606        return Some(t);
607    }
608
609    if let Some(t) = get_platform_constant_type(short_name) {
610        return Some(t);
611    }
612
613    if let Some(constants) = constants_map {
614        let normalized_name = ascii_lowercase_constant_name_atom(fqn);
615
616        if let Some(constant_metadata) = constants.get(&normalized_name)
617            && let Some(inferred_type) = &constant_metadata.inferred_type
618        {
619            return Some(inferred_type.clone());
620        }
621    }
622
623    None
624}
625
626#[inline]
627fn is_list_array_expression(expression: &Expression) -> bool {
628    match expression {
629        Expression::Array(Array { elements, .. }) | Expression::LegacyArray(LegacyArray { elements, .. }) => {
630            elements.iter().all(|element| matches!(element, ArrayElement::Value(_)))
631        }
632        _ => false,
633    }
634}
635
636#[inline]
637fn is_keyed_array_expression(expression: &Expression) -> bool {
638    match expression {
639        Expression::Array(Array { elements, .. }) | Expression::LegacyArray(LegacyArray { elements, .. }) => {
640            elements.iter().all(|element| matches!(element, ArrayElement::KeyValue(_)))
641        }
642        _ => false,
643    }
644}