Skip to main content

mago_codex/scanner/inference/
mod.rs

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