mago_analyzer/plugin/libraries/stdlib/array/
array_key_exists.rs1use 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}