Support string valued members in literal enums

This commit is contained in:
Anders Hejlsberg
2017-04-29 08:37:33 -07:00
parent d2fdebe246
commit 9e7cdb4df5
8 changed files with 90 additions and 59 deletions

View File

@@ -47,6 +47,7 @@ namespace ts {
let typeCount = 0;
let symbolCount = 0;
let enumCount = 0;
let symbolInstantiationDepth = 0;
const emptyArray: any[] = [];
@@ -210,8 +211,7 @@ namespace ts {
const tupleTypes: GenericType[] = [];
const unionTypes = createMap<UnionType>();
const intersectionTypes = createMap<IntersectionType>();
const stringLiteralTypes = createMap<LiteralType>();
const numericLiteralTypes = createMap<LiteralType>();
const literalTypes = createMap<LiteralType>();
const indexedAccessTypes = createMap<IndexedAccessType>();
const evolvingArrayTypes: EvolvingArrayType[] = [];
@@ -4904,34 +4904,36 @@ namespace ts {
return links.declaredType;
}
function isLiteralEnumMember(symbol: Symbol, member: EnumMember) {
function isLiteralEnumMember(member: EnumMember) {
const expr = member.initializer;
if (!expr) {
return !isInAmbientContext(member);
}
return expr.kind === SyntaxKind.NumericLiteral ||
return expr.kind === SyntaxKind.StringLiteral || expr.kind === SyntaxKind.NumericLiteral ||
expr.kind === SyntaxKind.PrefixUnaryExpression && (<PrefixUnaryExpression>expr).operator === SyntaxKind.MinusToken &&
(<PrefixUnaryExpression>expr).operand.kind === SyntaxKind.NumericLiteral ||
expr.kind === SyntaxKind.Identifier && !!symbol.exports.get((<Identifier>expr).text);
expr.kind === SyntaxKind.Identifier && (nodeIsMissing(expr) || !!getSymbolOfNode(member.parent).exports.get((<Identifier>expr).text));
}
function enumHasLiteralMembers(symbol: Symbol) {
function getEnumKind(symbol: Symbol): EnumKind {
const links = getSymbolLinks(symbol);
if (links.enumKind !== undefined) {
return links.enumKind;
}
let hasNonLiteralMember = false;
for (const declaration of symbol.declarations) {
if (declaration.kind === SyntaxKind.EnumDeclaration) {
for (const member of (<EnumDeclaration>declaration).members) {
if (!isLiteralEnumMember(symbol, member)) {
return false;
if (member.initializer && member.initializer.kind === SyntaxKind.StringLiteral) {
return links.enumKind = EnumKind.Literal;
}
if (!isLiteralEnumMember(member)) {
hasNonLiteralMember = true;
}
}
}
}
return true;
}
function createEnumLiteralType(symbol: Symbol, value: number) {
const type = createLiteralType(TypeFlags.NumberLiteral | TypeFlags.EnumLiteral, value);
type.symbol = symbol;
return type;
return links.enumKind = hasNonLiteralMember ? EnumKind.Numeric : EnumKind.Literal;
}
function getBaseTypeOfEnumLiteralType(type: Type) {
@@ -4943,17 +4945,14 @@ namespace ts {
if (links.declaredType) {
return links.declaredType;
}
if (enumHasLiteralMembers(symbol)) {
if (getEnumKind(symbol) === EnumKind.Literal) {
enumCount++;
const memberTypeList: Type[] = [];
const memberTypes: LiteralType[] = [];
for (const declaration of symbol.declarations) {
if (declaration.kind === SyntaxKind.EnumDeclaration) {
computeEnumMemberValues(<EnumDeclaration>declaration);
for (const member of (<EnumDeclaration>declaration).members) {
const memberSymbol = getSymbolOfNode(member);
const value = getEnumMemberValue(member);
if (!memberTypes[value]) {
const memberType = memberTypes[value] = createEnumLiteralType(memberSymbol, value);
const memberType = getLiteralType(getEnumMemberValue(member), enumCount, getSymbolOfNode(member));
if (!contains(memberTypeList, memberType)) {
memberTypeList.push(memberType);
}
}
@@ -4963,7 +4962,7 @@ namespace ts {
for (const declaration of symbol.declarations) {
if (declaration.kind === SyntaxKind.EnumDeclaration) {
for (const member of (<EnumDeclaration>declaration).members) {
getSymbolLinks(getSymbolOfNode(member)).declaredType = memberTypes[getEnumMemberValue(member)];
getSymbolLinks(getSymbolOfNode(member)).declaredType = getLiteralType(getEnumMemberValue(member), enumCount, getSymbolOfNode(member));
}
}
}
@@ -7517,8 +7516,9 @@ namespace ts {
return prop.flags & SymbolFlags.Method && find(prop.declarations, decl => isClassLike(decl.parent));
}
function createLiteralType(flags: TypeFlags, value: string | number) {
function createLiteralType(flags: TypeFlags, value: string | number, symbol: Symbol) {
const type = <LiteralType>createType(flags);
type.symbol = symbol;
type.value = value;
return type;
}
@@ -7526,7 +7526,7 @@ namespace ts {
function getFreshTypeOfLiteralType(type: Type) {
if (type.flags & TypeFlags.StringOrNumberLiteral && !(type.flags & TypeFlags.FreshLiteral)) {
if (!(<LiteralType>type).freshType) {
const freshType = <LiteralType>createLiteralType(type.flags | TypeFlags.FreshLiteral, (<LiteralType>type).value);
const freshType = <LiteralType>createLiteralType(type.flags | TypeFlags.FreshLiteral, (<LiteralType>type).value, (<LiteralType>type).symbol);
freshType.regularType = <LiteralType>type;
(<LiteralType>type).freshType = freshType;
}
@@ -7539,12 +7539,17 @@ namespace ts {
return type.flags & TypeFlags.StringOrNumberLiteral && type.flags & TypeFlags.FreshLiteral ? (<LiteralType>type).regularType : type;
}
function getLiteralType(value: string | number) {
const map = typeof value === "number" ? numericLiteralTypes : stringLiteralTypes;
const text = "" + value;
let type = map.get(text);
function getLiteralType(value: string | number, enumId?: number, symbol?: Symbol) {
// We store all literal types in a single map with keys of the form '#NNN' and '@SSS',
// where NNN is the text representation of a numeric literal and SSS are the characters
// of a string literal. For literal enum members we use 'EEE#NNN' and 'EEE@SSS', where
// EEE is a unique id for the containing enum type.
const qualifier = typeof value === "number" ? "#" : "@";
const key = enumId ? enumId + qualifier + value : qualifier + value;
let type = literalTypes.get(key);
if (!type) {
map.set(text, type = createLiteralType(typeof value === "number" ? TypeFlags.NumberLiteral : TypeFlags.StringLiteral, value));
const flags = (typeof value === "number" ? TypeFlags.NumberLiteral : TypeFlags.StringLiteral) | (enumId ? TypeFlags.EnumLiteral : 0);
literalTypes.set(key, type = createLiteralType(flags, value, symbol));
}
return type;
}
@@ -20716,17 +20721,22 @@ namespace ts {
return undefined;
}
function computeConstantValue(member: EnumMember): number {
function computeConstantValue(member: EnumMember): string | number {
const enumKind = getEnumKind(getSymbolOfNode(member.parent));
const isConstEnum = isConst(member.parent);
const initializer = member.initializer;
const value = evaluate(member.initializer);
const value = enumKind === EnumKind.Literal && !isLiteralEnumMember(member) ? undefined : evaluate(initializer);
if (value !== undefined) {
if (isConstEnum && !isFinite(value)) {
if (isConstEnum && typeof value === "number" && !isFinite(value)) {
error(initializer, isNaN(value) ?
Diagnostics.const_enum_member_initializer_was_evaluated_to_disallowed_value_NaN :
Diagnostics.const_enum_member_initializer_was_evaluated_to_a_non_finite_value);
}
}
else if (enumKind === EnumKind.Literal) {
error(initializer, Diagnostics.Computed_values_are_not_permitted_in_an_enum_with_string_valued_members);
return 0;
}
else if (isConstEnum) {
error(initializer, Diagnostics.In_const_enum_declarations_member_initializer_must_be_constant_expression);
}
@@ -20739,7 +20749,7 @@ namespace ts {
}
return value;
function evaluate(expr: Expression): number {
function evaluate(expr: Expression): string | number {
switch (expr.kind) {
case SyntaxKind.PrefixUnaryExpression:
const value = evaluate((<PrefixUnaryExpression>expr).operand);
@@ -20770,13 +20780,15 @@ namespace ts {
}
}
break;
case SyntaxKind.StringLiteral:
return (<StringLiteral>expr).text;
case SyntaxKind.NumericLiteral:
checkGrammarNumericLiteral(<NumericLiteral>expr);
return +(<NumericLiteral>expr).text;
case SyntaxKind.ParenthesizedExpression:
return evaluate((<ParenthesizedExpression>expr).expression);
case SyntaxKind.Identifier:
return evaluateEnumMember(expr, getSymbolOfNode(member.parent), (<Identifier>expr).text);
return nodeIsMissing(expr) ? 0 : evaluateEnumMember(expr, getSymbolOfNode(member.parent), (<Identifier>expr).text);
case SyntaxKind.ElementAccessExpression:
case SyntaxKind.PropertyAccessExpression:
if (isConstantMemberAccess(expr)) {
@@ -22476,7 +22488,7 @@ namespace ts {
return getNodeLinks(node).flags;
}
function getEnumMemberValue(node: EnumMember): number {
function getEnumMemberValue(node: EnumMember): string | number {
computeEnumMemberValues(<EnumDeclaration>node.parent);
return getNodeLinks(node).enumMemberValue;
}
@@ -22491,7 +22503,7 @@ namespace ts {
return false;
}
function getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): number {
function getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number {
if (node.kind === SyntaxKind.EnumMember) {
return getEnumMemberValue(<EnumMember>node);
}

View File

@@ -1843,6 +1843,10 @@
"category": "Error",
"code": 2550
},
"Computed values are not permitted in an enum with string valued members.": {
"category": "Error",
"code": 2551
},
"JSX element attributes type '{0}' may not be a union type.": {
"category": "Error",
"code": 2600

View File

@@ -1128,7 +1128,7 @@ namespace ts {
// check if constant enum value is integer
const constantValue = getConstantValue(expression);
// isFinite handles cases when constantValue is undefined
return isFinite(constantValue)
return typeof constantValue === "number" && isFinite(constantValue)
&& Math.floor(constantValue) === constantValue
&& printerOptions.removeComments;
}

View File

@@ -2248,7 +2248,7 @@ namespace ts {
/**
* Sets the constant value to emit for an expression.
*/
export function setConstantValue(node: PropertyAccessExpression | ElementAccessExpression, value: number) {
export function setConstantValue(node: PropertyAccessExpression | ElementAccessExpression, value: string | number) {
const emitNode = getOrCreateEmitNode(node);
emitNode.constantValue = value;
return node;

View File

@@ -2498,22 +2498,27 @@ namespace ts {
// we pass false as 'generateNameForComputedPropertyName' for a backward compatibility purposes
// old emitter always generate 'expression' part of the name as-is.
const name = getExpressionForPropertyName(member, /*generateNameForComputedPropertyName*/ false);
const valueExpression = transformEnumMemberDeclarationValue(member);
const innerAssignment = createAssignment(
createElementAccess(
currentNamespaceContainerName,
name
),
valueExpression
);
const outerAssignment = valueExpression.kind === SyntaxKind.StringLiteral ?
innerAssignment :
createAssignment(
createElementAccess(
currentNamespaceContainerName,
innerAssignment
),
name
);
return setTextRange(
createStatement(
setTextRange(
createAssignment(
createElementAccess(
currentNamespaceContainerName,
createAssignment(
createElementAccess(
currentNamespaceContainerName,
name
),
transformEnumMemberDeclarationValue(member)
)
),
name
),
outerAssignment,
member
)
),
@@ -3351,7 +3356,7 @@ namespace ts {
return node;
}
function tryGetConstEnumValue(node: Node): number {
function tryGetConstEnumValue(node: Node): string | number {
if (compilerOptions.isolatedModules) {
return undefined;
}

View File

@@ -2528,7 +2528,7 @@ namespace ts {
isUnknownSymbol(symbol: Symbol): boolean;
/* @internal */ getMergedSymbol(symbol: Symbol): Symbol;
getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): number;
getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number;
isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName, propertyName: string): boolean;
/** Follow all aliases to get the original symbol. */
getAliasedSymbol(symbol: Symbol): Symbol;
@@ -2731,7 +2731,7 @@ namespace ts {
isSymbolAccessible(symbol: Symbol, enclosingDeclaration: Node, meaning: SymbolFlags, shouldComputeAliasToMarkVisible: boolean): SymbolAccessibilityResult;
isEntityNameVisible(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node): SymbolVisibilityResult;
// Returns the constant value this property access resolves to, or 'undefined' for a non-constant
getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): number;
getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number;
getReferencedValueDeclaration(reference: Identifier): Declaration;
getTypeReferenceSerializationKind(typeName: EntityName, location?: Node): TypeReferenceSerializationKind;
isOptionalParameter(node: ParameterDeclaration): boolean;
@@ -2869,6 +2869,13 @@ namespace ts {
isDeclarationWithCollidingName?: boolean; // True if symbol is block scoped redeclaration
bindingElement?: BindingElement; // Binding element associated with property symbol
exportsSomeValue?: boolean; // True if module exports some value (not just types)
enumKind?: EnumKind; // Enum declaration classification
}
/* @internal */
export const enum EnumKind {
Numeric, // Numeric enum (each member has a TypeFlags.Enum type)
Literal // Literal enum (each member has a TypeFlags.EnumLiteral type)
}
/* @internal */
@@ -2941,7 +2948,7 @@ namespace ts {
resolvedSymbol?: Symbol; // Cached name resolution result
resolvedIndexInfo?: IndexInfo; // Cached indexing info resolution result
maybeTypePredicate?: boolean; // Cached check whether call expression might reference a type predicate
enumMemberValue?: number; // Constant value of enum member
enumMemberValue?: string | number; // Constant value of enum member
isVisible?: boolean; // Is this node visible
containsArgumentsReference?: boolean; // Whether a function-like declaration contains an 'arguments' reference
hasReportedStatementInAmbientContext?: boolean; // Cache boolean if we report statements in ambient context
@@ -3918,7 +3925,7 @@ namespace ts {
commentRange?: TextRange; // The text range to use when emitting leading or trailing comments
sourceMapRange?: TextRange; // The text range to use when emitting leading or trailing source mappings
tokenSourceMapRanges?: TextRange[]; // The text range to use when emitting source mappings for tokens
constantValue?: number; // The constant value of an expression
constantValue?: string | number; // The constant value of an expression
externalHelpersModuleName?: Identifier; // The local name for an imported helpers module
helpers?: EmitHelper[]; // Emit helpers for the node
}

View File

@@ -349,7 +349,7 @@ namespace ts {
Debug.fail(`Literal kind '${node.kind}' not accounted for.`);
}
function getQuotedEscapedLiteralText(leftQuote: string, text: string, rightQuote: string) {
export function getQuotedEscapedLiteralText(leftQuote: string, text: string, rightQuote: string) {
return leftQuote + escapeNonAsciiCharacters(escapeString(text)) + rightQuote;
}