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