Skip to content

Commit 37c14b8

Browse files
committed
Preserve input computed names in output declaration files when they are entity name expressions
1 parent e5758ab commit 37c14b8

File tree

2 files changed

+97
-13
lines changed

2 files changed

+97
-13
lines changed

src/compiler/checker.ts

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ import {
157157
ElementAccessChain,
158158
ElementAccessExpression,
159159
ElementFlags,
160+
ElementWithComputedPropertyName,
160161
EmitFlags,
161162
EmitHint,
162163
emitModuleKindIsNonNodeESM,
@@ -6700,7 +6701,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
67006701
}
67016702
function shouldWriteTypeOfFunctionSymbol() {
67026703
const isStaticMethodSymbol = !!(symbol.flags & SymbolFlags.Method) && // typeof static method
6703-
some(symbol.declarations, declaration => isStatic(declaration));
6704+
some(symbol.declarations, declaration => isStatic(declaration) && !isLateBindableIndexSignature(getNameOfDeclaration(declaration)!));
67046705
const isNonLocalFunctionSymbol = !!(symbol.flags & SymbolFlags.Function) &&
67056706
(symbol.parent || // is exported function symbol
67066707
forEach(symbol.declarations, declaration => declaration.parent.kind === SyntaxKind.SourceFile || declaration.parent.kind === SyntaxKind.ModuleBlock));
@@ -7056,6 +7057,37 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
70567057
return ids;
70577058
}
70587059

7060+
function indexInfoToObjectComputedNamesOrSignatureDeclaration(indexInfo: IndexInfo, context: NodeBuilderContext, typeNode: TypeNode | undefined): [IndexSignatureDeclaration] | PropertySignature[] {
7061+
if (indexInfo.components) {
7062+
// Index info is derived from object or class computed property names (plus explicit named members) - we can clone those instead of writing out the result computed index signature
7063+
let unusableResult = false;
7064+
const result = map(indexInfo.components, e => {
7065+
if (e.name && isComputedPropertyName(e.name) && isEntityNameExpression(e.name.expression)) {
7066+
trackComputedName(e.name.expression, context.enclosingDeclaration, context);
7067+
}
7068+
else {
7069+
// Computed name didn't take the form `[a.b.c]: something` - bail on using the computed name.
7070+
// TODO: Issue isolated declarations error on this fallback?
7071+
unusableResult = true;
7072+
}
7073+
return setTextRange(
7074+
context,
7075+
factory.createPropertySignature(
7076+
indexInfo.isReadonly ? [factory.createModifier(SyntaxKind.ReadonlyKeyword)] : undefined,
7077+
e.name,
7078+
/*questionToken*/ undefined,
7079+
typeNode || typeToTypeNodeHelper(getTypeOfSymbol(e.symbol), context),
7080+
),
7081+
e,
7082+
);
7083+
});
7084+
if (!unusableResult) {
7085+
return result;
7086+
}
7087+
}
7088+
return [indexInfoToIndexSignatureDeclarationHelper(indexInfo, context, typeNode)];
7089+
}
7090+
70597091
function createTypeNodesFromResolvedType(resolvedType: ResolvedType): TypeElement[] | undefined {
70607092
if (checkTruncationLength(context)) {
70617093
if (context.flags & NodeBuilderFlags.NoTruncation) {
@@ -7072,7 +7104,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
70727104
typeElements.push(signatureToSignatureDeclarationHelper(signature, SyntaxKind.ConstructSignature, context) as ConstructSignatureDeclaration);
70737105
}
70747106
for (const info of resolvedType.indexInfos) {
7075-
typeElements.push(indexInfoToIndexSignatureDeclarationHelper(info, context, resolvedType.objectFlags & ObjectFlags.ReverseMapped ? createElidedInformationPlaceholder(context) : undefined));
7107+
typeElements.push(...indexInfoToObjectComputedNamesOrSignatureDeclaration(info, context, resolvedType.objectFlags & ObjectFlags.ReverseMapped ? createElidedInformationPlaceholder(context) : undefined));
70767108
}
70777109

70787110
const properties = resolvedType.properties;
@@ -16247,8 +16279,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1624716279
return symbolTable.get(InternalSymbolName.Index);
1624816280
}
1624916281

16250-
function createIndexInfo(keyType: Type, type: Type, isReadonly: boolean, declaration?: IndexSignatureDeclaration): IndexInfo {
16251-
return { keyType, type, isReadonly, declaration };
16282+
function createIndexInfo(keyType: Type, type: Type, isReadonly: boolean, declaration?: IndexSignatureDeclaration, components?: ElementWithComputedPropertyName[]): IndexInfo {
16283+
return { keyType, type, isReadonly, declaration, components };
1625216284
}
1625316285

1625416286
function getIndexInfosOfSymbol(symbol: Symbol): IndexInfo[] {
@@ -19845,7 +19877,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1984519877
}
1984619878

1984719879
function getIndexInfoWithReadonly(info: IndexInfo, readonly: boolean) {
19848-
return info.isReadonly !== readonly ? createIndexInfo(info.keyType, info.type, readonly, info.declaration) : info;
19880+
return info.isReadonly !== readonly ? createIndexInfo(info.keyType, info.type, readonly, info.declaration, info.components) : info;
1984919881
}
1985019882

1985119883
function createLiteralType(flags: TypeFlags, value: string | number | PseudoBigInt, symbol?: Symbol, regularType?: LiteralType) {
@@ -20722,7 +20754,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2072220754
}
2072320755

2072420756
function instantiateIndexInfo(info: IndexInfo, mapper: TypeMapper) {
20725-
return createIndexInfo(info.keyType, instantiateType(info.type, mapper), info.isReadonly, info.declaration);
20757+
return createIndexInfo(info.keyType, instantiateType(info.type, mapper), info.isReadonly, info.declaration, info.components);
2072620758
}
2072720759

2072820760
// Returns true if the given expression contains (at any level of nesting) a function or arrow expression
@@ -25506,7 +25538,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2550625538
}
2550725539
}
2550825540
}
25509-
const result = createAnonymousType(type.symbol, members, emptyArray, emptyArray, sameMap(getIndexInfosOfType(type), info => createIndexInfo(info.keyType, getWidenedType(info.type), info.isReadonly)));
25541+
const result = createAnonymousType(type.symbol, members, emptyArray, emptyArray, sameMap(getIndexInfosOfType(type), info => createIndexInfo(info.keyType, getWidenedType(info.type), info.isReadonly, info.declaration, info.components)));
2551025542
result.objectFlags |= getObjectFlags(type) & (ObjectFlags.JSLiteral | ObjectFlags.NonInferrableType); // Retain js literal flag through widening
2551125543
return result;
2551225544
}
@@ -32881,9 +32913,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3288132913
isTypeAssignableToKind(checkComputedPropertyName(firstDecl.name), TypeFlags.ESSymbol));
3288232914
}
3288332915

