Skip to content

Commit 05e2ef1

Browse files
committed
Support casing modifiers in string template types
1 parent 45ea576 commit 05e2ef1

File tree

6 files changed

+74
-23
lines changed

6 files changed

+74
-23
lines changed

src/compiler/checker.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4462,10 +4462,12 @@ namespace ts {
44624462
}
44634463
if (type.flags & TypeFlags.Template) {
44644464
const texts = (<TemplateType>type).texts;
4465+
const casings = (<TemplateType>type).casings;
44654466
const types = (<TemplateType>type).types;
44664467
const templateHead = factory.createTemplateHead(texts[0]);
44674468
const templateSpans = factory.createNodeArray(
44684469
map(types, (t, i) => factory.createTemplateTypeSpan(
4470+
casings[i],
44694471
typeToTypeNodeHelper(t, context),
44704472
(i < types.length - 1 ? factory.createTemplateMiddle : factory.createTemplateTail)(texts[i + 1]))));
44714473
context.approximateLength += 2;
@@ -10864,7 +10866,7 @@ namespace ts {
1086410866
if (t.flags & TypeFlags.Template) {
1086510867
const types = (<TemplateType>t).types;
1086610868
const constraints = mapDefined(types, getBaseConstraint);
10867-
return constraints.length === types.length ? getTemplateType((<TemplateType>t).texts, constraints) : stringType;
10869+
return constraints.length === types.length ? getTemplateType((<TemplateType>t).texts, (<TemplateType>t).casings, constraints) : stringType;
1086810870
}
1086910871
if (t.flags & TypeFlags.IndexedAccess) {
1087010872
const baseObjectType = getBaseConstraint((<IndexedAccessType>t).objectType);
@@ -13396,32 +13398,28 @@ namespace ts {
1339613398
if (!links.resolvedType) {
1339713399
links.resolvedType = getTemplateType(
1339813400
[node.head.text, ...map(node.templateSpans, span => span.literal.text)],
13401+
map(node.templateSpans, span => span.casing),
1339913402
map(node.templateSpans, span => getTypeFromTypeNode(span.type)));
1340013403
}
1340113404
return links.resolvedType;
1340213405
}
1340313406

13404-
function getTemplateType(texts: readonly string[], types: readonly Type[]): Type {
13407+
function getTemplateType(texts: readonly string[], casings: readonly TemplateCasing[], types: readonly Type[]): Type {
1340513408
const unionIndex = findIndex(types, t => !!(t.flags & (TypeFlags.Never | TypeFlags.Union)));
1340613409
if (unionIndex >= 0) {
1340713410
return checkCrossProductUnion(types) ?
13408-
mapType(types[unionIndex], t => getTemplateType(texts, replaceElement(types, unionIndex, t))) :
13411+
mapType(types[unionIndex], t => getTemplateType(texts, casings, replaceElement(types, unionIndex, t))) :
1340913412
errorType;
1341013413
}
1341113414
let i = 0;
1341213415
while (i < types.length) {
1341313416
const t = types[i];
1341413417
if (t.flags & TypeFlags.Literal) {
13415-
const s = getTemplateStringForType(t) || "";
13418+
const s = applyTemplateCasing(getTemplateStringForType(t) || "", casings[i]);
1341613419
texts = [...texts.slice(0, i), texts[i] + s + texts[i + 1], ...texts.slice(i + 2)];
13420+
casings = [...casings.slice(0, i), ...casings.slice(i + 1)];
1341713421
types = [...types.slice(0, i), ...types.slice(i + 1)];
1341813422
}
13419-
else if (t.flags & TypeFlags.Template) {
13420-
const ts = (<TemplateType>t).texts;
13421-
texts = [...texts.slice(0, i), texts[i] + ts[0], ...ts.slice(1, -1), ts[ts.length - 1] + texts[i + 1], ...texts.slice(i + 2)];
13422-
types = [...types.slice(0, i), ...(<TemplateType>t).types, ...types.slice(i + 1)];
13423-
i += (<TemplateType>t).types.length;
13424-
}
1342513423
else if (isGenericIndexType(t)) {
1342613424
i++;
1342713425
}
@@ -13432,10 +13430,10 @@ namespace ts {
1343213430
if (types.length === 0) {
1343313431
return getLiteralType(texts[0]);
1343413432
}
13435-
const id = `${getTypeListId(types)}|${map(texts, t => t.length).join(",")}|${texts.join("")}`;
13433+
const id = `${getTypeListId(types)}|${casings.join(",")}|${map(texts, t => t.length).join(",")}|${texts.join("")}`;
1343613434
let type = templateTypes.get(id);
1343713435
if (!type) {
13438-
templateTypes.set(id, type = createTemplateType(texts, types));
13436+
templateTypes.set(id, type = createTemplateType(texts, casings, types));
1343913437
}
1344013438
return type;
1344113439
}
@@ -13448,9 +13446,20 @@ namespace ts {
1344813446
undefined;
1344913447
}
1345013448

13451-
function createTemplateType(texts: readonly string[], types: readonly Type[]) {
13449+
function applyTemplateCasing(str: string, casing: TemplateCasing) {
13450+
switch (casing) {
13451+
case TemplateCasing.Uppercase: return str.toUpperCase();
13452+
case TemplateCasing.Lowercase: return str.toLowerCase();
13453+
case TemplateCasing.Capitalize: return str.charAt(0).toUpperCase() + str.slice(1);
13454+
case TemplateCasing.Uncapitalize: return str.charAt(0).toLowerCase() + str.slice(1);
13455+
}
13456+
return str;
13457+
}
13458+
13459+
function createTemplateType(texts: readonly string[], casings: readonly TemplateCasing[], types: readonly Type[]) {
1345213460
const type = <TemplateType>createType(TypeFlags.Template);
1345313461
type.texts = texts;
13462+
type.casings = casings;
1345413463
type.types = types;
1345513464
return type;
1345613465
}
@@ -15072,7 +15081,7 @@ namespace ts {
1507215081
return getIndexType(instantiateType((<IndexType>type).type, mapper));
1507315082
}
1507415083
if (flags & TypeFlags.Template) {
15075-
return getTemplateType((<TemplateType>type).texts, instantiateTypes((<TemplateType>type).types, mapper));
15084+
return getTemplateType((<TemplateType>type).texts, (<TemplateType>type).casings, instantiateTypes((<TemplateType>type).types, mapper));
1507615085
}
1507715086
if (flags & TypeFlags.IndexedAccess) {
1507815087
return getIndexedAccessType(instantiateType((<IndexedAccessType>type).objectType, mapper), instantiateType((<IndexedAccessType>type).indexType, mapper), /*accessNode*/ undefined, type.aliasSymbol, instantiateTypes(type.aliasTypeArguments, mapper));
@@ -19931,7 +19940,7 @@ namespace ts {
1993119940
function inferToTemplateType(source: Type, target: TemplateType) {
1993219941
if (source.flags & (TypeFlags.StringLike | TypeFlags.Index)) {
1993319942
const matches = source.flags & TypeFlags.StringLiteral ? inferLiteralsFromTemplateType(<StringLiteralType>source, target) :
19934-
source.flags & TypeFlags.Template && arraysEqual((<TemplateType>source).texts, target.texts) ? (<TemplateType>source).types :
19943+
source.flags & TypeFlags.Template && arraysEqual((<TemplateType>source).texts, target.texts) && arraysEqual((<TemplateType>source).casings, target.casings)? (<TemplateType>source).types :
1993519944
undefined;
1993619945
const types = target.types;
1993719946
for (let i = 0; i < types.length; i++) {

src/compiler/emitter.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2009,6 +2009,15 @@ namespace ts {
20092009
}
20102010

20112011
function emitTemplateTypeSpan(node: TemplateTypeSpan) {
2012+
const keyword = node.casing === TemplateCasing.Uppercase ? "uppercase" :
2013+
node.casing === TemplateCasing.Lowercase ? "lowercase" :
2014+
node.casing === TemplateCasing.Capitalize ? "capitalize" :
2015+
node.casing === TemplateCasing.Uncapitalize ? "uncapitalize" :
2016+
undefined;
2017+
if (keyword) {
2018+
writeKeyword(keyword);
2019+
writeSpace();
2020+
}
20122021
emit(node.type);
20132022
emit(node.literal);
20142023
}

src/compiler/factory/nodeFactory.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,19 +1599,21 @@ namespace ts {
15991599
}
16001600

16011601
// @api
1602-
function createTemplateTypeSpan(type: TypeNode, literal: TemplateMiddle | TemplateTail) {
1602+
function createTemplateTypeSpan(casing: TemplateCasing, type: TypeNode, literal: TemplateMiddle | TemplateTail) {
16031603
const node = createBaseNode<TemplateTypeSpan>(SyntaxKind.TemplateTypeSpan);
1604+
node.casing = casing;
16041605
node.type = type;
16051606
node.literal = literal;
16061607
node.transformFlags = TransformFlags.ContainsTypeScript;
16071608
return node;
16081609
}
16091610

16101611
// @api
1611-
function updateTemplateTypeSpan(node: TemplateTypeSpan, type: TypeNode, literal: TemplateMiddle | TemplateTail) {
1612-
return node.type !== type
1612+
function updateTemplateTypeSpan(casing: TemplateCasing, node: TemplateTypeSpan, type: TypeNode, literal: TemplateMiddle | TemplateTail) {
1613+
return node.casing !== casing
1614+
|| node.type !== type
16131615
|| node.literal !== literal
1614-
? update(createTemplateTypeSpan(type, literal), node)
1616+
? update(createTemplateTypeSpan(casing, type, literal), node)
16151617
: node;
16161618
}
16171619

src/compiler/parser.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2608,13 +2608,22 @@ namespace ts {
26082608
const pos = getNodePos();
26092609
return finishNode(
26102610
factory.createTemplateTypeSpan(
2611+
parseTemplateCasing(),
26112612
parseType(),
26122613
parseLiteralOfTemplateSpan(/*isTaggedTemplate*/ false)
26132614
),
26142615
pos
26152616
);
26162617
}
26172618

2619+
function parseTemplateCasing(): TemplateCasing {
2620+
return parseOptional(SyntaxKind.UppercaseKeyword) ? TemplateCasing.Uppercase :
2621+
parseOptional(SyntaxKind.LowercaseKeyword) ? TemplateCasing.Lowercase :
2622+
parseOptional(SyntaxKind.CapitalizeKeyword) ? TemplateCasing.Capitalize :
2623+
parseOptional(SyntaxKind.UncapitalizeKeyword) ? TemplateCasing.Uncapitalize :
2624+
TemplateCasing.None;
2625+
}
2626+
26182627
function parseLiteralOfTemplateSpan(isTaggedTemplate: boolean) {
26192628
if (token() === SyntaxKind.CloseBraceToken) {
26202629
reScanTemplateToken(isTaggedTemplate);

src/compiler/scanner.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ namespace ts {
151151
yield: SyntaxKind.YieldKeyword,
152152
async: SyntaxKind.AsyncKeyword,
153153
await: SyntaxKind.AwaitKeyword,
154+
uppercase: SyntaxKind.UppercaseKeyword,
155+
lowercase: SyntaxKind.LowercaseKeyword,
156+
capitalize: SyntaxKind.CapitalizeKeyword,
157+
uncapitalize: SyntaxKind.UncapitalizeKeyword,
154158
of: SyntaxKind.OfKeyword,
155159
};
156160

src/compiler/types.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ namespace ts {
186186
FromKeyword,
187187
GlobalKeyword,
188188
BigIntKeyword,
189+
UppercaseKeyword,
190+
LowercaseKeyword,
191+
CapitalizeKeyword,
192+
UncapitalizeKeyword,
189193
OfKeyword, // LastKeyword and LastToken and LastContextualKeyword
190194

191195
// Parse tree nodes
@@ -538,6 +542,7 @@ namespace ts {
538542
| SyntaxKind.BigIntKeyword
539543
| SyntaxKind.BooleanKeyword
540544
| SyntaxKind.BreakKeyword
545+
| SyntaxKind.CapitalizeKeyword
541546
| SyntaxKind.CaseKeyword
542547
| SyntaxKind.CatchKeyword
543548
| SyntaxKind.ClassKeyword
@@ -570,6 +575,7 @@ namespace ts {
570575
| SyntaxKind.IsKeyword
571576
| SyntaxKind.KeyOfKeyword
572577
| SyntaxKind.LetKeyword
578+
| SyntaxKind.LowercaseKeyword
573579
| SyntaxKind.ModuleKeyword
574580
| SyntaxKind.NamespaceKeyword
575581
| SyntaxKind.NeverKeyword
@@ -597,9 +603,11 @@ namespace ts {
597603
| SyntaxKind.TryKeyword
598604
| SyntaxKind.TypeKeyword
599605
| SyntaxKind.TypeOfKeyword
606+
| SyntaxKind.UncapitalizeKeyword
600607
| SyntaxKind.UndefinedKeyword
601608
| SyntaxKind.UniqueKeyword
602609
| SyntaxKind.UnknownKeyword
610+
| SyntaxKind.UppercaseKeyword
603611
| SyntaxKind.VarKeyword
604612
| SyntaxKind.VoidKeyword
605613
| SyntaxKind.WhileKeyword
@@ -1653,9 +1661,18 @@ namespace ts {
16531661
export interface TemplateTypeSpan extends TypeNode {
16541662
readonly kind: SyntaxKind.TemplateTypeSpan,
16551663
readonly parent: TemplateTypeNode;
1664+
readonly casing: TemplateCasing;
16561665
readonly type: TypeNode;
16571666
readonly literal: TemplateMiddle | TemplateTail;
1658-
}
1667+
}
1668+
1669+
export const enum TemplateCasing {
1670+
None,
1671+
Uppercase,
1672+
Lowercase,
1673+
Capitalize,
1674+
Uncapitalize,
1675+
}
16591676

16601677
// Note: 'brands' in our syntax nodes serve to give us a small amount of nominal typing.
16611678
// Consider 'Expression'. Without the brand, 'Expression' is actually no different
@@ -5337,7 +5354,8 @@ namespace ts {
53375354
}
53385355

53395356
export interface TemplateType extends InstantiableType {
5340-
texts: readonly string[]; // Always one element longer than types
5357+
texts: readonly string[]; // Always one element longer than casings/types
5358+
casings: readonly TemplateCasing[]; // Always at least one element
53415359
types: readonly Type[]; // Always at least one element
53425360
}
53435361

@@ -6720,8 +6738,8 @@ namespace ts {
67206738
createIndexSignature(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode): IndexSignatureDeclaration;
67216739
/* @internal */ createIndexSignature(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined): IndexSignatureDeclaration; // eslint-disable-line @typescript-eslint/unified-signatures
67226740
updateIndexSignature(node: IndexSignatureDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode): IndexSignatureDeclaration;
6723-
createTemplateTypeSpan(type: TypeNode, literal: TemplateMiddle | TemplateTail): TemplateTypeSpan;
6724-
updateTemplateTypeSpan(node: TemplateTypeSpan, type: TypeNode, literal: TemplateMiddle | TemplateTail): TemplateTypeSpan;
6741+
createTemplateTypeSpan(casing: TemplateCasing, type: TypeNode, literal: TemplateMiddle | TemplateTail): TemplateTypeSpan;
6742+
updateTemplateTypeSpan(casing: TemplateCasing, node: TemplateTypeSpan, type: TypeNode, literal: TemplateMiddle | TemplateTail): TemplateTypeSpan;
67256743

67266744
//
67276745
// Types

0 commit comments

Comments
 (0)