Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement erasable Enum Annotations #61414

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
ElementAccessExpression,
EntityNameExpression,
EnumDeclaration,
EnumLiteralExpression,
escapeLeadingUnderscores,
every,
ExportAssignment,
Expand Down Expand Up @@ -155,6 +156,7 @@ import {
isEmptyObjectLiteral,
isEntityNameExpression,
isEnumConst,
isEnumLiteralExpression,
isExportAssignment,
isExportDeclaration,
isExportsIdentifier,
Expand Down Expand Up @@ -822,6 +824,9 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
if (isNamedDeclaration(node)) {
setParent(node.name, node);
}
if (isEnumLiteralExpression(node) && symbol.valueDeclaration === node.parent) {
// This is not a real redeclaration, but is in fact two separate meanings for the same symbol.
}
// Report errors every position with duplicate declaration
// Report errors on previous encountered declarations
let message = symbol.flags & SymbolFlags.BlockScopedVariable
Expand Down Expand Up @@ -2272,6 +2277,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
case SyntaxKind.ClassDeclaration:
return declareClassMember(node, symbolFlags, symbolExcludes);

case SyntaxKind.EnumLiteralExpression:
case SyntaxKind.EnumDeclaration:
return declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes);

Expand Down Expand Up @@ -3044,6 +3050,8 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
return bindBlockScopedDeclaration(node as Declaration, SymbolFlags.Interface, SymbolFlags.InterfaceExcludes);
case SyntaxKind.TypeAliasDeclaration:
return bindBlockScopedDeclaration(node as Declaration, SymbolFlags.TypeAlias, SymbolFlags.TypeAliasExcludes);
case SyntaxKind.EnumLiteralExpression:
return bindEnumLiteralExpression(node as EnumLiteralExpression);
case SyntaxKind.EnumDeclaration:
return bindEnumDeclaration(node as EnumDeclaration);
case SyntaxKind.ModuleDeclaration:
Expand Down Expand Up @@ -3655,6 +3663,17 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
: bindBlockScopedDeclaration(node, SymbolFlags.RegularEnum, SymbolFlags.RegularEnumExcludes);
}

function bindEnumLiteralExpression(node: EnumLiteralExpression) {
Debug.assert(isVariableDeclaration(node.parent));

const varSymbol: Symbol = node.parent.symbol;
node.symbol = varSymbol;
varSymbol.flags |= isEnumConst(node) ? SymbolFlags.ConstEnum : SymbolFlags.RegularEnum;
varSymbol.flags &= ~(SymbolFlags.Assignment | SymbolFlags.Variable);
varSymbol.exports ??= createSymbolTable();
appendIfUnique(varSymbol.declarations, node);
}