32916+
function isSymbolWithComputedName(symbol: Symbol) {
32917+
const firstDecl = symbol.declarations?.[0];
32918+
return firstDecl && isNamedDeclaration(firstDecl) && isComputedPropertyName(firstDecl.name);
32919+
}
32920+
3288432921
// NOTE: currently does not make pattern literal indexers, eg `${number}px`
3288532922
function getObjectLiteralIndexInfo(isReadonly: boolean, offset: number, properties: Symbol[], keyType: Type): IndexInfo {
3288632923
const propTypes: Type[] = [];
32924+
let components: ElementWithComputedPropertyName[] | undefined;
3288732925
for (let i = offset; i < properties.length; i++) {
3288832926
const prop = properties[i];
3288932927
if (
@@ -32892,10 +32930,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3289232930
keyType === esSymbolType && isSymbolWithSymbolName(prop)
3289332931
) {
3289432932
propTypes.push(getTypeOfSymbol(properties[i]));
32933+
if (isSymbolWithComputedName(properties[i])) {
32934+
components = append(components, properties[i].declarations?.[0]! as ElementWithComputedPropertyName);
32935+
}
3289532936
}
3289632937
}
3289732938
const unionType = propTypes.length ? getUnionType(propTypes, UnionReduction.Subtype) : undefinedType;
32898-
return createIndexInfo(keyType, unionType, isReadonly);
32939+
return createIndexInfo(keyType, unionType, isReadonly, /*declaration*/ undefined, components);
3289932940
}
3290032941

