mago_analyzer/plugin/libraries/psl/dict/
select_keys.rs1use std::collections::BTreeMap;
4
5use mago_codex::ttype::atomic::TAtomic;
6use mago_codex::ttype::atomic::array::TArray;
7use mago_codex::ttype::atomic::array::key::ArrayKey;
8use mago_codex::ttype::atomic::array::keyed::TKeyedArray;
9use mago_codex::ttype::combine_union_types;
10use mago_codex::ttype::combiner::CombinerOptions;
11use mago_codex::ttype::get_iterable_parameters;
12use mago_codex::ttype::union::TUnion;
13
14use crate::plugin::context::InvocationInfo;
15use crate::plugin::context::ProviderContext;
16use crate::plugin::provider::Provider;
17use crate::plugin::provider::ProviderMeta;
18use crate::plugin::provider::function::FunctionReturnTypeProvider;
19use crate::plugin::provider::function::FunctionTarget;
20
21static META: ProviderMeta = ProviderMeta::new(
22 "psl::dict::select_keys",
23 "Psl\\Dict\\select_keys",
24 "Returns array with only selected keys from input",
25);
26
27#[derive(Default)]
36pub struct SelectKeysProvider;
37
38impl Provider for SelectKeysProvider {
39 fn meta() -> &'static ProviderMeta {
40 &META
41 }
42}
43
44impl FunctionReturnTypeProvider for SelectKeysProvider {
45 fn targets() -> FunctionTarget {
46 FunctionTarget::Exact(b"psl\\dict\\select_keys")
47 }
48
49 fn get_return_type(
50 &self,
51 context: &ProviderContext<'_, '_, '_>,
52 invocation: &InvocationInfo<'_, '_, '_>,
53 ) -> Option<TUnion> {
54 let iterable_expr = invocation.get_argument(0, &[b"iterable"])?;
55 let keys_expr = invocation.get_argument(1, &[b"keys"])?;
56
57 let iterable_type = context.get_expression_type(iterable_expr)?;
58 let keys_type = context.get_expression_type(keys_expr)?;
59
60 if !keys_type.is_single() {
61 return None;
62 }
63
64 let selected_keys = extract_literal_keys(keys_type.get_single())?;
65 if selected_keys.is_empty() {
66 return None;
67 }
68
69 let codebase = context.codebase();
70
71 let mut known_items: BTreeMap<ArrayKey, (bool, TUnion)> = BTreeMap::new();
72 let mut generic_value_type: Option<TUnion> = None;
73
74 for atomic in iterable_type.types.as_ref() {
75 if let TAtomic::Array(TArray::Keyed(keyed)) = atomic {
76 if let Some(items) = &keyed.known_items {
77 for key in &selected_keys {
78 if let Some((optional, value_type)) = items.get(key) {
79 known_items
80 .entry(*key)
81 .and_modify(|(is_optional, existing)| {
82 *is_optional = *is_optional || *optional;
83 *existing =
84 combine_union_types(existing, value_type, codebase, CombinerOptions::default());
85 })
86 .or_insert_with(|| (*optional, value_type.clone()));
87 }
88 }
89 }
90
91 if let Some((_, value_param)) = &keyed.parameters {
92 generic_value_type = Some(match generic_value_type {
93 Some(existing) => {
94 combine_union_types(&existing, value_param, codebase, CombinerOptions::default())
95 }
96 None => (**value_param).clone(),
97 });
98 }
99
100 continue;
101 }
102
103 if let Some((_, value_type)) = get_iterable_parameters(atomic, codebase) {
104 generic_value_type = Some(match generic_value_type {
105 Some(existing) => combine_union_types(&existing, &value_type, codebase, CombinerOptions::default()),
106 None => value_type,
107 });
108 }
109 }
110
111 let mut result_items: BTreeMap<ArrayKey, (bool, TUnion)> = BTreeMap::new();
112 for key in &selected_keys {
113 if let Some(item) = known_items.get(key) {
114 result_items.insert(*key, item.clone());
115 } else if let Some(value_type) = generic_value_type.as_ref() {
116 result_items.insert(*key, (true, value_type.clone()));
117 } else {
118 }
120 }
121
122 if result_items.is_empty() {
123 return None;
124 }
125
126 let mut result = TKeyedArray::new();
127 result.known_items = Some(result_items);
128 result.non_empty = known_items.values().any(|(optional, _)| !optional);
129
130 Some(TUnion::from_atomic(TAtomic::Array(TArray::Keyed(result))))
131 }
132}
133
134fn extract_literal_keys(atomic: &TAtomic) -> Option<Vec<ArrayKey>> {
136 match atomic {
137 TAtomic::Array(TArray::List(list)) => {
138 let known_elements = list.known_elements.as_ref()?;
139 let mut keys = Vec::new();
140
141 for (_, element_type) in known_elements.values() {
142 keys.push(union_to_array_key(element_type)?);
143 }
144
145 if keys.is_empty() { None } else { Some(keys) }
146 }
147 TAtomic::Array(TArray::Keyed(keyed)) => {
148 let known_items = keyed.known_items.as_ref()?;
149 let mut keys = Vec::new();
150
151 for (_, value_type) in known_items.values() {
152 keys.push(union_to_array_key(value_type)?);
153 }
154
155 if keys.is_empty() { None } else { Some(keys) }
156 }
157 _ => None,
158 }
159}
160
161fn union_to_array_key(union: &TUnion) -> Option<ArrayKey> {
163 if !union.is_single() {
164 return None;
165 }
166
167 let atomic = union.get_single();
168 if let Some(value) = atomic.get_literal_string_value() {
169 Some(ArrayKey::String(mago_word::word(value)))
170 } else {
171 atomic.get_literal_int_value().map(ArrayKey::Integer)
172 }
173}