Skip to main content

mago_analyzer/plugin/libraries/stdlib/array/
array_key_exists.rs

1//! `array_key_exists()` / `key_exists()` return type provider.
2//!
3//! When called with a literal key on an array whose shape is fully known and does not
4//! contain that key, narrow the return type from `bool` to `false`. This lets the
5//! generic "impossible condition" reporter fire on the enclosing `if`, matching the
6//! behavior already seen for `empty()` and `isset()` on the same missing key.
7
8use mago_codex::ttype::atomic::TAtomic;
9use mago_codex::ttype::atomic::array::TArray;
10use mago_codex::ttype::atomic::array::key::ArrayKey;
11use mago_codex::ttype::get_false;
12use mago_codex::ttype::union::TUnion;
13use mago_syntax::cst::Expression;
14use mago_syntax::cst::Literal;
15
16use crate::plugin::context::InvocationInfo;
17use crate::plugin::context::ProviderContext;
18use crate::plugin::provider::Provider;
19use crate::plugin::provider::ProviderMeta;
20use crate::plugin::provider::function::FunctionReturnTypeProvider;
21use crate::plugin::provider::function::FunctionTarget;
22
23static META: ProviderMeta = ProviderMeta::new(
24    "php::array::array_key_exists",
25    "array_key_exists",
26    "Narrows the return to `false` when the literal key is provably absent from the array's known shape",
27);
28
29#[derive(Default)]
30pub struct ArrayKeyExistsProvider;
31
32impl Provider for ArrayKeyExistsProvider {
33    fn meta() -> &'static ProviderMeta {
34        &META
35    }
36}
37
38impl FunctionReturnTypeProvider for ArrayKeyExistsProvider {
39    fn targets() -> FunctionTarget {
40        FunctionTarget::ExactMultiple(&[b"array_key_exists", b"key_exists"])
41    }
42
43    fn get_return_type(
44        &self,
45        context: &ProviderContext<'_, '_, '_>,
46        invocation: &InvocationInfo<'_, '_, '_>,
47    ) -> Option<TUnion> {
48        let key_expr = invocation.get_argument(0, &[b"key"])?;
49        let array_expr = invocation.get_argument(1, &[b"array"])?;
50
51        let literal_key = literal_array_key_from_expression(key_expr)?;
52        let array_type = context.get_expression_type(array_expr)?;
53
54        if array_type.types.is_empty() {
55            return None;
56        }
57
58        for atomic in array_type.types.as_ref() {
59            match atomic {
60                TAtomic::Array(TArray::Keyed(keyed)) => {
61                    if keyed.get_generic_parameters().is_some() {
62                        return None;
63                    }
64
65                    match keyed.get_known_items() {
66                        Some(known) if !known.contains_key(&literal_key) => {}
67                        _ => return None,
68                    }
69                }
70                TAtomic::Array(TArray::List(list)) => {
71                    let ArrayKey::Integer(i) = literal_key else {
72                        return None;
73                    };
74
75                    if i < 0 {
76                        continue;
77                    }
78
79                    if !list.element_type.is_never() {
80                        return None;
81                    }
82
83                    if let Some(known_elements) = &list.known_elements
84                        && known_elements.contains_key(&(i as usize))
85                    {
86                        return None;
87                    }
88                }
89                _ => return None,
90            }
91        }
92
93        Some(get_false())
94    }
95}
96
97fn literal_array_key_from_expression(expr: &Expression<'_>) -> Option<ArrayKey> {
98    match expr {
99        Expression::Literal(Literal::String(s)) => s.value.map(|v| ArrayKey::String(mago_word::word(v))),
100        Expression::Literal(Literal::Integer(i)) => i.value.and_then(|v| i64::try_from(v).ok()).map(ArrayKey::Integer),
101        _ => None,
102    }
103}