Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
23 changes: 13 additions & 10 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,7 @@ import {
isJsxSpreadAttribute,
isJSXTagName,
isKnownSymbol,
isLateBindableAccessExpression,
isLateVisibilityPaintedStatement,
isLeftHandSideExpression,
isLineBreak,
Expand Down Expand Up @@ -6042,8 +6043,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

function getMeaningOfEntityNameReference(entityName: EntityNameOrEntityNameExpression): SymbolFlags {
// get symbol of the first identifier of the entityName
function getMeaningOfEntityNameReference(entityName: EntityNameOrEntityNameExpression | ElementAccessExpression): SymbolFlags {
// get symbol of the first identifier of the entityName or element access chain
let meaning: SymbolFlags;
if (
entityName.parent.kind === SyntaxKind.TypeQuery ||
Expand All @@ -6056,6 +6057,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
else if (
entityName.kind === SyntaxKind.QualifiedName || entityName.kind === SyntaxKind.PropertyAccessExpression ||
entityName.kind === SyntaxKind.ElementAccessExpression ||
entityName.parent.kind === SyntaxKind.ImportEqualsDeclaration ||
(entityName.parent.kind === SyntaxKind.QualifiedName && (entityName.parent as QualifiedName).left === entityName) ||
(entityName.parent.kind === SyntaxKind.PropertyAccessExpression && (entityName.parent as PropertyAccessExpression).expression === entityName) ||
Expand All @@ -6072,7 +6074,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return meaning;
}

function isEntityNameVisible(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node, shouldComputeAliasToMakeVisible = true): SymbolVisibilityResult {
function isEntityNameVisible(entityName: EntityNameOrEntityNameExpression | ElementAccessExpression, enclosingDeclaration: Node, shouldComputeAliasToMakeVisible = true): SymbolVisibilityResult {
const meaning = getMeaningOfEntityNameReference(entityName);
const firstIdentifier = getFirstIdentifier(entityName);
const symbol = resolveName(enclosingDeclaration, firstIdentifier.escapedText, meaning, /*nameNotFoundMessage*/ undefined, /*isUse*/ false);
Expand Down Expand Up @@ -8369,9 +8371,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

function trackComputedName(accessExpression: EntityNameOrEntityNameExpression, enclosingDeclaration: Node | undefined, context: NodeBuilderContext) {
function trackComputedName(accessExpression: EntityNameOrEntityNameExpression | ElementAccessExpression, enclosingDeclaration: Node | undefined, context: NodeBuilderContext) {
if (!context.tracker.canTrackSymbol) return;
// get symbol of the first identifier of the entityName
// get symbol of the first identifier of the entityName or element access chain
const firstIdentifier = getFirstIdentifier(accessExpression);
const name = resolveName(enclosingDeclaration, firstIdentifier.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue, /*nameNotFoundMessage*/ undefined, /*isUse*/ true);
if (name) {
Expand Down Expand Up @@ -13721,7 +13723,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return false;
}
const expr = isComputedPropertyName(node) ? node.expression : node.argumentExpression;
return isEntityNameExpression(expr);
return isLateBindableAccessExpression(expr);
}

function isTypeUsableAsIndexSignature(type: Type): boolean {
Expand Down Expand Up @@ -51444,9 +51446,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
return result;

function trackComputedName(accessExpression: EntityNameOrEntityNameExpression) {
function trackComputedName(accessExpression: EntityNameOrEntityNameExpression | ElementAccessExpression) {
if (!tracker.trackSymbol) return;
// get symbol of the first identifier of the entityName
// get symbol of the first identifier of the entityName or element access chain
const firstIdentifier = getFirstIdentifier(accessExpression);
const name = resolveName(firstIdentifier, firstIdentifier.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue, /*nameNotFoundMessage*/ undefined, /*isUse*/ true);
if (name) {
Expand Down Expand Up @@ -52910,8 +52912,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function checkGrammarForInvalidDynamicName(node: DeclarationName, message: DiagnosticMessage) {
// Even non-bindable names are allowed as late-bound implied index signatures so long as the name is a simple `a.b.c` type name expression
if (isNonBindableDynamicName(node) && !isEntityNameExpression(isElementAccessExpression(node) ? skipParentheses(node.argumentExpression) : (node as ComputedPropertyName).expression)) {
// Even non-bindable names are allowed as late-bound implied index signatures so long as the name is a simple `a.b.c` or `a['b']` type name expression
// isLateBindableAccessExpression handles skipParentheses internally
if (isNonBindableDynamicName(node) && !isLateBindableAccessExpression(isElementAccessExpression(node) ? node.argumentExpression : (node as ComputedPropertyName).expression)) {
return grammarErrorOnNode(node, message);
}
}
Expand Down
17 changes: 11 additions & 6 deletions src/compiler/transformers/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
declarationNameToString,
Diagnostics,
DiagnosticWithLocation,
ElementAccessExpression,
EmitFlags,
EmitHost,
EmitResolver,
Expand Down Expand Up @@ -118,6 +119,7 @@ import {
isInternalDeclaration,
isJSDocImportTag,
isJsonSourceFile,
isLateBindableAccessExpression,
isLateVisibilityPaintedStatement,
isLiteralImportTypeNode,
isMappedTypeNode,
Expand Down Expand Up @@ -616,8 +618,11 @@ export function transformDeclarations(context: TransformationContext): Transform
if (elem.kind === SyntaxKind.OmittedExpression) {
return elem;
}
if (elem.propertyName && isComputedPropertyName(elem.propertyName) && isEntityNameExpression(elem.propertyName.expression)) {
checkEntityNameVisibility(elem.propertyName.expression, enclosingDeclaration);
if (elem.propertyName && isComputedPropertyName(elem.propertyName)) {
const expr = elem.propertyName.expression;
if (isLateBindableAccessExpression(expr)) {
checkEntityNameVisibility(expr, enclosingDeclaration);
}
}

return factory.updateBindingElement(
Expand Down Expand Up @@ -815,7 +820,7 @@ export function transformDeclarations(context: TransformationContext): Transform
|| isMappedTypeNode(node);
}

function checkEntityNameVisibility(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node) {
function checkEntityNameVisibility(entityName: EntityNameOrEntityNameExpression | ElementAccessExpression, enclosingDeclaration: Node) {
const visibilityResult = resolver.isEntityNameVisible(entityName, enclosingDeclaration);
handleSymbolAccessibilityError(visibilityResult);
}
Expand Down Expand Up @@ -1012,16 +1017,16 @@ export function transformDeclarations(context: TransformationContext): Transform
return;
}
else if (
// Type declarations just need to double-check that the input computed name is an entity name expression
// Type declarations just need to double-check that the input computed name is a late-bindable access expression
(isInterfaceDeclaration(input.parent) || isTypeLiteralNode(input.parent))
&& !isEntityNameExpression(input.name.expression)
&& !isLateBindableAccessExpression(input.name.expression)
) {
context.addDiagnostic(createDiagnosticForNode(input, Diagnostics.Computed_properties_must_be_number_or_string_literals_variables_or_dotted_expressions_with_isolatedDeclarations));
return;
}
}
}
else if (!resolver.isLateBound(getParseTreeNode(input) as Declaration) || !isEntityNameExpression(input.name.expression)) {
else if (!resolver.isLateBound(getParseTreeNode(input) as Declaration) || !isLateBindableAccessExpression(input.name.expression)) {
return;
}
}
Expand Down
16 changes: 12 additions & 4 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1813,11 +1813,19 @@ export interface GeneratedPrivateIdentifier extends PrivateIdentifier {
}

/** @internal */
// A name that supports late-binding (used in checker)
// A name that supports late-binding (used in checker).
// Supports both property access chains (a.b.c) and element access chains (a['b']['c']).
export interface LateBoundName extends ComputedPropertyName {
readonly expression: EntityNameExpression;
readonly expression: EntityNameExpression | ElementAccessExpression;
}

/**
* An expression that can be used in a late-bindable computed property name.
* Includes property access chains (a.b.c) and element access chains with literal keys (a['b']['c']).
* @internal
*/
export type LateBindableAccessExpression = EntityNameExpression | ElementAccessExpression;

export interface Decorator extends Node {
readonly kind: SyntaxKind.Decorator;
readonly parent: NamedDeclaration;
Expand Down Expand Up @@ -5917,7 +5925,7 @@ export interface EmitResolver {
createTypeOfExpression(expr: Expression, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): TypeNode | undefined;
createLiteralConstValue(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration, tracker: SymbolTracker): Expression;
isSymbolAccessible(symbol: Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags | undefined, shouldComputeAliasToMarkVisible: boolean): SymbolAccessibilityResult;
isEntityNameVisible(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node): SymbolVisibilityResult;
isEntityNameVisible(entityName: EntityNameOrEntityNameExpression | ElementAccessExpression, enclosingDeclaration: Node): SymbolVisibilityResult;
// Returns the constant value this property access resolves to, or 'undefined' for a non-constant
getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number | undefined;
getEnumMemberValue(node: EnumMember): EvaluatorResult | undefined;
Expand Down Expand Up @@ -10614,7 +10622,7 @@ export interface SyntacticTypeNodeBuilderResolver {
getAllAccessorDeclarations(declaration: AccessorDeclaration): AllAccessorDeclarations;
requiresAddingImplicitUndefined(declaration: ParameterDeclaration | PropertySignature | JSDocParameterTag | JSDocPropertyTag | PropertyDeclaration, symbol: Symbol | undefined, enclosingDeclaration: Node | undefined): boolean;
isDefinitelyReferenceToGlobalSymbolObject(node: Node): boolean;
isEntityNameVisible(context: SyntacticTypeNodeBuilderContext, entityName: EntityNameOrEntityNameExpression, shouldComputeAliasToMakeVisible?: boolean): SymbolVisibilityResult;
isEntityNameVisible(context: SyntacticTypeNodeBuilderContext, entityName: EntityNameOrEntityNameExpression | ElementAccessExpression, shouldComputeAliasToMakeVisible?: boolean): SymbolVisibilityResult;
serializeExistingTypeNode(context: SyntacticTypeNodeBuilderContext, node: TypeNode, addUndefined?: boolean): TypeNode | undefined;
serializeReturnTypeForSignature(context: SyntacticTypeNodeBuilderContext, signatureDeclaration: SignatureDeclaration | JSDocSignature, symbol: Symbol | undefined): TypeNode | undefined;
serializeTypeOfExpression(context: SyntacticTypeNodeBuilderContext, expr: Expression): TypeNode;
Expand Down
45 changes: 40 additions & 5 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ import {
LanguageVariant,
last,
lastOrUndefined,
LateBindableAccessExpression,
LateVisibilityPaintedStatement,
length,
libMap,
Expand Down Expand Up @@ -7454,8 +7455,13 @@ export function isEntityNameExpression(node: Node): node is EntityNameExpression
return node.kind === SyntaxKind.Identifier || isPropertyAccessEntityNameExpression(node);
}

/** @internal */
export function getFirstIdentifier(node: EntityNameOrEntityNameExpression): Identifier {
/**
* Gets the first identifier in a name chain.
* Supports qualified names (a.b.c), property access expressions (a.b.c),
* and element access expressions (a['b']['c']).
* @internal
*/
export function getFirstIdentifier(node: EntityNameOrEntityNameExpression | ElementAccessExpression): Identifier {
switch (node.kind) {
case SyntaxKind.Identifier:
return node;
Expand All @@ -7466,14 +7472,43 @@ export function getFirstIdentifier(node: EntityNameOrEntityNameExpression): Iden
while (node.kind !== SyntaxKind.Identifier);
return node;
case SyntaxKind.PropertyAccessExpression:
case SyntaxKind.ElementAccessExpression:
let expr: Expression = node;
do {
node = node.expression;
expr = skipParentheses((expr as PropertyAccessExpression | ElementAccessExpression).expression);
}
while (node.kind !== SyntaxKind.Identifier);
return node;
while (expr.kind !== SyntaxKind.Identifier);
return expr as Identifier;
}
}

/**
* Returns true if the expression is a valid late-bindable access expression.
* A late-bindable access expression is:
* - An Identifier
* - A PropertyAccessExpression where the base is a late-bindable access expression
* - An ElementAccessExpression with a string/numeric literal key where the base is a late-bindable access expression
*
* This supports mixed chains like: obj.a['b'].c['d']
* Parentheses are skipped to support expressions like: (obj.a)['b']
* @internal
*/
export function isLateBindableAccessExpression(node: Node): node is LateBindableAccessExpression {
node = skipParentheses(node as Expression);

if (isIdentifier(node)) {
return true;
}
// For PropertyAccessExpression, require the name to be an Identifier (not PrivateIdentifier)
if (isPropertyAccessExpression(node) && isIdentifier(node.name)) {
return isLateBindableAccessExpression(node.expression);
}
if (isElementAccessExpression(node) && isStringOrNumericLiteralLike(skipParentheses(node.argumentExpression))) {
return isLateBindableAccessExpression(node.expression);
}
return false;
}

/** @internal */
export function isDottedName(node: Expression): boolean {
return node.kind === SyntaxKind.Identifier
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//// [tests/cases/compiler/enumKeysAsComputedPropertiesWithBracketNotation.ts] ////

//// [enumKeysAsComputedPropertiesWithBracketNotation.ts]
// Test that enum keys accessed with bracket notation can be used as computed properties
// Regression test for https://github.com/microsoft/TypeScript/issues/25083

enum Type {
Foo = 'foo',
'3x14' = '3x14'
}

// All of these should work
type TypeMap = {
[Type.Foo]: string; // Property access
[Type['3x14']]: number; // Element access with non-identifier key
}

// Bracket notation with identifier key should also work (equivalent to property access)
type TypeMap2 = {
[Type['Foo']]: boolean;
}

// Nested element access should work
const nested = {
inner: {
key: 'hello' as const
}
};

type TypeMap3 = {
[nested.inner.key]: string;
}

// Element access on deeply nested path
type TypeMap4 = {
[nested['inner']['key']]: string;
}

// Mixed chain: element access followed by property access
type TypeMap5 = {
[nested['inner'].key]: string;
}

// Mixed chain: property access followed by element access
type TypeMap6 = {
[nested.inner['key']]: string;
}

// Complex mixed chain
const deep = {
a: {
b: {
c: {
d: 'value' as const
}
}
}
};

type TypeMap7 = {
[deep.a['b'].c['d']]: string;
}

type TypeMap8 = {
[deep['a'].b['c'].d]: string;
}

// Parenthesized expressions
type TypeMap9 = {
[(nested.inner).key]: string;
}

type TypeMap10 = {
[(nested['inner']).key]: string;
}

type TypeMap11 = {
[(nested).inner.key]: string;
}

// Parenthesized keys in element access
type TypeMap12 = {
[nested[('inner')]['key']]: string;
}

type TypeMap13 = {
[nested['inner'][('key')]]: string;
}

type TypeMap14 = {
[deep[('a')][('b')].c['d']]: string;
}


//// [enumKeysAsComputedPropertiesWithBracketNotation.js]
"use strict";
// Test that enum keys accessed with bracket notation can be used as computed properties
// Regression test for https://github.com/microsoft/TypeScript/issues/25083
var Type;
(function (Type) {
Type["Foo"] = "foo";
Type["3x14"] = "3x14";
})(Type || (Type = {}));
// Nested element access should work
var nested = {
inner: {
key: 'hello'
}
};
// Complex mixed chain
var deep = {
a: {
b: {
c: {
d: 'value'
}
}
}
};
Loading
Loading