Skip to content

Commit 9e7cdb4

Browse files
committed
Support string valued members in literal enums
1 parent d2fdebe commit 9e7cdb4

File tree

8 files changed

+90
-59
lines changed

8 files changed

+90
-59
lines changed

src/compiler/checker.ts

Lines changed: 49 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ namespace ts {
4747

4848
let typeCount = 0;
4949
let symbolCount = 0;
50+
let enumCount = 0;
5051
let symbolInstantiationDepth = 0;
5152

5253
const emptyArray: any[] = [];
@@ -210,8 +211,7 @@ namespace ts {
210211
const tupleTypes: GenericType[] = [];
211212
const unionTypes = createMap<UnionType>();
212213
const intersectionTypes = createMap<IntersectionType>();
213-
const stringLiteralTypes = createMap<LiteralType>();
214-
const numericLiteralTypes = createMap<LiteralType>();
214+
const literalTypes = createMap<LiteralType>();
215215
const indexedAccessTypes = createMap<IndexedAccessType>();
216216
const evolvingArrayTypes: EvolvingArrayType[] = [];
217217

@@ -4904,34 +4904,36 @@ namespace ts {
49044904
return links.declaredType;
49054905
}
49064906

4907-
function isLiteralEnumMember(symbol: Symbol, member: EnumMember) {
4907+
function isLiteralEnumMember(member: EnumMember) {
49084908
const expr = member.initializer;
49094909
if (!expr) {
49104910
return !isInAmbientContext(member);
49114911
}
4912-
return expr.kind === SyntaxKind.NumericLiteral ||
4912+
return expr.kind === SyntaxKind.StringLiteral || expr.kind === SyntaxKind.NumericLiteral ||
49134913
expr.kind === SyntaxKind.PrefixUnaryExpression && (<PrefixUnaryExpression>expr).operator === SyntaxKind.MinusToken &&
49144914
(<PrefixUnaryExpression>expr).operand.kind === SyntaxKind.NumericLiteral ||
4915-
expr.kind === SyntaxKind.Identifier && !!symbol.exports.get((<Identifier>expr).text);
4915+
expr.kind === SyntaxKind.Identifier && (nodeIsMissing(expr) || !!getSymbolOfNode(member.parent).exports.get((<Identifier>expr).text));
49164916
}
49174917

4918-
function enumHasLiteralMembers(symbol: Symbol) {
4918+
function getEnumKind(symbol: Symbol): EnumKind {
4919+
const links = getSymbolLinks(symbol);
4920+
if (links.enumKind !== undefined) {
4921+
return links.enumKind;
4922+
}
4923+
let hasNonLiteralMember = false;
49194924
for (const declaration of symbol.declarations) {
49204925
if (declaration.kind === SyntaxKind.EnumDeclaration) {
49214926
for (const member of (<EnumDeclaration>declaration).members) {
4922-
if (!isLiteralEnumMember(symbol, member)) {
4923-
return false;
4927+
if (member.initializer && member.initializer.kind === SyntaxKind.StringLiteral) {
4928+
return links.enumKind = EnumKind.Literal;
4929+
}
4930+
if (!isLiteralEnumMember(member)) {
4931+
hasNonLiteralMember = true;
49244932
}
49254933
}
49264934
}
49274935
}
4928-
return true;
4929-
}
4930-
4931-
function createEnumLiteralType(symbol: Symbol, value: number) {
4932-
const type = createLiteralType(TypeFlags.NumberLiteral | TypeFlags.EnumLiteral, value);
4933-
type.symbol = symbol;
4934-
return type;
4936+
return links.enumKind = hasNonLiteralMember ? EnumKind.Numeric : EnumKind.Literal;
49354937
}
49364938

49374939
function getBaseTypeOfEnumLiteralType(type: Type) {
@@ -4943,17 +4945,14 @@ namespace ts {
49434945
if (links.declaredType) {
49444946
return links.declaredType;
49454947
}
4946-
if (enumHasLiteralMembers(symbol)) {
4948+
if (getEnumKind(symbol) === EnumKind.Literal) {
4949+
enumCount++;
49474950
const memberTypeList: Type[] = [];
4948-
const memberTypes: LiteralType[] = [];
49494951
for (const declaration of symbol.declarations) {
49504952
if (declaration.kind === SyntaxKind.EnumDeclaration) {
4951-
computeEnumMemberValues(<EnumDeclaration>declaration);
49524953
for (const member of (<EnumDeclaration>declaration).members) {
4953-
const memberSymbol = getSymbolOfNode(member);
4954-
const value = getEnumMemberValue(member);
4955-
if (!memberTypes[value]) {
4956-
const memberType = memberTypes[value] = createEnumLiteralType(memberSymbol, value);
4954+
const memberType = getLiteralType(getEnumMemberValue(member), enumCount, getSymbolOfNode(member));
4955+
if (!contains(memberTypeList, memberType)) {
49574956
memberTypeList.push(memberType);
49584957
}
49594958
}
@@ -4963,7 +4962,7 @@ namespace ts {
49634962
for (const declaration of symbol.declarations) {
49644963
if (declaration.kind === SyntaxKind.EnumDeclaration) {
49654964
for (const member of (<EnumDeclaration>declaration).members) {
4966-
getSymbolLinks(getSymbolOfNode(member)).declaredType = memberTypes[getEnumMemberValue(member)];
4965+
getSymbolLinks(getSymbolOfNode(member)).declaredType = getLiteralType(getEnumMemberValue(member), enumCount, getSymbolOfNode(member));
49674966
}
49684967
}
49694968
}
@@ -7517,16 +7516,17 @@ namespace ts {
75177516
return prop.flags & SymbolFlags.Method && find(prop.declarations, decl => isClassLike(decl.parent));
75187517
}
75197518

7520-
function createLiteralType(flags: TypeFlags, value: string | number) {
7519+
function createLiteralType(flags: TypeFlags, value: string | number, symbol: Symbol) {
75217520
const type = <LiteralType>createType(flags);
7521+
type.symbol = symbol;
75227522
type.value = value;
75237523
return type;
75247524
}
75257525

75267526
function getFreshTypeOfLiteralType(type: Type) {
75277527
if (type.flags & TypeFlags.StringOrNumberLiteral && !(type.flags & TypeFlags.FreshLiteral)) {
75287528
if (!(<LiteralType>type).freshType) {
7529-
const freshType = <LiteralType>createLiteralType(type.flags | TypeFlags.FreshLiteral, (<LiteralType>type).value);
7529+
const freshType = <LiteralType>createLiteralType(type.flags | TypeFlags.FreshLiteral, (<LiteralType>type).value, (<LiteralType>type).symbol);
75307530
freshType.regularType = <LiteralType>type;
75317531
(<LiteralType>type).freshType = freshType;
75327532
}
@@ -7539,12 +7539,17 @@ namespace ts {
75397539
return type.flags & TypeFlags.StringOrNumberLiteral && type.flags & TypeFlags.FreshLiteral ? (<LiteralType>type).regularType : type;
75407540
}
75417541

7542-
function getLiteralType(value: string | number) {
7543-
const map = typeof value === "number" ? numericLiteralTypes : stringLiteralTypes;
7544-
const text = "" + value;
7545-
let type = map.get(text);
7542+
function getLiteralType(value: string | number, enumId?: number, symbol?: Symbol) {
7543+
// We store all literal types in a single map with keys of the form '#NNN' and '@SSS',
7544+
// where NNN is the text representation of a numeric literal and SSS are the characters
7545+
// of a string literal. For literal enum members we use 'EEE#NNN' and 'EEE@SSS', where
7546+
// EEE is a unique id for the containing enum type.
7547+
const qualifier = typeof value === "number" ? "#" : "@";
7548+
const key = enumId ? enumId + qualifier + value : qualifier + value;
7549+
let type = literalTypes.get(key);
75467550
if (!type) {
7547-
map.set(text, type = createLiteralType(typeof value === "number" ? TypeFlags.NumberLiteral : TypeFlags.StringLiteral, value));
7551+
const flags = (typeof value === "number" ? TypeFlags.NumberLiteral : TypeFlags.StringLiteral) | (enumId ? TypeFlags.EnumLiteral : 0);
7552+
literalTypes.set(key, type = createLiteralType(flags, value, symbol));
75487553
}
75497554
return type;
75507555
}
@@ -20716,17 +20721,22 @@ namespace ts {
2071620721
return undefined;
2071720722
}
2071820723

20719-
function computeConstantValue(member: EnumMember): number {
20724+
function computeConstantValue(member: EnumMember): string | number {
20725+
const enumKind = getEnumKind(getSymbolOfNode(member.parent));
2072020726
const isConstEnum = isConst(member.parent);
2072120727
const initializer = member.initializer;
20722-
const value = evaluate(member.initializer);
20728+
const value = enumKind === EnumKind.Literal && !isLiteralEnumMember(member) ? undefined : evaluate(initializer);
2072320729
if (value !== undefined) {
20724-
if (isConstEnum && !isFinite(value)) {
20730+
if (isConstEnum && typeof value === "number" && !isFinite(value)) {
2072520731
error(initializer, isNaN(value) ?
2072620732
Diagnostics.const_enum_member_initializer_was_evaluated_to_disallowed_value_NaN :
2072720733
Diagnostics.const_enum_member_initializer_was_evaluated_to_a_non_finite_value);
2072820734
}
2072920735
}
20736+
else if (enumKind === EnumKind.Literal) {
20737+
error(initializer, Diagnostics.Computed_values_are_not_permitted_in_an_enum_with_string_valued_members);
20738+
return 0;
20739+
}
2073020740
else if (isConstEnum) {
2073120741
error(initializer, Diagnostics.In_const_enum_declarations_member_initializer_must_be_constant_expression);
2073220742
}
@@ -20739,7 +20749,7 @@ namespace ts {
2073920749
}
2074020750
return value;
2074120751

20742-
function evaluate(expr: Expression): number {
20752+
function evaluate(expr: Expression): string | number {
2074320753
switch (expr.kind) {
2074420754
case SyntaxKind.PrefixUnaryExpression:
2074520755
const value = evaluate((<PrefixUnaryExpression>expr).operand);
@@ -20770,13 +20780,15 @@ namespace ts {
2077020780
}
2077120781
}
2077220782
break;
20783+
case SyntaxKind.StringLiteral:
20784+
return (<StringLiteral>expr).text;
2077320785
case SyntaxKind.NumericLiteral:
2077420786
checkGrammarNumericLiteral(<NumericLiteral>expr);
2077520787
return +(<NumericLiteral>expr).text;
2077620788
case SyntaxKind.ParenthesizedExpression:
2077720789
return evaluate((<ParenthesizedExpression>expr).expression);
2077820790
case SyntaxKind.Identifier:
20779-
return evaluateEnumMember(expr, getSymbolOfNode(member.parent), (<Identifier>expr).text);
20791+
return nodeIsMissing(expr) ? 0 : evaluateEnumMember(expr, getSymbolOfNode(member.parent), (<Identifier>expr).text);
2078020792
case SyntaxKind.ElementAccessExpression:
2078120793
case SyntaxKind.PropertyAccessExpression:
2078220794
if (isConstantMemberAccess(expr)) {
@@ -22476,7 +22488,7 @@ namespace ts {
2247622488
return getNodeLinks(node).flags;
2247722489
}
2247822490

22479-
function getEnumMemberValue(node: EnumMember): number {
22491+
function getEnumMemberValue(node: EnumMember): string | number {
2248022492
computeEnumMemberValues(<EnumDeclaration>node.parent);
2248122493
return getNodeLinks(node).enumMemberValue;
2248222494
}
@@ -22491,7 +22503,7 @@ namespace ts {
2249122503
return false;
2249222504
}
2249322505

22494-
function getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): number {
22506+
function getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number {
2249522507
if (node.kind === SyntaxKind.EnumMember) {
2249622508
return getEnumMemberValue(<EnumMember>node);
2249722509
}

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1843,6 +1843,10 @@
18431843
"category": "Error",
18441844
"code": 2550
18451845
},
1846+
"Computed values are not permitted in an enum with string valued members.": {
1847+
"category": "Error",
1848+
"code": 2551
1849+
},
18461850
"JSX element attributes type '{0}' may not be a union type.": {
18471851
"category": "Error",
18481852
"code": 2600

src/compiler/emitter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,7 @@ namespace ts {
11281128
// check if constant enum value is integer
11291129
const constantValue = getConstantValue(expression);
11301130
// isFinite handles cases when constantValue is undefined
1131-
return isFinite(constantValue)
1131+
return typeof constantValue === "number" && isFinite(constantValue)
11321132
&& Math.floor(constantValue) === constantValue
11331133
&& printerOptions.removeComments;
11341134
}

src/compiler/factory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2248,7 +2248,7 @@ namespace ts {
22482248
/**
22492249
* Sets the constant value to emit for an expression.
22502250
*/
2251-
export function setConstantValue(node: PropertyAccessExpression | ElementAccessExpression, value: number) {
2251+
export function setConstantValue(node: PropertyAccessExpression | ElementAccessExpression, value: string | number) {
22522252
const emitNode = getOrCreateEmitNode(node);
22532253
emitNode.constantValue = value;
22542254
return node;

src/compiler/transformers/ts.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2498,22 +2498,27 @@ namespace ts {
24982498
// we pass false as 'generateNameForComputedPropertyName' for a backward compatibility purposes
24992499
// old emitter always generate 'expression' part of the name as-is.
25002500
const name = getExpressionForPropertyName(member, /*generateNameForComputedPropertyName*/ false);
2501+
const valueExpression = transformEnumMemberDeclarationValue(member);
2502+
const innerAssignment = createAssignment(
2503+
createElementAccess(
2504+
currentNamespaceContainerName,
2505+
name
2506+
),
2507+
valueExpression
2508+
);
2509+
const outerAssignment = valueExpression.kind === SyntaxKind.StringLiteral ?
2510+
innerAssignment :
2511+
createAssignment(
2512+
createElementAccess(
2513+
currentNamespaceContainerName,
2514+
innerAssignment
2515+
),
2516+
name
2517+
);
25012518
return setTextRange(
25022519
createStatement(
25032520
setTextRange(
2504-
createAssignment(
2505-
createElementAccess(
2506-
currentNamespaceContainerName,
2507-
createAssignment(
2508-
createElementAccess(
2509-
currentNamespaceContainerName,
2510-
name
2511-
),
2512-
transformEnumMemberDeclarationValue(member)
2513-
)
2514-
),
2515-
name
2516-
),
2521+
outerAssignment,
25172522
member
25182523
)
25192524
),
@@ -3351,7 +3356,7 @@ namespace ts {
33513356
return node;
33523357
}
33533358

3354-
function tryGetConstEnumValue(node: Node): number {
3359+
function tryGetConstEnumValue(node: Node): string | number {
33553360
if (compilerOptions.isolatedModules) {
33563361
return undefined;
33573362
}

src/compiler/types.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2528,7 +2528,7 @@ namespace ts {
25282528
isUnknownSymbol(symbol: Symbol): boolean;
25292529
/* @internal */ getMergedSymbol(symbol: Symbol): Symbol;
25302530

2531-
getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): number;
2531+
getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number;
25322532
isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName, propertyName: string): boolean;
25332533
/** Follow all aliases to get the original symbol. */
25342534
getAliasedSymbol(symbol: Symbol): Symbol;
@@ -2731,7 +2731,7 @@ namespace ts {
27312731
isSymbolAccessible(symbol: Symbol, enclosingDeclaration: Node, meaning: SymbolFlags, shouldComputeAliasToMarkVisible: boolean): SymbolAccessibilityResult;
27322732
isEntityNameVisible(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node): SymbolVisibilityResult;
27332733
// Returns the constant value this property access resolves to, or 'undefined' for a non-constant
2734-
getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): number;
2734+
getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number;
27352735
getReferencedValueDeclaration(reference: Identifier): Declaration;
27362736
getTypeReferenceSerializationKind(typeName: EntityName, location?: Node): TypeReferenceSerializationKind;
27372737
isOptionalParameter(node: ParameterDeclaration): boolean;
@@ -2869,6 +2869,13 @@ namespace ts {
28692869
isDeclarationWithCollidingName?: boolean; // True if symbol is block scoped redeclaration
28702870
bindingElement?: BindingElement; // Binding element associated with property symbol
28712871
exportsSomeValue?: boolean; // True if module exports some value (not just types)
2872+
enumKind?: EnumKind; // Enum declaration classification
2873+
}
2874+
2875+
/* @internal */
2876+
export const enum EnumKind {
2877+
Numeric, // Numeric enum (each member has a TypeFlags.Enum type)
2878+
Literal // Literal enum (each member has a TypeFlags.EnumLiteral type)
28722879
}
28732880

28742881
/* @internal */
@@ -2941,7 +2948,7 @@ namespace ts {
29412948
resolvedSymbol?: Symbol; // Cached name resolution result
29422949
resolvedIndexInfo?: IndexInfo; // Cached indexing info resolution result
29432950
maybeTypePredicate?: boolean; // Cached check whether call expression might reference a type predicate
2944-
enumMemberValue?: number; // Constant value of enum member
2951+
enumMemberValue?: string | number; // Constant value of enum member
29452952
isVisible?: boolean; // Is this node visible
29462953
containsArgumentsReference?: boolean; // Whether a function-like declaration contains an 'arguments' reference
29472954
hasReportedStatementInAmbientContext?: boolean; // Cache boolean if we report statements in ambient context
@@ -3918,7 +3925,7 @@ namespace ts {
39183925
commentRange?: TextRange; // The text range to use when emitting leading or trailing comments
39193926
sourceMapRange?: TextRange; // The text range to use when emitting leading or trailing source mappings
39203927
tokenSourceMapRanges?: TextRange[]; // The text range to use when emitting source mappings for tokens
3921-
constantValue?: number; // The constant value of an expression
3928+
constantValue?: string | number; // The constant value of an expression
39223929
externalHelpersModuleName?: Identifier; // The local name for an imported helpers module
39233930
helpers?: EmitHelper[]; // Emit helpers for the node
39243931
}

src/compiler/utilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ namespace ts {
349349
Debug.fail(`Literal kind '${node.kind}' not accounted for.`);
350350
}
351351

352-
function getQuotedEscapedLiteralText(leftQuote: string, text: string, rightQuote: string) {
352+
export function getQuotedEscapedLiteralText(leftQuote: string, text: string, rightQuote: string) {
353353
return leftQuote + escapeNonAsciiCharacters(escapeString(text)) + rightQuote;
354354
}
355355

0 commit comments

Comments
 (0)