mago_analyzer/plugin/libraries/stdlib/array/
array_merge.rs1use std::borrow::Cow;
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7use mago_codex::ttype::atomic::TAtomic;
8use mago_codex::ttype::atomic::array::TArray;
9use mago_codex::ttype::atomic::array::key::ArrayKey;
10use mago_codex::ttype::atomic::array::keyed::TKeyedArray;
11use mago_codex::ttype::atomic::array::list::TList;
12use mago_codex::ttype::combine_union_types;
13use mago_codex::ttype::combiner::CombinerOptions;
14use mago_codex::ttype::get_array_parameters;
15use mago_codex::ttype::get_arraykey;
16use mago_codex::ttype::get_int;
17use mago_codex::ttype::get_iterable_parameters;
18use mago_codex::ttype::get_mixed;
19use mago_codex::ttype::get_never;
20use mago_codex::ttype::union::TUnion;
21
22use crate::plugin::context::InvocationInfo;
23use crate::plugin::context::ProviderContext;
24use crate::plugin::provider::Provider;
25use crate::plugin::provider::ProviderMeta;
26use crate::plugin::provider::function::FunctionReturnTypeProvider;
27use crate::plugin::provider::function::FunctionTarget;
28
29static META: ProviderMeta =
30 ProviderMeta::new("php::array::array_merge", "array_merge", "Returns merged array with combined types");
31
32static TARGETS: [&[u8]; 2] = [b"array_merge", b"psl\\dict\\merge"];
33
34#[derive(Default)]
38pub struct ArrayMergeProvider;
39
40impl Provider for ArrayMergeProvider {
41 fn meta() -> &'static ProviderMeta {
42 &META
43 }
44}
45
46impl FunctionReturnTypeProvider for ArrayMergeProvider {
47 fn targets() -> FunctionTarget {
48 FunctionTarget::ExactMultiple(&TARGETS)
49 }
50
51 fn get_return_type(
52 &self,
53 context: &ProviderContext<'_, '_, '_>,
54 invocation: &InvocationInfo<'_, '_, '_>,
55 ) -> Option<TUnion> {
56 let arguments = invocation.arguments();
57 if arguments.is_empty() {
58 return None;
59 }
60
61 let codebase = context.codebase();
62
63 let mut merged_items: BTreeMap<ArrayKey, (bool, TUnion)> = BTreeMap::new();
64 let mut merged_list_elements: BTreeMap<usize, (bool, TUnion)> = BTreeMap::new();
65 let mut next_list_index: usize = 0;
66 let mut has_parameters = false;
67 let mut merged_key_type: Option<TUnion> = None;
68 let mut merged_value_type: Option<TUnion> = None;
69 let mut any_argument_non_empty = false;
70 let mut all_arguments_are_lists = true;
71 let mut all_lists_are_closed = true;
72
73 for invocation_argument in arguments {
74 let argument_expr = invocation_argument.value()?;
75 let argument_type = context.get_expression_type(argument_expr)?;
76
77 if !argument_type.is_single() {
78 return None;
79 }
80
81 let argument_type = if invocation_argument.is_unpacked() {
82 let inner = get_iterable_parameters(argument_type.get_single(), codebase);
83 let (_, value_type) = inner?;
84 if !value_type.is_single() {
85 return None;
86 }
87
88 if argument_type.get_single().is_non_empty_list() || argument_type.get_single().is_non_empty_array() {
89 any_argument_non_empty = true;
90 }
91
92 Cow::Owned(value_type)
93 } else {
94 Cow::Borrowed(argument_type)
95 };
96
97 let iterable = argument_type.get_single();
98
99 if let TAtomic::Array(array) = iterable {
100 match array {
101 TArray::Keyed(keyed) => {
102 let is_empty_array = keyed.known_items.is_none() && keyed.parameters.is_none();
103
104 if !is_empty_array {
105 all_arguments_are_lists = false;
106 }
107
108 if keyed.non_empty {
109 any_argument_non_empty = true;
110 }
111
112 if let Some(items) = keyed.known_items.as_ref() {
113 for (key, value) in items {
114 merged_items.insert(*key, value.clone());
115 }
116 }
117
118 if let Some((key_type, value_type)) = &keyed.parameters {
119 has_parameters = true;
120 merged_key_type = Some(match merged_key_type {
121 Some(existing) => {
122 combine_union_types(&existing, key_type, codebase, CombinerOptions::default())
123 }
124 None => (**key_type).clone(),
125 });
126 merged_value_type = Some(match merged_value_type {
127 Some(existing) => {
128 combine_union_types(&existing, value_type, codebase, CombinerOptions::default())
129 }
130 None => (**value_type).clone(),
131 });
132 }
133 }
134 TArray::List(list) => {
135 if list.non_empty {
136 any_argument_non_empty = true;
137 }
138
139 let is_list_closed = list.element_type.is_never();
140 if !is_list_closed {
141 all_lists_are_closed = false;
142 }
143
144 if let Some(known_elements) = list.known_elements.as_ref() {
145 for (idx, (optional, element_type)) in known_elements {
146 let new_idx = next_list_index + idx;
147 merged_list_elements.insert(new_idx, (*optional, element_type.clone()));
148 }
149 if let Some(max_idx) = known_elements.keys().max() {
150 next_list_index += max_idx + 1;
151 }
152 } else if list.non_empty {
153 next_list_index += 1; }
155
156 let (_, list_value_type) = get_array_parameters(&TArray::List(list.clone()), codebase);
157
158 has_parameters = true;
159 merged_value_type = Some(match merged_value_type {
160 Some(existing) => {
161 combine_union_types(&existing, &list_value_type, codebase, CombinerOptions::default())
162 }
163 None => list_value_type,
164 });
165
166 if !all_arguments_are_lists {
167 let key_type = get_int();
168 merged_key_type = Some(match merged_key_type {
169 Some(existing) => {
170 combine_union_types(&existing, &key_type, codebase, CombinerOptions::default())
171 }
172 None => key_type,
173 });
174 }
175 }
176 }
177 } else if let Some((iterable_key, iterable_value)) = get_iterable_parameters(iterable, codebase) {
178 all_arguments_are_lists = false;
179 has_parameters = true;
180 merged_key_type = Some(match merged_key_type {
181 Some(existing) => {
182 combine_union_types(&existing, &iterable_key, codebase, CombinerOptions::default())
183 }
184 None => iterable_key,
185 });
186 merged_value_type = Some(match merged_value_type {
187 Some(existing) => {
188 combine_union_types(&existing, &iterable_value, codebase, CombinerOptions::default())
189 }
190 None => iterable_value,
191 });
192 } else {
193 return None;
194 }
195 }
196
197 if all_arguments_are_lists {
198 let element_type =
199 if all_lists_are_closed { get_never() } else { merged_value_type.unwrap_or_else(get_mixed) };
200
201 let mut result_list = TList::new(Arc::new(element_type));
202 result_list.non_empty = any_argument_non_empty;
203
204 if !merged_list_elements.is_empty() {
205 result_list.known_elements = Some(merged_list_elements);
206 }
207
208 Some(TUnion::from_atomic(TAtomic::Array(TArray::List(result_list))))
209 } else {
210 let mut result_array = TKeyedArray::new();
211
212 let has_merged_items = !merged_items.is_empty();
213 if has_merged_items {
214 result_array.known_items = Some(merged_items);
215 }
216
217 result_array.non_empty = any_argument_non_empty || has_merged_items;
218
219 if has_parameters {
220 result_array.parameters = Some((
221 Arc::new(merged_key_type.unwrap_or_else(get_arraykey)),
222 Arc::new(merged_value_type.unwrap_or_else(get_mixed)),
223 ));
224 }
225
226 Some(TUnion::from_atomic(TAtomic::Array(TArray::Keyed(result_array))))
227 }
228 }
229}