function bindVariableDeclarationOrBindingElement(node: VariableDeclaration | BindingElement) {
if (inStrictMode) {
checkStrictModeEvalOrArguments(node, node.name);
Expand Down Expand Up @@ -3914,6 +3933,7 @@ export function getContainerFlags(node: Node): ContainerFlags {
case SyntaxKind.ClassExpression:
case SyntaxKind.ClassDeclaration:
case SyntaxKind.EnumDeclaration:
case SyntaxKind.EnumLiteralExpression:
case SyntaxKind.ObjectLiteralExpression:
case SyntaxKind.TypeLiteral:
case SyntaxKind.JSDocTypeLiteral:
Expand Down
46 changes: 38 additions & 8 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ import {
EntityNameOrEntityNameExpression,
entityNameToString,
EnumDeclaration,
EnumLiteralExpression,
EnumMember,
EnumType,
equateValues,
Expand Down Expand Up @@ -536,7 +537,11 @@ import {
isEntityNameExpression,
isEnumConst,
isEnumDeclaration,
isEnumLiteralDeclaration,
isEnumLiteralExpression,
isEnumMember,
isEnumTypeAnnotation,
isEnumTypeReference,
isExclusivelyTypeOnlyImportOrExport,
isExpandoPropertyDeclaration,
isExportAssignment,
Expand Down Expand Up @@ -11984,7 +11989,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
// getTypeOfSymbol dispatches some JS merges incorrectly because their symbol flags are not mutually exclusive.
// Re-dispatch based on valueDeclaration.kind instead.
else if (isEnumDeclaration(declaration)) {
else if (isEnumDeclaration(declaration) || isEnumLiteralExpression(declaration)) {
type = getTypeOfFuncClassEnumModule(symbol);
}
else if (isEnumMember(declaration)) {
Expand Down Expand Up @@ -12876,7 +12881,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const memberTypeList: Type[] = [];
if (symbol.declarations) {
for (const declaration of symbol.declarations) {
if (declaration.kind === SyntaxKind.EnumDeclaration) {
if (declaration.kind === SyntaxKind.EnumDeclaration || declaration.kind === SyntaxKind.EnumLiteralExpression) {
for (const member of (declaration as EnumDeclaration).members) {
if (hasBindableName(member)) {
const memberSymbol = getSymbolOfDeclaration(member);
Expand Down Expand Up @@ -16706,6 +16711,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
links.resolvedSymbol = unknownSymbol;
return links.resolvedType = checkExpressionCached(node.parent.expression);
}
// `var MyEnum: enum = { FirstValue: 1, SecondValue: 2 }` should resolve to a union of the enum values.
if (isEnumTypeReference(node) && isVariableDeclaration(node.parent) && node.parent.initializer && isEnumLiteralExpression(node.parent.initializer)) {
return links.resolvedType = checkExpressionCached(node.parent.initializer);
}
let symbol: Symbol | undefined;
let type: Type | undefined;
const meaning = SymbolFlags.Type;
Expand Down Expand Up @@ -16760,6 +16769,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
case SyntaxKind.ClassDeclaration:
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.EnumDeclaration:
case SyntaxKind.EnumLiteralExpression:
return declaration;
}
}
Expand Down Expand Up @@ -41109,6 +41119,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return checkArrayLiteral(node as ArrayLiteralExpression, checkMode, forceTuple);
case SyntaxKind.ObjectLiteralExpression:
return checkObjectLiteral(node as ObjectLiteralExpression, checkMode);
case SyntaxKind.EnumLiteralExpression:
return checkEnumLiteralExpression(node as EnumLiteralExpression);
case SyntaxKind.PropertyAccessExpression:
return checkPropertyAccessExpression(node as PropertyAccessExpression, checkMode);
case SyntaxKind.QualifiedName:
Expand Down Expand Up @@ -44128,7 +44140,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
checkClassNameCollisionWithObject(name);
}
}
else if (isEnumDeclaration(node)) {
else if (isEnumDeclaration(node) || (isVariableDeclaration(node) && node.initializer && isEnumLiteralExpression(node.initializer))) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here too

checkTypeNameIsReserved(name, Diagnostics.Enum_name_cannot_be_0);
}
}
Expand Down Expand Up @@ -47033,16 +47045,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

function computeEnumMemberValues(node: EnumDeclaration) {
function computeEnumMemberValues(node: EnumDeclaration | EnumLiteralExpression) {
const nodeLinks = getNodeLinks(node);
if (!(nodeLinks.flags & NodeCheckFlags.EnumValuesComputed)) {
nodeLinks.flags |= NodeCheckFlags.EnumValuesComputed;
let autoValue: number | undefined = 0;
// EnumLiteralExpressions are essentially plain ObjectLiteralExpressions and can not have computed values.
const hasComputedValues = !isEnumLiteralExpression(node);
let autoValue: number | undefined = hasComputedValues ? 0 : undefined;
let previous: EnumMember | undefined;
for (const member of node.members) {
const result = computeEnumMemberValue(member, autoValue, previous);
getNodeLinks(member).enumMemberValue = result;
autoValue = typeof result.value === "number" ? result.value + 1 : undefined;
autoValue = (hasComputedValues && typeof result.value === "number") ? result.value + 1 : undefined;
previous = member;
}
}
Expand Down Expand Up @@ -47090,6 +47104,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const isConstEnum = isEnumConst(member.parent);
const initializer = member.initializer!;
const result = evaluate(initializer, member);
const isDecl = isEnumDeclaration(member.parent);
if (result.value !== undefined) {
if (isConstEnum && typeof result.value === "number" && !isFinite(result.value)) {
error(
Expand All @@ -47103,7 +47118,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
error(
initializer,
Diagnostics._0_has_a_string_type_but_must_have_syntactically_recognizable_string_syntax_when_isolatedModules_is_enabled,
`${idText(member.parent.name)}.${getTextOfPropertyName(member.name)}`,
isDecl ? `${idText(member.parent.name)}.${getTextOfPropertyName(member.name)}` : `${member.parent.name}.${getTextOfPropertyName(member.name)}`,
);
}
}
Expand Down Expand Up @@ -47191,6 +47206,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
addLazyDiagnostic(() => checkEnumDeclarationWorker(node));
}

function checkEnumLiteralExpression(node: EnumLiteralExpression) {
addLazyDiagnostic(() => checkEnumDeclarationWorker(node as any));
return getTypeOfSymbol(getSymbolOfDeclaration(node));
}

function checkEnumDeclarationWorker(node: EnumDeclaration) {
// Grammar checking
checkGrammarModifiers(node);
Expand Down Expand Up @@ -47218,7 +47238,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const enumIsConst = isEnumConst(node);
// check that const is placed\omitted on all enum declarations
forEach(enumSymbol.declarations, decl => {
if (isEnumDeclaration(decl) && isEnumConst(decl) !== enumIsConst) {
if ((isEnumDeclaration(decl) || isEnumLiteralExpression(decl)) && isEnumConst(decl) !== enumIsConst) {
error(getNameOfDeclaration(decl), Diagnostics.Enum_declarations_must_all_be_const_or_non_const);
}
});
Expand Down Expand Up @@ -48854,6 +48874,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
case SyntaxKind.ModuleDeclaration:
copyLocallyVisibleExportSymbols(getSymbolOfDeclaration(location as ModuleDeclaration | SourceFile).exports!, meaning & SymbolFlags.ModuleMember);
break;
case SyntaxKind.EnumLiteralExpression:
case SyntaxKind.EnumDeclaration:
copySymbols(getSymbolOfDeclaration(location as EnumDeclaration).exports!, meaning & SymbolFlags.EnumMember);
break;
Expand Down Expand Up @@ -49304,6 +49325,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// no other meta properties are valid syntax, thus no others should have symbols
return undefined;
}
else if (isEnumTypeAnnotation(node)) {
// Avoid symbolizing "enum" keywords in type annotations.
return undefined;
}
}

switch (node.kind) {
Expand Down Expand Up @@ -49490,6 +49515,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (isDeclarationNameOrImportPropertyName(node)) {
const symbol = getSymbolAtLocation(node);
if (symbol) {
if (symbol.valueDeclaration && isEnumLiteralDeclaration(symbol.valueDeclaration)) {
return getDeclaredTypeOfEnum(symbol);
}
return getTypeOfSymbol(symbol);
}
return errorType;
Expand Down Expand Up @@ -50352,6 +50380,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
case SyntaxKind.PropertyAssignment:
case SyntaxKind.ShorthandPropertyAssignment:
case SyntaxKind.EnumMember:
case SyntaxKind.EnumLiteralExpression:
case SyntaxKind.ObjectLiteralExpression:
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.FunctionExpression:
Expand Down Expand Up @@ -51329,6 +51358,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
findFirstModifierExcept(node, SyntaxKind.AwaitKeyword) :
find(node.modifiers, isModifier);
case SyntaxKind.EnumDeclaration:
case SyntaxKind.EnumLiteralExpression:
return findFirstModifierExcept(node, SyntaxKind.ConstKeyword);
default:
Debug.assertNever(node);
Expand Down
10 changes: 10 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import {
ensureTrailingDirectorySeparator,
EntityName,
EnumDeclaration,
EnumLiteralExpression,
EnumMember,
escapeJsxAttributeString,
escapeLeadingUnderscores,
Expand Down Expand Up @@ -1924,6 +1925,8 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
return emitArrayLiteralExpression(node as ArrayLiteralExpression);
case SyntaxKind.ObjectLiteralExpression:
return emitObjectLiteralExpression(node as ObjectLiteralExpression);
case SyntaxKind.EnumLiteralExpression:
return emitEnumLiteralExpression(node as EnumLiteralExpression);
case SyntaxKind.PropertyAccessExpression:
return emitPropertyAccessExpression(node as PropertyAccessExpression);
case SyntaxKind.ElementAccessExpression:
Expand Down Expand Up @@ -2617,6 +2620,13 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
popNameGenerationScope(node);
}

function emitEnumLiteralExpression(node: EnumLiteralExpression) {
writeSpace();
writePunctuation("{");
emitList(node, node.members, ListFormat.EnumMembers);
writePunctuation("}");
}

function emitPropertyAccessExpression(node: PropertyAccessExpression) {
emitExpression(node.expression, parenthesizer.parenthesizeLeftSideOfAccess);
const token = node.questionDotToken || setTextRangePosEnd(factory.createToken(SyntaxKind.DotToken) as DotToken, node.expression.end, node.name.pos);
Expand Down
38 changes: 38 additions & 0 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import {
EndOfFileToken,
EntityName,
EnumDeclaration,
EnumLiteralExpression,
EnumMember,
EqualsGreaterThanToken,
escapeLeadingUnderscores,
Expand Down Expand Up @@ -160,6 +161,7 @@ import {
isElementAccessChain,
isElementAccessExpression,
isEnumDeclaration,
isEnumLiteralExpression,
isExclamationToken,
isExportAssignment,
isExportDeclaration,
Expand Down Expand Up @@ -754,6 +756,8 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
updateTypeAliasDeclaration,
createEnumDeclaration,
updateEnumDeclaration,
createEnumLiteralExpression,
updateEnumLiteralExpression,
createModuleDeclaration,
updateModuleDeclaration,
createModuleBlock,
Expand Down Expand Up @@ -4539,6 +4543,39 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
: node;
}

// @api
function createEnumLiteralExpression(
modifiers: readonly ModifierLike[] | undefined,
name: __String,
members: readonly EnumMember[],
) {
const node = createBaseDeclaration<EnumLiteralExpression>(SyntaxKind.EnumLiteralExpression);
node.modifiers = asNodeArray(modifiers);
node.name = name;
node.members = createNodeArray(members);
node.transformFlags |= propagateChildrenFlags(node.modifiers) |
propagateChildrenFlags(node.members) |
TransformFlags.ContainsTypeScript;
node.transformFlags &= ~TransformFlags.ContainsPossibleTopLevelAwait; // Enum declarations cannot contain `await`

node.jsDoc = undefined; // initialized by parser (JsDocContainer)
return node;
}

// @api
function updateEnumLiteralExpression(
node: EnumLiteralExpression,
modifiers: readonly ModifierLike[] | undefined,
name: __String,
members: readonly EnumMember[],
) {
return node.modifiers !== modifiers
|| node.name !== name
|| node.members !== members
? update(createEnumLiteralExpression(modifiers, name, members), node)
: node;
}

// @api
function createModuleDeclaration(
modifiers: readonly ModifierLike[] | undefined,
Expand Down Expand Up @@ -7086,6 +7123,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
isInterfaceDeclaration(node) ? updateInterfaceDeclaration(node, modifierArray, node.name, node.typeParameters, node.heritageClauses, node.members) :
isTypeAliasDeclaration(node) ? updateTypeAliasDeclaration(node, modifierArray, node.name, node.typeParameters, node.type) :
isEnumDeclaration(node) ? updateEnumDeclaration(node, modifierArray, node.name, node.members) :
isEnumLiteralExpression(node) ? updateEnumLiteralExpression(node, modifierArray, node.name, node.members) :
isModuleDeclaration(node) ? updateModuleDeclaration(node, modifierArray, node.name, node.body) :
isImportEqualsDeclaration(node) ? updateImportEqualsDeclaration(node, modifierArray, node.isTypeOnly, node.name, node.moduleReference) :
isImportDeclaration(node) ? updateImportDeclaration(node, modifierArray, node.importClause, node.moduleSpecifier, node.attributes) :
Expand Down
5 changes: 5 additions & 0 deletions src/compiler/factory/nodeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
ElementAccessExpression,
EmptyStatement,
EnumDeclaration,
EnumLiteralExpression,
EnumMember,
EqualsGreaterThanToken,
ExclamationToken,
Expand Down Expand Up @@ -817,6 +818,10 @@ export function isEnumDeclaration(node: Node): node is EnumDeclaration {
return node.kind === SyntaxKind.EnumDeclaration;
}

export function isEnumLiteralExpression(node: Node): node is EnumLiteralExpression {
return node.kind === SyntaxKind.EnumLiteralExpression;
}

export function isModuleDeclaration(node: Node): node is ModuleDeclaration {
return node.kind === SyntaxKind.ModuleDeclaration;
}
Expand Down
Loading