Skip to main content

mago_analyzer/plugin/libraries/stdlib/url/
parse_url.rs

1//! `parse_url()` return type provider.
2
3use 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::atomic::scalar::TScalar;
10use mago_codex::ttype::atomic::scalar::bool::TBool;
11use mago_codex::ttype::atomic::scalar::int::TInteger;
12use mago_codex::ttype::atomic::scalar::string::TString;
13use mago_codex::ttype::get_int_range;
14use mago_codex::ttype::get_non_empty_string;
15use mago_codex::ttype::union::TUnion;
16use mago_word::Word;
17
18use crate::plugin::context::InvocationInfo;
19use crate::plugin::context::ProviderContext;
20use crate::plugin::provider::Provider;
21use crate::plugin::provider::ProviderMeta;
22use crate::plugin::provider::function::FunctionReturnTypeProvider;
23use crate::plugin::provider::function::FunctionTarget;
24
25const PHP_URL_SCHEME: i64 = 0;
26const PHP_URL_HOST: i64 = 1;
27const PHP_URL_PORT: i64 = 2;
28const PHP_URL_USER: i64 = 3;
29const PHP_URL_PASS: i64 = 4;
30const PHP_URL_PATH: i64 = 5;
31const PHP_URL_QUERY: i64 = 6;
32const PHP_URL_FRAGMENT: i64 = 7;
33
34static META: ProviderMeta = ProviderMeta::new(
35    "php::url::parse_url",
36    "parse_url",
37    "Returns typed array or component value based on component argument",
38);
39
40/// Provider for the `parse_url()` function.
41///
42/// When called without a component argument, returns `false|array{...}` with URL parts.
43/// When called with a specific component constant, returns the appropriate narrowed type.
44#[derive(Default)]
45pub struct ParseUrlProvider;
46
47impl Provider for ParseUrlProvider {
48    fn meta() -> &'static ProviderMeta {
49        &META
50    }
51}
52
53impl FunctionReturnTypeProvider for ParseUrlProvider {
54    fn targets() -> FunctionTarget {
55        FunctionTarget::Exact(b"parse_url")
56    }
57
58    fn get_return_type(
59        &self,
60        context: &ProviderContext<'_, '_, '_>,
61        invocation: &InvocationInfo<'_, '_, '_>,
62    ) -> Option<TUnion> {
63        let component_arg = invocation.get_argument(1, &[b"component"]);
64
65        if let Some(arg) = component_arg {
66            if let Some(component_type) = context.get_expression_type(arg) {
67                let values = collect_component_values(component_type);
68
69                if let Some(values) = values {
70                    if values.is_empty() {
71                        // No valid component values - fall through to generic return
72                    } else {
73                        let mut result_types: Vec<TAtomic> = Vec::new();
74                        for value in values {
75                            let component_ret = get_component_return_type(value);
76                            for atomic in component_ret.types.iter() {
77                                if !result_types.contains(atomic) {
78                                    result_types.push(atomic.clone());
79                                }
80                            }
81                        }
82
83                        return Some(TUnion::from_vec(result_types));
84                    }
85                }
86            }
87
88            // Component provided but not resolvable - return generic union type
89            return Some(get_all_components_return_type());
90        }
91
92        // No component argument - return full array type
93        Some(get_full_array_return_type())
94    }
95}
96
97/// Collects all possible component values from a type.
98/// Returns `None` if the type represents an unbounded set of integers.
99/// Returns `Some(vec![])` if the type is empty or has no valid integers.
100fn collect_component_values(component_type: &TUnion) -> Option<Vec<i64>> {
101    let mut values = Vec::new();
102
103    for atomic in component_type.types.iter() {
104        if let TAtomic::Scalar(TScalar::Integer(int_type)) = atomic {
105            match *int_type {
106                TInteger::Literal(v) => {
107                    if !values.contains(&v) {
108                        values.push(v);
109                    }
110                }
111                TInteger::Range(from, to) => {
112                    let effective_from = from.max(-1);
113                    let effective_to = to.min(7);
114
115                    if effective_from <= effective_to {
116                        for v in effective_from..=effective_to {
117                            if !values.contains(&v) {
118                                values.push(v);
119                            }
120                        }
121                    }
122                }
123                TInteger::From(from) => {
124                    let effective_from = from.max(-1);
125                    if effective_from <= 7 {
126                        for v in effective_from..=7 {
127                            if !values.contains(&v) {
128                                values.push(v);
129                            }
130                        }
131                    }
132                }
133                TInteger::To(to) => {
134                    let effective_to = to.min(7);
135                    if -1 <= effective_to {
136                        for v in -1..=effective_to {
137                            if !values.contains(&v) {
138                                values.push(v);
139                            }
140                        }
141                    }
142                }
143                TInteger::Unspecified | TInteger::UnspecifiedLiteral => {
144                    return None;
145                }
146            }
147        }
148    }
149
150    Some(values)
151}
152
153/// Returns the type for a specific URL component.
154/// All component types include `false` since `parse_url` returns `false` for seriously malformed URLs.
155///
156/// See: https://www.php.net/manual/en/function.parse-url.php
157fn get_component_return_type(component: i64) -> TUnion {
158    let false_type = TAtomic::Scalar(TScalar::Bool(TBool::r#false()));
159
160    match component {
161        PHP_URL_SCHEME | PHP_URL_HOST | PHP_URL_USER | PHP_URL_PASS | PHP_URL_PATH | PHP_URL_QUERY
162        | PHP_URL_FRAGMENT => {
163            // false|null|non-empty-string
164            TUnion::from_vec(vec![false_type, TAtomic::Null, TAtomic::Scalar(TScalar::String(TString::non_empty()))])
165        }
166        PHP_URL_PORT => {
167            // false|null|int<0, 65535>
168            TUnion::from_vec(vec![
169                false_type,
170                TAtomic::Null,
171                TAtomic::Scalar(TScalar::Integer(TInteger::Range(0, 0xFFFF))),
172            ])
173        }
174        -1 => {
175            // -1 is equivalent to no component - return full array
176            get_full_array_return_type()
177        }
178        _ => TUnion::from_vec(vec![false_type]),
179    }
180}
181
182/// Returns the union of all possible component return types.
183/// Used when component type is non-literal (e.g., `int`).
184/// `false|null|int<0, 65535>|string|array{...}`
185fn get_all_components_return_type() -> TUnion {
186    let mut all_components_return_type = get_full_array_return_type();
187    all_components_return_type.types.to_mut().push(TAtomic::Null);
188    all_components_return_type.types.to_mut().push(TAtomic::Scalar(TScalar::Integer(TInteger::Range(0, 0xFFFF))));
189    all_components_return_type.types.to_mut().push(TAtomic::Scalar(TScalar::String(TString::general())));
190
191    all_components_return_type
192}
193
194/// Returns the full array type when no component is specified.
195fn get_full_array_return_type() -> TUnion {
196    let mut known_items: BTreeMap<ArrayKey, (bool, TUnion)> = BTreeMap::new();
197
198    let optional_string_fields = ["scheme", "user", "pass", "host", "path", "query", "fragment"];
199    for field in optional_string_fields {
200        known_items.insert(ArrayKey::String(Word::from(field)), (true, get_non_empty_string()));
201    }
202
203    known_items.insert(ArrayKey::String(Word::from("port")), (true, get_int_range(Some(0), Some(0xFFFF))));
204
205    let keyed_array = TKeyedArray::new().with_known_items(known_items);
206
207    TUnion::from_vec(vec![TAtomic::Scalar(TScalar::Bool(TBool::r#false())), TAtomic::Array(TArray::Keyed(keyed_array))])
208}