3290132942
function getImmediateAliasedSymbol(symbol: Symbol): Symbol | undefined {
@@ -45911,9 +45952,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4591145952
const typeDeclaration = symbol.valueDeclaration;
4591245953
if (typeDeclaration && isClassLike(typeDeclaration)) {
4591345954
for (const member of typeDeclaration.members) {
45914-
// Only process instance properties with computed names here. Static properties cannot be in conflict with indexers,
45915-
// and properties with literal names were already checked.
45916-
if (!isStatic(member) && !hasBindableName(member)) {
45955+
// Only process instance properties against instance index signatures and static properties against static index signatures
45956+
if (
45957+
(
45958+
(!isStaticIndex && !isStatic(member)) ||
45959+
(isStaticIndex && isStatic(member))
45960+
) && !hasBindableName(member)
45961+
) {
4591745962
const symbol = getSymbolOfDeclaration(member);
4591845963
checkIndexConstraintForProperty(type, symbol, getTypeOfExpression((member as DynamicNamedDeclaration).name.expression), getNonMissingTypeOfSymbol(symbol));
4591945964
}
@@ -50540,6 +50585,31 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
5054050585
result ||= [];
5054150586
for (const info of infoList!) {
5054250587
if (info.declaration) continue;
50588+
if (info.components) {
50589+
let unusableResult = false;
50590+
const recreatedComponents = map(info.components, e => {
50591+
if (e.name && isComputedPropertyName(e.name) && isEntityNameExpression(e.name.expression)) {
50592+
trackComputedName(e.name.expression);
50593+
}
50594+
else {
50595+
// Computed name didn't take the form `[a.b.c]: something` - bail on using the computed name.
50596+
// TODO: Issue isolated declarations error on this fallback?
50597+
unusableResult = true;
50598+
}
50599+
const mods = infoList === staticInfos ? [factory.createModifier(SyntaxKind.StaticKeyword)] as Modifier[] : undefined;
50600+
return factory.createPropertyDeclaration(
50601+
append(mods, info.isReadonly ? factory.createModifier(SyntaxKind.ReadonlyKeyword) : undefined),
50602+
e.name,
50603+
/*questionOrExclamationToken*/ undefined,
50604+
nodeBuilder.typeToTypeNode(getTypeOfSymbol(e.symbol), enclosing, flags, internalFlags, tracker),
50605+
/*initializer*/ undefined,
50606+
);
50607+
});
50608+
if (!unusableResult) {
50609+
result.push(...recreatedComponents);
50610+
continue;
50611+
}
50612+
}
5054350613
const node = nodeBuilder.indexInfoToIndexSignatureDeclaration(info, enclosing, flags, internalFlags, tracker);
5054450614
if (node && infoList === staticInfos) {
5054550615
(((node as Mutable<typeof node>).modifiers ||= factory.createNodeArray()) as MutableNodeArray<Modifier>).unshift(factory.createModifier(SyntaxKind.StaticKeyword));
@@ -50550,6 +50620,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
5055050620
}
5055150621
}
5055250622
return result;
50623+
50624+
function trackComputedName(accessExpression: EntityNameOrEntityNameExpression) {
50625+
if (!tracker.trackSymbol) return;
50626+
// get symbol of the first identifier of the entityName
50627+
const firstIdentifier = getFirstIdentifier(accessExpression);
50628+
const name = resolveName(firstIdentifier, firstIdentifier.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue, /*nameNotFoundMessage*/ undefined, /*isUse*/ true);
50629+
if (name) {
50630+
tracker.trackSymbol(name, enclosing, SymbolFlags.Value);
50631+
}
50632+
}
5055350633
},
5055450634
};
5055550635

@@ -52000,7 +52080,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
5200052080
}
5200152081

5200252082
function checkGrammarForInvalidDynamicName(node: DeclarationName, message: DiagnosticMessage) {
52003-
if (isNonBindableDynamicName(node)) {
52083+
// 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
52084+
if (isNonBindableDynamicName(node) && !isEntityNameExpression(isElementAccessExpression(node) ? skipParentheses(node.argumentExpression) : (node as ComputedPropertyName).expression)) {
5200452085
return grammarErrorOnNode(node, message);
5200552086
}
5200652087
}

src/compiler/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5867,7 +5867,7 @@ export interface EmitResolver {
58675867
getDeclarationStatementsForSourceFile(node: SourceFile, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): Statement[] | undefined;
58685868
isImportRequiredByAugmentation(decl: ImportDeclaration): boolean;
58695869
isDefinitelyReferenceToGlobalSymbolObject(node: Node): boolean;
5870-
createLateBoundIndexSignatures(cls: ClassLikeDeclaration, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): IndexSignatureDeclaration[] | undefined;
5870+
createLateBoundIndexSignatures(cls: ClassLikeDeclaration, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): (IndexSignatureDeclaration | PropertyDeclaration)[] | undefined;
58715871
}
58725872

58735873
// dprint-ignore
@@ -6995,11 +6995,14 @@ export const enum IndexKind {
69956995
Number,
69966996
}
69976997

6998+
export type ElementWithComputedPropertyName = (ClassElement | ObjectLiteralElement) & { name: ComputedPropertyName; };
6999+
69987000
export interface IndexInfo {
69997001
keyType: Type;
70007002
type: Type;
70017003
isReadonly: boolean;
70027004
declaration?: IndexSignatureDeclaration;
7005+
components?: ElementWithComputedPropertyName[];
70037006
}
70047007

70057008
/** @internal */

0 commit comments

Comments
 (0)