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