Skip to main content

tsz_solver/type_queries/
traversal.rs

1//! Type traversal and property access classification helpers.
2//!
3//! This module provides classification enums and functions for traversing
4//! type structures. These are used by the checker to determine how to walk
5//! into nested types for property access resolution, symbol resolution,
6//! and diagnostic property name collection — without directly matching on
7//! `TypeData` variants.
8
9use crate::TypeDatabase;
10use crate::type_queries::data::{get_callable_shape, get_object_shape};
11use crate::types::{IntrinsicKind, TemplateSpan, TypeData, TypeId};
12use tsz_common::interner::Atom;
13
14// =============================================================================
15// PropertyAccessClassification - Classification for property access resolution
16// =============================================================================
17
18/// Classification for property access resolution.
19#[derive(Debug, Clone)]
20pub enum PropertyAccessClassification {
21    /// Direct object type that can have properties accessed
22    Direct(TypeId),
23    /// Symbol reference - needs resolution first
24    SymbolRef(crate::types::SymbolRef),
25    /// Type query (typeof) - needs symbol resolution
26    TypeQuery(crate::types::SymbolRef),
27    /// Generic application - needs instantiation
28    Application {
29        app_id: crate::types::TypeApplicationId,
30    },
31    /// Union - access on each member
32    Union(Vec<TypeId>),
33    /// Intersection - access on each member
34    Intersection(Vec<TypeId>),
35    /// Index access - needs evaluation
36    IndexAccess { object: TypeId, index: TypeId },
37    /// Readonly wrapper - unwrap and continue
38    Readonly(TypeId),
39    /// Callable type - may need Function interface expansion
40    Callable(TypeId),
41    /// Type parameter with constraint
42    TypeParameter { constraint: Option<TypeId> },
43    /// Needs evaluation (Conditional, Mapped, `KeyOf`)
44    NeedsEvaluation(TypeId),
45    /// Primitive or resolved type
46    Resolved(TypeId),
47}
48
49/// Classify a type for property access resolution.
50pub fn classify_for_property_access(
51    db: &dyn TypeDatabase,
52    type_id: TypeId,
53) -> PropertyAccessClassification {
54    let Some(key) = db.lookup(type_id) else {
55        return PropertyAccessClassification::Resolved(type_id);
56    };
57
58    match key {
59        TypeData::Object(_) | TypeData::ObjectWithIndex(_) => {
60            PropertyAccessClassification::Direct(type_id)
61        }
62        TypeData::TypeQuery(sym_ref) => PropertyAccessClassification::TypeQuery(sym_ref),
63        TypeData::Application(app_id) => PropertyAccessClassification::Application { app_id },
64        TypeData::Union(list_id) => {
65            let members = db.type_list(list_id);
66            PropertyAccessClassification::Union(members.to_vec())
67        }
68        TypeData::Intersection(list_id) => {
69            let members = db.type_list(list_id);
70            PropertyAccessClassification::Intersection(members.to_vec())
71        }
72        TypeData::IndexAccess(object, index) => {
73            PropertyAccessClassification::IndexAccess { object, index }
74        }
75        TypeData::ReadonlyType(inner) | TypeData::NoInfer(inner) => PropertyAccessClassification::Readonly(inner),
76        TypeData::Function(_) | TypeData::Callable(_) => {
77            PropertyAccessClassification::Callable(type_id)
78        }
79        TypeData::TypeParameter(info) | TypeData::Infer(info) => {
80            PropertyAccessClassification::TypeParameter {
81                constraint: info.constraint,
82            }
83        }
84        TypeData::Conditional(_) | TypeData::Mapped(_) | TypeData::KeyOf(_) => {
85            PropertyAccessClassification::NeedsEvaluation(type_id)
86        }
87        // BoundParameter is a resolved type (leaf node)
88        TypeData::BoundParameter(_)
89        // Primitives and resolved types (Lazy needs special handling when DefId lookup is implemented)
90        | TypeData::Intrinsic(_)
91        | TypeData::Literal(_)
92        | TypeData::Array(_)
93        | TypeData::Tuple(_)
94        | TypeData::Lazy(_)
95        | TypeData::Recursive(_)
96        | TypeData::TemplateLiteral(_)
97        | TypeData::UniqueSymbol(_)
98        | TypeData::ThisType
99        | TypeData::StringIntrinsic { .. }
100        | TypeData::ModuleNamespace(_)
101        | TypeData::Error
102        | TypeData::Enum(_, _) => PropertyAccessClassification::Resolved(type_id),
103    }
104}
105
106// =============================================================================
107// TypeTraversalKind - Classification for type structure traversal
108// =============================================================================
109
110/// Classification for traversing type structure to resolve symbols.
111///
112/// This enum is used by `ensure_application_symbols_resolved_inner` to
113/// determine how to traverse into nested types without directly matching
114/// on `TypeData` in the checker layer.
115#[derive(Debug, Clone)]
116pub enum TypeTraversalKind {
117    /// Application type - resolve base symbol and recurse into base and args
118    Application {
119        app_id: crate::types::TypeApplicationId,
120        base: TypeId,
121        args: Vec<TypeId>,
122    },
123    /// Symbol reference - resolve the symbol
124    SymbolRef(crate::types::SymbolRef),
125    /// Lazy type reference (`DefId`) - needs resolution before traversal
126    Lazy(crate::def::DefId),
127    /// Type query (typeof X) - value-space reference that needs resolution
128    TypeQuery(crate::types::SymbolRef),
129    /// Type parameter - recurse into constraint and default if present
130    TypeParameter {
131        constraint: Option<TypeId>,
132        default: Option<TypeId>,
133    },
134    /// Union or intersection - recurse into members
135    Members(Vec<TypeId>),
136    /// Function type - recurse into type params, params, return type, etc.
137    Function(crate::types::FunctionShapeId),
138    /// Callable type - recurse into signatures and properties
139    Callable(crate::types::CallableShapeId),
140    /// Object type - recurse into properties and index signatures
141    Object(crate::types::ObjectShapeId),
142    /// Array type - recurse into element type
143    Array(TypeId),
144    /// Tuple type - recurse into element types
145    Tuple(crate::types::TupleListId),
146    /// Conditional type - recurse into check, extends, true, and false types
147    Conditional(crate::types::ConditionalTypeId),
148    /// Mapped type - recurse into constraint, template, and name type
149    Mapped(crate::types::MappedTypeId),
150    /// Readonly wrapper - recurse into inner type
151    Readonly(TypeId),
152    /// Index access - recurse into object and index types
153    IndexAccess { object: TypeId, index: TypeId },
154    /// `KeyOf` - recurse into inner type
155    KeyOf(TypeId),
156    /// Template literal - extract types from spans
157    TemplateLiteral(Vec<TypeId>),
158    /// String intrinsic - traverse the type argument
159    StringIntrinsic(TypeId),
160    /// Terminal type - no further traversal needed
161    Terminal,
162}
163
164/// Classify a type for structure traversal (symbol resolution).
165///
166/// This function examines a type and returns information about how to
167/// traverse into its nested types. Used by `ensure_application_symbols_resolved_inner`.
168pub fn classify_for_traversal(db: &dyn TypeDatabase, type_id: TypeId) -> TypeTraversalKind {
169    let Some(key) = db.lookup(type_id) else {
170        return TypeTraversalKind::Terminal;
171    };
172
173    match key {
174        TypeData::Application(app_id) => {
175            let app = db.type_application(app_id);
176            TypeTraversalKind::Application {
177                app_id,
178                base: app.base,
179                args: app.args.clone(),
180            }
181        }
182        TypeData::TypeParameter(info) | TypeData::Infer(info) => TypeTraversalKind::TypeParameter {
183            constraint: info.constraint,
184            default: info.default,
185        },
186        TypeData::Union(list_id) | TypeData::Intersection(list_id) => {
187            let members = db.type_list(list_id);
188            TypeTraversalKind::Members(members.to_vec())
189        }
190        TypeData::Function(shape_id) => TypeTraversalKind::Function(shape_id),
191        TypeData::Callable(shape_id) => TypeTraversalKind::Callable(shape_id),
192        TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id) => {
193            TypeTraversalKind::Object(shape_id)
194        }
195        TypeData::Array(elem) => TypeTraversalKind::Array(elem),
196        TypeData::Tuple(list_id) => TypeTraversalKind::Tuple(list_id),
197        TypeData::Conditional(cond_id) => TypeTraversalKind::Conditional(cond_id),
198        TypeData::Mapped(mapped_id) => TypeTraversalKind::Mapped(mapped_id),
199        TypeData::ReadonlyType(inner) | TypeData::NoInfer(inner) => {
200            TypeTraversalKind::Readonly(inner)
201        }
202        TypeData::IndexAccess(object, index) => TypeTraversalKind::IndexAccess { object, index },
203        TypeData::KeyOf(inner) => TypeTraversalKind::KeyOf(inner),
204        // Template literal - extract types from spans for traversal
205        TypeData::TemplateLiteral(list_id) => {
206            let spans = db.template_list(list_id);
207            let types: Vec<TypeId> = spans
208                .iter()
209                .filter_map(|span| match span {
210                    TemplateSpan::Type(id) => Some(*id),
211                    _ => None,
212                })
213                .collect();
214            if types.is_empty() {
215                TypeTraversalKind::Terminal
216            } else {
217                TypeTraversalKind::TemplateLiteral(types)
218            }
219        }
220        // String intrinsic - traverse the type argument
221        TypeData::StringIntrinsic { type_arg, .. } => TypeTraversalKind::StringIntrinsic(type_arg),
222        // Lazy type reference - needs resolution before traversal
223        TypeData::Lazy(def_id) => TypeTraversalKind::Lazy(def_id),
224        // Type query (typeof X) - value-space reference
225        TypeData::TypeQuery(symbol_ref) => TypeTraversalKind::TypeQuery(symbol_ref),
226        // Terminal types - no nested types to traverse
227        TypeData::BoundParameter(_)
228        | TypeData::Intrinsic(_)
229        | TypeData::Literal(_)
230        | TypeData::Recursive(_)
231        | TypeData::UniqueSymbol(_)
232        | TypeData::ThisType
233        | TypeData::ModuleNamespace(_)
234        | TypeData::Error
235        | TypeData::Enum(_, _) => TypeTraversalKind::Terminal,
236    }
237}
238
239/// Check if a type is a lazy type and return the `DefId`.
240///
241/// This is a helper for checking if the base of an Application is a Lazy type.
242pub fn get_lazy_if_def(db: &dyn TypeDatabase, type_id: TypeId) -> Option<crate::def::DefId> {
243    match db.lookup(type_id) {
244        Some(TypeData::Lazy(def_id)) => Some(def_id),
245        _ => None,
246    }
247}
248
249/// High-level property traversal classification for diagnostics/reporting.
250///
251/// This keeps traversal-shape branching inside solver queries so checker code
252/// can remain thin orchestration.
253#[derive(Debug, Clone)]
254pub enum PropertyTraversalKind {
255    Object(std::sync::Arc<crate::types::ObjectShape>),
256    Callable(std::sync::Arc<crate::types::CallableShape>),
257    Members(Vec<TypeId>),
258    Other,
259}
260
261/// Classify a type into a property traversal shape for checker diagnostics.
262pub fn classify_property_traversal(
263    db: &dyn TypeDatabase,
264    type_id: TypeId,
265) -> PropertyTraversalKind {
266    match classify_for_traversal(db, type_id) {
267        TypeTraversalKind::Object(_) => get_object_shape(db, type_id)
268            .map_or(PropertyTraversalKind::Other, PropertyTraversalKind::Object),
269        TypeTraversalKind::Callable(_) => get_callable_shape(db, type_id).map_or(
270            PropertyTraversalKind::Other,
271            PropertyTraversalKind::Callable,
272        ),
273        TypeTraversalKind::Members(members) => PropertyTraversalKind::Members(members),
274        _ => PropertyTraversalKind::Other,
275    }
276}
277
278/// Collect property names reachable from a type for diagnostics/suggestions.
279///
280/// Traversal shape decisions stay in solver so checker can remain orchestration-only.
281pub fn collect_property_name_atoms_for_diagnostics(
282    db: &dyn TypeDatabase,
283    type_id: TypeId,
284    max_depth: usize,
285) -> Vec<Atom> {
286    fn collect_inner(
287        db: &dyn TypeDatabase,
288        type_id: TypeId,
289        out: &mut Vec<Atom>,
290        depth: usize,
291        max_depth: usize,
292    ) {
293        if depth > max_depth {
294            return;
295        }
296        match classify_property_traversal(db, type_id) {
297            PropertyTraversalKind::Object(shape) => {
298                for prop in &shape.properties {
299                    out.push(prop.name);
300                }
301            }
302            PropertyTraversalKind::Callable(shape) => {
303                for prop in &shape.properties {
304                    out.push(prop.name);
305                }
306            }
307            PropertyTraversalKind::Members(members) => {
308                for member in members {
309                    collect_inner(db, member, out, depth + 1, max_depth);
310                }
311            }
312            PropertyTraversalKind::Other => {}
313        }
314    }
315
316    let mut atoms = Vec::new();
317    collect_inner(db, type_id, &mut atoms, 0, max_depth);
318    atoms.sort_unstable();
319    atoms.dedup();
320    atoms
321}
322
323/// Collect property names accessible on a type for spelling suggestions.
324///
325/// For union types, only properties present in ALL members are returned (intersection).
326/// This matches tsc: "did you mean" for union access uses only common/accessible properties.
327pub fn collect_accessible_property_names_for_suggestion(
328    db: &dyn TypeDatabase,
329    type_id: TypeId,
330    max_depth: usize,
331) -> Vec<Atom> {
332    if let Some(TypeData::Union(list_id)) = db.lookup(type_id) {
333        let members = db.type_list(list_id).to_vec();
334        if members.is_empty() {
335            return vec![];
336        }
337        let mut common = collect_property_name_atoms_for_diagnostics(db, members[0], max_depth);
338        common.sort_unstable();
339        common.dedup();
340        for &member in &members[1..] {
341            let mut member_props =
342                collect_property_name_atoms_for_diagnostics(db, member, max_depth);
343            member_props.sort_unstable();
344            member_props.dedup();
345            common.retain(|a| member_props.binary_search(a).is_ok());
346            if common.is_empty() {
347                return vec![];
348            }
349        }
350        return common;
351    }
352    collect_property_name_atoms_for_diagnostics(db, type_id, max_depth)
353}
354
355/// Checks if a type is exclusively `null`, `undefined`, or a union of both.
356pub fn is_only_null_or_undefined(db: &dyn TypeDatabase, type_id: TypeId) -> bool {
357    if type_id == TypeId::NULL || type_id == TypeId::UNDEFINED {
358        return true;
359    }
360    match db.lookup(type_id) {
361        Some(TypeData::Intrinsic(IntrinsicKind::Null | IntrinsicKind::Undefined)) => true,
362        Some(TypeData::Union(list_id)) => {
363            let members = db.type_list(list_id);
364            members.iter().all(|&m| is_only_null_or_undefined(db, m))
365        }
366        _ => false,
367    }
368}