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;