Skip to main content

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

1//! `range()` return type provider.
2
3use mago_codex::ttype::atomic::TAtomic;
4use mago_codex::ttype::atomic::scalar::TScalar;
5use mago_codex::ttype::atomic::scalar::float::TFloat;
6use mago_codex::ttype::atomic::scalar::int::TInteger;
7use mago_codex::ttype::atomic::scalar::string::TString;
8use mago_codex::ttype::get_non_empty_list;
9use mago_codex::ttype::union::TUnion;
10use mago_codex::ttype::wrap_atomic;
11
12use crate::plugin::context::InvocationInfo;
13use crate::plugin::context::ProviderContext;
14use crate::plugin::provider::Provider;
15use crate::plugin::provider::ProviderMeta;
16use crate::plugin::provider::function::FunctionReturnTypeProvider;
17use crate::plugin::provider::function::FunctionTarget;
18
19static META: ProviderMeta =
20    ProviderMeta::new("php::array::range", "range", "Returns non-empty-list with element type based on arguments");
21
22/// Provider for the `range()` function.
23///
24/// Infers the element type of the returned list based on the types and values
25/// of the `$start` and `$end` arguments:
26/// - Both ints: `non-empty-list<int>` with appropriate range (positive, negative, etc.)
27/// - Either float: `non-empty-list<float>`
28/// - Both strings: `non-empty-list<non-empty-string>`
29/// - Empty string start: treats as 0 for numeric ranges
30#[derive(Default)]
31pub struct RangeProvider;
32
33impl Provider for RangeProvider {
34    fn meta() -> &'static ProviderMeta {
35        &META
36    }
37}
38
39impl FunctionReturnTypeProvider for RangeProvider {
40    fn targets() -> FunctionTarget {
41        FunctionTarget::Exact(b"range")
42    }
43
44    fn get_return_type(
45        &self,
46        context: &ProviderContext<'_, '_, '_>,
47        invocation: &InvocationInfo<'_, '_, '_>,
48    ) -> Option<TUnion> {
49        let start_expr = invocation.get_argument(0, &[b"start"])?;
50        let end_expr = invocation.get_argument(1, &[b"end"])?;
51
52        let start_type = context.get_expression_type(start_expr)?;
53        let end_type = context.get_expression_type(end_expr)?;
54
55        if !start_type.is_single() || !end_type.is_single() {
56            return None;
57        }
58
59        let start_atomic = start_type.get_single();
60        let end_atomic = end_type.get_single();
61
62        let element_type = infer_range_element_type(start_atomic, end_atomic)?;
63
64        Some(get_non_empty_list(wrap_atomic(element_type)))
65    }
66}
67
68fn infer_range_element_type(start: &TAtomic, end: &TAtomic) -> Option<TAtomic> {
69    let start_kind = classify(start);
70    let end_kind = classify(end);
71
72    match (start_kind, end_kind) {
73        (ArgKind::Int(start_val), ArgKind::Int(end_val)) => Some(int_element_type(start_val, end_val)),
74        (ArgKind::Float(start_val), ArgKind::Float(end_val)) => Some(float_element_type(start_val, end_val)),
75        (ArgKind::Int(start_val), ArgKind::Float(end_val)) => {
76            Some(float_element_type(start_val.map(|v| v as f64), end_val))
77        }
78        (ArgKind::Float(start_val), ArgKind::Int(end_val)) => {
79            Some(float_element_type(start_val, end_val.map(|v| v as f64)))
80        }
81        (ArgKind::NonEmptyString, ArgKind::NonEmptyString) => {
82            Some(TAtomic::Scalar(TScalar::String(TString::non_empty())))
83        }
84        (ArgKind::EmptyString, ArgKind::EmptyString) => Some(TAtomic::Scalar(TScalar::Integer(TInteger::Literal(0)))),
85        (ArgKind::EmptyString, ArgKind::Int(end_val)) => Some(int_element_type(Some(0), end_val)),
86        (ArgKind::EmptyString, ArgKind::Float(end_val)) => Some(float_element_type(Some(0.0), end_val)),
87        (ArgKind::Int(start_val), ArgKind::EmptyString) => Some(int_element_type(start_val, Some(0))),
88        (ArgKind::Float(start_val), ArgKind::EmptyString) => Some(float_element_type(start_val, Some(0.0))),
89        _ => None,
90    }
91}
92
93/// Classifies a range argument into its kind.
94#[derive(Debug, Clone, Copy)]
95enum ArgKind {
96    Int(Option<i64>),
97    Float(Option<f64>),
98    NonEmptyString,
99    EmptyString,
100    Unknown,
101}
102
103fn classify(atomic: &TAtomic) -> ArgKind {
104    match atomic {
105        TAtomic::Scalar(TScalar::Integer(int)) => ArgKind::Int(int.get_literal_value()),
106        TAtomic::Scalar(TScalar::Float(float)) => ArgKind::Float(float.get_literal_value()),
107        TAtomic::Scalar(TScalar::String(string)) => {
108            if let Some(value) = string.get_known_literal_value() {
109                if value.is_empty() { ArgKind::EmptyString } else { ArgKind::NonEmptyString }
110            } else if string.is_non_empty() {
111                ArgKind::NonEmptyString
112            } else {
113                ArgKind::Unknown
114            }
115        }
116        _ => ArgKind::Unknown,
117    }
118}
119
120/// Returns the most precise int type for a range(start, end).
121fn int_element_type(start: Option<i64>, end: Option<i64>) -> TAtomic {
122    let (min, max) = match (start, end) {
123        (Some(a), Some(b)) => (Some(a.min(b)), Some(a.max(b))),
124        _ => (None, None),
125    };
126
127    let int = match (min, max) {
128        (Some(min), Some(max)) if min == max => TInteger::Literal(min),
129        (Some(min), Some(max)) if min >= 1 => TInteger::Range(min, max),
130        (Some(min), Some(max)) if min >= 0 => TInteger::Range(min, max),
131        (Some(min), Some(max)) if max <= -1 => TInteger::Range(min, max),
132        (Some(min), Some(max)) if max <= 0 => TInteger::Range(min, max),
133        (Some(min), Some(max)) => TInteger::Range(min, max),
134        (Some(min), None) if min >= 1 => TInteger::positive(),
135        (Some(min), None) if min >= 0 => TInteger::non_negative(),
136        (Some(min), None) => TInteger::From(min),
137        (None, Some(max)) if max <= -1 => TInteger::negative(),
138        (None, Some(max)) if max <= 0 => TInteger::non_positive(),
139        (None, Some(max)) => TInteger::To(max),
140        (None, None) => TInteger::Unspecified,
141    };
142
143    TAtomic::Scalar(TScalar::Integer(int))
144}
145
146/// Returns the most precise float type for a range with float arguments.
147fn float_element_type(_start: Option<f64>, _end: Option<f64>) -> TAtomic {
148    TAtomic::Scalar(TScalar::Float(TFloat::Float))
149}