diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index e1c185360d3..689d3e44a7c 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -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(); const intersectionTypes = createMap(); - const stringLiteralTypes = createMap(); - const numericLiteralTypes = createMap(); + const literalTypes = createMap(); const indexedAccessTypes = createMap(); 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 && (expr).operator === SyntaxKind.MinusToken && (expr).operand.kind === SyntaxKind.NumericLiteral || - expr.kind === SyntaxKind.Identifier && !!symbol.exports.get((expr).text); + expr.kind === SyntaxKind.Identifier && (nodeIsMissing(expr) || !!getSymbolOfNode(member.parent).exports.get((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 (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(declaration); for (const member of (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 (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 = 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 (!(type).freshType) { - const freshType = createLiteralType(type.flags | TypeFlags.FreshLiteral, (type).value); + const freshType = createLiteralType(type.flags | TypeFlags.FreshLiteral, (type).value, (type).symbol); freshType.regularType = type; (type).freshType = freshType; } @@ -7539,12 +7539,17 @@ namespace ts { return type.flags & TypeFlags.StringOrNumberLiteral && type.flags & TypeFlags.FreshLiteral ? (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((expr).operand); @@ -20770,13 +20780,15 @@ namespace ts { } } break; + case SyntaxKind.StringLiteral: + return (expr).text; case SyntaxKind.NumericLiteral: checkGrammarNumericLiteral(expr); return +(expr).text; case SyntaxKind.ParenthesizedExpression: return evaluate((expr).expression); case SyntaxKind.Identifier: - return evaluateEnumMember(expr, getSymbolOfNode(member.parent), (expr).text); + return nodeIsMissing(expr) ? 0 : evaluateEnumMember(expr, getSymbolOfNode(member.parent), (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(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(node); } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index b69adf708f2..43c1c6334cd 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -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 diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 669c9f5dd36..0f00f664e0d 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -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; } diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 5c7486debe3..074e6489a73 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -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; diff --git a/src/compiler/transformers/ts.ts b/src/compiler/transformers/ts.ts index 780e519bb53..5b0623094c3 100644 --- a/src/compiler/transformers/ts.ts +++ b/src/compiler/transformers/ts.ts @@ -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; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index f17dec4bd12..da69d64e04d 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -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 } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 646a7b22e3f..8080f6985e8 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -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; } diff --git a/src/services/symbolDisplay.ts b/src/services/symbolDisplay.ts index 41bdf4d8ada..8f61ca9c914 100644 --- a/src/services/symbolDisplay.ts +++ b/src/services/symbolDisplay.ts @@ -330,7 +330,10 @@ namespace ts.SymbolDisplay { displayParts.push(spacePart()); displayParts.push(operatorPart(SyntaxKind.EqualsToken)); displayParts.push(spacePart()); - displayParts.push(displayPart(constantValue.toString(), SymbolDisplayPartKind.numericLiteral)); + const valuePart = typeof constantValue === "number" ? + displayPart("" + constantValue, SymbolDisplayPartKind.numericLiteral) : + displayPart(getQuotedEscapedLiteralText('"', constantValue, '"'), SymbolDisplayPartKind.stringLiteral); + displayParts.push(valuePart); } } }