Skip to main content

tsz_solver/objects/
literal.rs

1//! Object literal type construction.
2//!
3//! This module provides a builder for constructing object types from
4//! property information, with support for:
5//! - Property collection and merging
6//! - Spread operators
7//! - Contextual typing
8
9use crate::TypeDatabase;
10use crate::contextual::ContextualTypeContext;
11use crate::types::{ObjectFlags, PropertyInfo, TypeData, TypeId};
12use rustc_hash::FxHashMap;
13use tsz_common::interner::Atom;
14
15/// Builder for constructing object literal types.
16///
17/// This is a solver component that handles the pure type construction
18/// aspects of object literals, separate from AST traversal and error reporting.
19pub struct ObjectLiteralBuilder<'a> {
20    db: &'a dyn TypeDatabase,
21}
22
23impl<'a> ObjectLiteralBuilder<'a> {
24    /// Create a new object literal builder.
25    pub fn new(db: &'a dyn TypeDatabase) -> Self {
26        ObjectLiteralBuilder { db }
27    }
28
29    /// Build object type from properties.
30    ///
31    /// This creates a fresh object type with the given properties.
32    /// The properties are sorted by name for consistent hashing.
33    pub fn build_object_type(&self, properties: Vec<PropertyInfo>) -> TypeId {
34        self.db.object_fresh(properties)
35    }
36
37    /// Build object type with index signature.
38    ///
39    /// Creates an object type with both properties and optional
40    /// string/number index signatures.
41    pub fn build_object_with_index(
42        &self,
43        properties: Vec<PropertyInfo>,
44        string_index: Option<crate::types::IndexSignature>,
45        number_index: Option<crate::types::IndexSignature>,
46    ) -> TypeId {
47        use crate::types::ObjectShape;
48        self.db.object_with_index(ObjectShape {
49            flags: ObjectFlags::FRESH_LITERAL,
50            properties,
51            string_index,
52            number_index,
53            symbol: None,
54        })
55    }
56
57    /// Merge spread properties into base properties.
58    ///
59    /// Given a base set of properties and a spread type, extracts all properties
60    /// from the spread type and merges them into the base (later properties override).
61    ///
62    /// Example:
63    /// ```typescript
64    /// const base = { x: 1 };
65    /// const spread = { y: 2, x: 3 };
66    /// // Result: { x: 3, y: 2 }
67    /// ```
68    pub fn merge_spread(
69        &self,
70        base_properties: Vec<PropertyInfo>,
71        spread_type: TypeId,
72    ) -> Vec<PropertyInfo> {
73        let mut merged: FxHashMap<Atom, PropertyInfo> =
74            base_properties.into_iter().map(|p| (p.name, p)).collect();
75
76        // Extract properties from spread type
77        for prop in self.extract_properties(spread_type) {
78            merged.insert(prop.name, prop);
79        }
80
81        merged.into_values().collect()
82    }
83
84    /// Apply contextual typing to property types.
85    ///
86    /// When an object literal has a contextual type, each property value
87    /// should be narrowed by the corresponding property type from the context.
88    ///
89    /// Example:
90    /// ```typescript
91    /// type Point = { x: number; y: number };
92    /// const p: Point = { x: 1, y: '2' };  // Error: '2' is not assignable to number
93    /// ```
94    pub fn apply_contextual_types(
95        &self,
96        properties: Vec<PropertyInfo>,
97        contextual: TypeId,
98    ) -> Vec<PropertyInfo> {
99        let ctx = ContextualTypeContext::with_expected(self.db, contextual);
100
101        properties
102            .into_iter()
103            .map(|prop| {
104                let prop_name = self.db.resolve_atom_ref(prop.name);
105                let contextual_prop_type = ctx.get_property_type(&prop_name);
106
107                if let Some(ctx_type) = contextual_prop_type {
108                    PropertyInfo {
109                        type_id: self.apply_contextual_type(prop.type_id, ctx_type),
110                        ..prop
111                    }
112                } else {
113                    prop
114                }
115            })
116            .collect()
117    }
118
119    /// Collect all properties for object spread and spread-mutation paths.
120    ///
121    /// This is the solver-side public entrypoint used by query APIs for object
122    /// spread property extraction, including `CheckerState::get_type_of_object_literal`.
123    pub fn collect_spread_properties(&self, spread_type: TypeId) -> Vec<PropertyInfo> {
124        self.extract_properties(spread_type)
125    }
126
127    /// Extract all properties from a type (for spread operations).
128    ///
129    /// This handles:
130    /// - Object types
131    /// - Callable types (function properties)
132    /// - Intersection types (merge all properties)
133    ///
134    /// Returns an empty vec for types that don't have properties.
135    fn extract_properties(&self, type_id: TypeId) -> Vec<PropertyInfo> {
136        let Some(key) = self.db.lookup(type_id) else {
137            return Vec::new();
138        };
139
140        match key {
141            TypeData::Object(shape_id) | TypeData::ObjectWithIndex(shape_id) => {
142                let shape = self.db.object_shape(shape_id);
143                shape.properties.to_vec()
144            }
145            TypeData::Callable(shape_id) => {
146                let shape = self.db.callable_shape(shape_id);
147                shape.properties.to_vec()
148            }
149            TypeData::Intersection(list_id) => {
150                let members = self.db.type_list(list_id);
151                let mut merged: FxHashMap<Atom, PropertyInfo> = FxHashMap::default();
152
153                for &member in members.iter() {
154                    for prop in self.extract_properties(member) {
155                        merged.insert(prop.name, prop);
156                    }
157                }
158
159                merged.into_values().collect()
160            }
161            _ => Vec::new(),
162        }
163    }
164
165    /// Apply contextual type to a value type.
166    ///
167    /// Uses bidirectional type inference to narrow the value type
168    /// based on the expected contextual type.
169    fn apply_contextual_type(&self, value_type: TypeId, ctx_type: TypeId) -> TypeId {
170        // If the value type is 'any' or the contextual type is 'any', no narrowing occurs
171        if value_type == TypeId::ANY || ctx_type == TypeId::ANY {
172            return value_type;
173        }
174
175        // If the value type already satisfies the contextual type, use the contextual type
176        // This is the key insight from bidirectional typing
177        //
178        // FIX: Use CompatChecker (The Lawyer) instead of SubtypeChecker (The Judge)
179        // to ensure TypeScript's assignability rules are applied during contextual typing.
180        // This is critical for freshness checks (excess properties) and other TS rules.
181        use crate::relations::compat::CompatChecker;
182        let mut checker = CompatChecker::new(self.db);
183
184        if checker.is_assignable(value_type, ctx_type) {
185            ctx_type
186        } else {
187            value_type
188        }
189    }
190}
191
192#[cfg(test)]
193#[path = "../../tests/object_literal_tests.rs"]
194mod tests;