Skip to main content

tsz_solver/
utils.rs

1//! Shared utility functions for the solver module.
2//!
3//! This module contains common utilities used across multiple solver components
4//! to avoid code duplication.
5
6use crate::caches::db::TypeDatabase;
7use crate::types::{ObjectShapeId, PropertyInfo, PropertyLookup, TypeId};
8use tsz_common::interner::Atom;
9
10/// Checks if a property name is numeric by resolving the atom and checking its string representation.
11///
12/// This function consolidates the previously duplicated `is_numeric_property_name` implementations
13/// from operations.rs, evaluate.rs, subtype.rs, and infer.rs.
14pub fn is_numeric_property_name(interner: &dyn TypeDatabase, name: Atom) -> bool {
15    let prop_name = interner.resolve_atom_ref(name);
16    is_numeric_literal_name(prop_name.as_ref())
17}
18
19/// Checks if a string represents a numeric literal name.
20///
21/// Returns `true` for:
22/// - "`NaN`", "Infinity", "-Infinity"
23/// - Numeric strings that round-trip correctly through JavaScript's number-to-string conversion
24pub fn is_numeric_literal_name(name: &str) -> bool {
25    if name == "NaN" || name == "Infinity" || name == "-Infinity" {
26        return true;
27    }
28
29    let value: f64 = match name.parse() {
30        Ok(value) => value,
31        Err(_) => return false,
32    };
33    if !value.is_finite() {
34        return false;
35    }
36
37    js_number_to_string(value) == name
38}
39
40/// Canonicalizes a numeric property name to its JavaScript canonical form.
41///
42/// If the input parses as a finite number, returns `Some(canonical_form)` where
43/// `canonical_form` matches JavaScript's `Number.prototype.toString()`.
44/// For example, `"1."`, `"1.0"`, and `"1"` all canonicalize to `"1"`.
45/// Returns `None` if the name is not a numeric literal.
46pub fn canonicalize_numeric_name(name: &str) -> Option<String> {
47    let value: f64 = tsz_common::numeric::parse_numeric_literal_value(name)?;
48    if !value.is_finite() && !value.is_nan() {
49        return None;
50    }
51    Some(js_number_to_string(value))
52}
53
54/// Converts a JavaScript number to its string representation.
55///
56/// This matches JavaScript's `Number.prototype.toString()` behavior for proper
57/// numeric literal name checking.
58fn js_number_to_string(value: f64) -> String {
59    if value.is_nan() {
60        return "NaN".to_string();
61    }
62    if value == 0.0 {
63        return "0".to_string();
64    }
65    if value.is_infinite() {
66        return if value.is_sign_negative() {
67            "-Infinity".to_string()
68        } else {
69            "Infinity".to_string()
70        };
71    }
72
73    let abs = value.abs();
74    if !(1e-6..1e21).contains(&abs) {
75        let mut formatted = format!("{value:e}");
76        if let Some(split) = formatted.find('e') {
77            let (mantissa, exp) = formatted.split_at(split);
78            let exp_digits = exp.strip_prefix('e').unwrap_or("");
79            let (sign, digits) = if let Some(digits) = exp_digits.strip_prefix('-') {
80                ('-', digits)
81            } else {
82                ('+', exp_digits)
83            };
84            let trimmed = digits.trim_start_matches('0');
85            let digits = if trimmed.is_empty() { "0" } else { trimmed };
86            formatted = format!("{mantissa}e{sign}{digits}");
87        }
88        return formatted;
89    }
90
91    let formatted = value.to_string();
92    if formatted == "-0" {
93        "0".to_string()
94    } else {
95        formatted
96    }
97}
98
99/// Reduces a vector of types to a union, single type, or NEVER.
100///
101/// This helper eliminates the common pattern:
102/// ```ignore
103/// if types.is_empty() {
104///     TypeId::NEVER
105/// } else if types.len() == 1 {
106///     types[0]
107/// } else {
108///     db.union(types)
109/// }
110/// ```
111///
112/// # Examples
113///
114/// ```ignore
115/// let narrowed = union_or_single(db, filtered_members);
116/// ```
117pub fn union_or_single(db: &dyn TypeDatabase, types: Vec<TypeId>) -> TypeId {
118    match types.len() {
119        0 => TypeId::NEVER,
120        1 => types[0],
121        _ => db.union(types),
122    }
123}
124
125/// Reduces a vector of types to an intersection, single type, or NEVER.
126///
127/// This helper eliminates the common pattern:
128/// ```ignore
129/// if types.is_empty() {
130///     TypeId::NEVER
131/// } else if types.len() == 1 {
132///     types[0]
133/// } else {
134///     db.intersection(types)
135/// }
136/// ```
137///
138/// # Examples
139///
140/// ```ignore
141/// let narrowed = intersection_or_single(db, instance_types);
142/// ```
143pub fn intersection_or_single(db: &dyn TypeDatabase, types: Vec<TypeId>) -> TypeId {
144    match types.len() {
145        0 => TypeId::NEVER,
146        1 => types[0],
147        _ => db.intersection(types),
148    }
149}
150
151/// Extension trait for `TypeId` with chainable methods for common operations.
152///
153/// This trait provides idiomatic Rust methods to reduce boilerplate when
154/// working with `TypeId` values. Methods are designed to be chainable and
155/// composable with iterator combinators.
156///
157/// # Examples
158///
159/// ```ignore
160/// // Filter out NEVER types in a map operation
161/// .filter_map(|&id| some_operation(id).non_never())
162/// ```
163pub trait TypeIdExt {
164    /// Returns Some(self) if self is not NEVER, otherwise None.
165    ///
166    /// This is useful for `filter_map` chains where you want to skip NEVER results.
167    fn non_never(self) -> Option<Self>
168    where
169        Self: Sized;
170}
171
172impl TypeIdExt for TypeId {
173    #[inline]
174    fn non_never(self) -> Option<Self> {
175        (self != Self::NEVER).then_some(self)
176    }
177}
178
179/// Look up a property by name, using the cached property index if available.
180///
181/// This consolidates the duplicated `lookup_property` implementations from
182/// `subtype_rules/objects.rs` and `infer_bct.rs`.
183pub fn lookup_property<'props>(
184    db: &dyn TypeDatabase,
185    props: &'props [PropertyInfo],
186    shape_id: Option<ObjectShapeId>,
187    name: Atom,
188) -> Option<&'props PropertyInfo> {
189    if let Some(shape_id) = shape_id {
190        match db.object_property_index(shape_id, name) {
191            PropertyLookup::Found(idx) => return props.get(idx),
192            PropertyLookup::NotFound => return None,
193            PropertyLookup::Uncached => {}
194        }
195    }
196    props
197        .binary_search_by_key(&name, |p| p.name)
198        .ok()
199        .map(|idx| &props[idx])
200}
201
202/// Find a common base type for a set of types using the provided `get_base` function.
203///
204/// Returns `Some(base)` if all types share the same base, `None` otherwise.
205/// Used by both expression operations (literal widening) and BCT inference (nominal hierarchy).
206pub fn find_common_base_type(
207    types: &[TypeId],
208    get_base: impl Fn(TypeId) -> Option<TypeId>,
209) -> Option<TypeId> {
210    let first_base = get_base(*types.first()?)?;
211    for &ty in types.iter().skip(1) {
212        if get_base(ty)? != first_base {
213            return None;
214        }
215    }
216    Some(first_base)
217}
218
219/// Get the effective read type of a property, adding `undefined` if the property is optional.
220///
221/// When a property is marked as optional (`prop.optional == true`), its read type
222/// should include `undefined` to match TypeScript's behavior. This consolidates the
223/// previously duplicated `optional_property_type` methods from `PropertyAccessEvaluator`,
224/// `TypeEvaluator`, `CallEvaluator`, and `InferenceContext`.
225///
226/// Note: `SubtypeChecker` has its own version that respects `exactOptionalPropertyTypes`.
227pub fn optional_property_type(db: &dyn TypeDatabase, prop: &PropertyInfo) -> TypeId {
228    if prop.optional {
229        db.union2(prop.type_id, TypeId::UNDEFINED)
230    } else {
231        prop.type_id
232    }
233}
234
235/// Get the effective write type of a property, adding `undefined` if the property is optional.
236///
237/// Similar to [`optional_property_type`] but uses the property's `write_type` field instead.
238pub fn optional_property_write_type(db: &dyn TypeDatabase, prop: &PropertyInfo) -> TypeId {
239    if prop.optional {
240        db.union2(prop.write_type, TypeId::UNDEFINED)
241    } else {
242        prop.write_type
243    }
244}
245
246#[cfg(test)]
247#[path = "../tests/utils_tests.rs"]
248mod tests;