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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
//! String intrinsic type evaluation.
//!
//! Handles TypeScript's string manipulation intrinsics:
//! - Uppercase<T>
//! - Lowercase<T>
//! - Capitalize<T>
//! - Uncapitalize<T>
use crate::subtype::TypeResolver;
use crate::types::{
IntrinsicKind, LiteralValue, StringIntrinsicKind, TemplateLiteralId, TemplateSpan, TypeData,
TypeId,
};
use super::super::evaluate::TypeEvaluator;
impl<'a, R: TypeResolver> TypeEvaluator<'a, R> {
/// Evaluate string manipulation intrinsic types (Uppercase, Lowercase, Capitalize, Uncapitalize)
/// These distribute over unions and transform string literal types
pub(crate) fn evaluate_string_intrinsic(
&mut self,
kind: StringIntrinsicKind,
type_arg: TypeId,
) -> TypeId {
// First evaluate the type argument
let evaluated_arg = self.evaluate(type_arg);
let key = match self.interner().lookup(evaluated_arg) {
Some(k) => k,
None => return TypeId::ERROR,
};
match key {
// Handle unions - distribute the operation over each member
// Use recurse_string_intrinsic to respect depth limits
TypeData::Union(members) => {
let members = self.interner().type_list(members);
let transformed: Vec<TypeId> = members
.iter()
.map(|&member| self.recurse_string_intrinsic(kind, member))
.collect();
self.interner().union(transformed)
}
// String literal types - apply the transformation
TypeData::Literal(LiteralValue::String(atom)) => {
let s = self.interner().resolve_atom_ref(atom);
let transformed = match kind {
StringIntrinsicKind::Uppercase => s.to_uppercase(),
StringIntrinsicKind::Lowercase => s.to_lowercase(),
StringIntrinsicKind::Capitalize => {
if s.is_empty() {
s.to_string()
} else {
let mut chars = s.chars();
match chars.next() {
Some(first) => {
let upper: String = first.to_uppercase().collect();
upper + chars.as_str()
}
None => s.to_string(),
}
}
}
StringIntrinsicKind::Uncapitalize => {
if s.is_empty() {
s.to_string()
} else {
let mut chars = s.chars();
match chars.next() {
Some(first) => {
let lower: String = first.to_lowercase().collect();
lower + chars.as_str()
}
None => s.to_string(),
}
}
}
};
self.interner().literal_string(&transformed)
}
// Template literal types - apply the transformation
TypeData::TemplateLiteral(spans) => {
self.apply_string_intrinsic_to_template_literal(kind, spans)
}
// The intrinsic string type passes through unchanged but wrapped in the intrinsic
// so we preserve the Uppercase/Lowercase constraint (e.g. `string extends Uppercase<string>` is false).
TypeData::Intrinsic(IntrinsicKind::String) => {
self.interner().string_intrinsic(kind, evaluated_arg)
}
// For type parameters and other deferred types, keep as StringIntrinsic
TypeData::TypeParameter(_)
| TypeData::Infer(_)
| TypeData::KeyOf(_)
| TypeData::IndexAccess(_, _) => self.interner().string_intrinsic(kind, evaluated_arg),
// Handle chained string intrinsics: Uppercase<Lowercase<T>>
// The inner intrinsic already wraps the type, so wrap again with outer
TypeData::StringIntrinsic {
kind: _inner_kind,
type_arg: _inner_arg,
} => {
// Wrap the already-evaluated intrinsic with the outer one
// This creates Uppercase<Lowercase<T>> structure which will be
// evaluated layer by layer when the type parameter is substituted
self.interner().string_intrinsic(kind, evaluated_arg)
}
// For all other types, return error
_ => TypeId::ERROR,
}
}
/// Helper to recursively evaluate string intrinsic while respecting depth limits.
pub(crate) fn recurse_string_intrinsic(
&mut self,
kind: StringIntrinsicKind,
type_arg: TypeId,
) -> TypeId {
let string_intrinsic = self.interner().string_intrinsic(kind, type_arg);
self.evaluate(string_intrinsic)
}
/// Apply a string intrinsic to a template literal type
///
/// This handles cases like `Uppercase<hello-${string}>`, which should produce
/// a template literal with uppercase text spans: `HELLO-${string}`
///
/// For template literals with type interpolations:
/// - Text spans are transformed (uppercased, lowercased, etc.)
/// - Type spans are wrapped in the same string intrinsic
/// - For Capitalize/Uncapitalize, special handling for the first span
pub(crate) fn apply_string_intrinsic_to_template_literal(
&self,
kind: StringIntrinsicKind,
spans: TemplateLiteralId,
) -> TypeId {
let span_list = self.interner().template_list(spans);
// Check if all spans are Text (no type interpolation)
let all_text = span_list
.iter()
.all(|span| matches!(span, TemplateSpan::Text(_)));
if all_text {
// All spans are text - we can concatenate and transform
let mut result = String::new();
for span in span_list.iter() {
if let TemplateSpan::Text(atom) = span {
let text = self.interner().resolve_atom_ref(*atom);
result.push_str(&text);
}
}
let transformed = self.apply_string_transform(kind, &result);
return self.interner().literal_string(&transformed);
}
// Template literal with type interpolations
// Transform text spans and wrap type spans in the intrinsic
let mut new_spans: Vec<TemplateSpan> = Vec::with_capacity(span_list.len());
let mut is_first_span = true;
for span in span_list.iter() {
match span {
TemplateSpan::Text(atom) => {
let text = self.interner().resolve_atom_ref(*atom);
let transformed = if is_first_span {
// For first text span, apply full transformation (including Capitalize/Uncapitalize)
self.apply_string_transform(kind, &text)
} else {
// For subsequent text spans, only apply Uppercase/Lowercase
// Capitalize/Uncapitalize only affect the first character
match kind {
StringIntrinsicKind::Uppercase => text.to_uppercase(),
StringIntrinsicKind::Lowercase => text.to_lowercase(),
StringIntrinsicKind::Capitalize | StringIntrinsicKind::Uncapitalize => {
text.to_string()
}
}
};
let new_atom = self.interner().intern_string(&transformed);
new_spans.push(TemplateSpan::Text(new_atom));
// After a non-empty text span, subsequent spans are not "first"
if !text.is_empty() {
is_first_span = false;
}
}
TemplateSpan::Type(type_id) => {
// For type interpolations, wrap in the appropriate string intrinsic
// For Capitalize/Uncapitalize on non-first position, we don't wrap
// since those only affect the first character
let wrapped_type = if is_first_span {
// First position type: apply the intrinsic
self.interner().string_intrinsic(kind, *type_id)
} else {
// Non-first position: only Uppercase/Lowercase apply
match kind {
StringIntrinsicKind::Uppercase | StringIntrinsicKind::Lowercase => {
self.interner().string_intrinsic(kind, *type_id)
}
StringIntrinsicKind::Capitalize | StringIntrinsicKind::Uncapitalize => {
// Capitalize/Uncapitalize don't affect non-first positions
*type_id
}
}
};
new_spans.push(TemplateSpan::Type(wrapped_type));
// After a type span, we're definitely not first anymore
is_first_span = false;
}
}
}
self.interner().template_literal(new_spans)
}
/// Apply a string transformation to a string value
pub(crate) fn apply_string_transform(&self, kind: StringIntrinsicKind, s: &str) -> String {
match kind {
StringIntrinsicKind::Uppercase => s.to_uppercase(),
StringIntrinsicKind::Lowercase => s.to_lowercase(),
StringIntrinsicKind::Capitalize => {
if s.is_empty() {
s.to_string()
} else {
let mut chars = s.chars();
match chars.next() {
Some(first) => {
let upper: String = first.to_uppercase().collect();
upper + chars.as_str()
}
None => s.to_string(),
}
}
}
StringIntrinsicKind::Uncapitalize => {
if s.is_empty() {
s.to_string()
} else {
let mut chars = s.chars();
match chars.next() {
Some(first) => {
let lower: String = first.to_lowercase().collect();
lower + chars.as_str()
}
None => s.to_string(),
}
}
}
}
}
}