Skip to content

Commit 11ff822

Browse files
committed
Preserve input computed names in output declaration files when they are entity name expressions
1 parent ca18009 commit 11ff822

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,
@@ -6851,7 +6852,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
68516852
}
68526853
function shouldWriteTypeOfFunctionSymbol() {
68536854
const isStaticMethodSymbol = !!(symbol.flags & SymbolFlags.Method) && // typeof static method
6854-
some(symbol.declarations, declaration => isStatic(declaration));
6855+
some(symbol.declarations, declaration => isStatic(declaration) && !isLateBindableIndexSignature(getNameOfDeclaration(declaration)!));
68556856
const isNonLocalFunctionSymbol = !!(symbol.flags & SymbolFlags.Function) &&
68566857
(symbol.parent || // is exported function symbol
68576858
forEach(symbol.declarations, declaration => declaration.parent.kind === SyntaxKind.SourceFile || declaration.parent.kind === SyntaxKind.ModuleBlock));
@@ -7207,6 +7208,37 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
72077208
return ids;
72087209
}
72097210

7211+
function indexInfoToObjectComputedNamesOrSignatureDeclaration(indexInfo: IndexInfo, context: NodeBuilderContext, typeNode: TypeNode | undefined): [IndexSignatureDeclaration] | PropertySignature[] {
7212+
if (indexInfo.components) {
7213+
// 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
7214+
let unusableResult = false;
7215+
const result = map(indexInfo.components, e => {
7216+
if (e.name && isComputedPropertyName(e.name) && isEntityNameExpression(e.name.expression)) {
7217+
trackComputedName(e.name.expression, context.enclosingDeclaration, context);
7218+
}
7219+
else {
7220+
// Computed name didn't take the form `[a.b.c]: something` - bail on using the computed name.
7221+
// TODO: Issue isolated declarations error on this fallback?
7222+
unusableResult = true;
7223+
}
7224+
return setTextRange(
7225+
context,
7226+
factory.createPropertySignature(
7227+
indexInfo.isReadonly ? [factory.createModifier(SyntaxKind.ReadonlyKeyword)] : undefined,
7228+
e.name,
7229+
/*questionToken*/ undefined,
7230+
typeNode || typeToTypeNodeHelper(getTypeOfSymbol(e.symbol), context),
7231+
),
7232+
e,
7233+
);
7234+
});
7235+
if (!unusableResult) {
7236+
return result;
7237+
}
7238+
}
7239+
return [indexInfoToIndexSignatureDeclarationHelper(indexInfo, context, typeNode)];
7240+
}
7241+
72107242
function createTypeNodesFromResolvedType(resolvedType: ResolvedType): TypeElement[] | undefined {
72117243
if (checkTruncationLength(context)) {
72127244
if (context.flags & NodeBuilderFlags.NoTruncation) {
@@ -7223,7 +7255,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
72237255
typeElements.push(signatureToSignatureDeclarationHelper(signature, SyntaxKind.ConstructSignature, context) as ConstructSignatureDeclaration);
72247256
}
72257257
for (const info of resolvedType.indexInfos) {
7226-
typeElements.push(indexInfoToIndexSignatureDeclarationHelper(info, context, resolvedType.objectFlags & ObjectFlags.ReverseMapped ? createElidedInformationPlaceholder(context) : undefined));
7258+
typeElements.push(...indexInfoToObjectComputedNamesOrSignatureDeclaration(info, context, resolvedType.objectFlags & ObjectFlags.ReverseMapped ? createElidedInformationPlaceholder(context) : undefined));
72277259
}
72287260

72297261
const properties = resolvedType.properties;
@@ -15956,8 +15988,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1595615988
return symbolTable.get(InternalSymbolName.Index);
1595715989
}
1595815990

15959-
function createIndexInfo(keyType: Type, type: Type, isReadonly: boolean, declaration?: IndexSignatureDeclaration): IndexInfo {
15960-
return { keyType, type, isReadonly, declaration };
15991+
function createIndexInfo(keyType: Type, type: Type, isReadonly: boolean, declaration?: IndexSignatureDeclaration, components?: ElementWithComputedPropertyName[]): IndexInfo {
15992+
return { keyType, type, isReadonly, declaration, components };
1596115993
}
1596215994

1596315995
function getIndexInfosOfSymbol(symbol: Symbol): IndexInfo[] {
@@ -19554,7 +19586,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1955419586
}
1955519587

1955619588
function getIndexInfoWithReadonly(info: IndexInfo, readonly: boolean) {
19557-
return info.isReadonly !== readonly ? createIndexInfo(info.keyType, info.type, readonly, info.declaration) : info;
19589+
return info.isReadonly !== readonly ? createIndexInfo(info.keyType, info.type, readonly, info.declaration, info.components) : info;
1955819590
}
1955919591

1956019592
function createLiteralType(flags: TypeFlags, value: string | number | PseudoBigInt, symbol?: Symbol, regularType?: LiteralType) {
@@ -20431,7 +20463,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2043120463
}
2043220464

2043320465
function instantiateIndexInfo(info: IndexInfo, mapper: TypeMapper) {
20434-
return createIndexInfo(info.keyType, instantiateType(info.type, mapper), info.isReadonly, info.declaration);
20466+
return createIndexInfo(info.keyType, instantiateType(info.type, mapper), info.isReadonly, info.declaration, info.components);
2043520467
}
2043620468

2043720469
// Returns true if the given expression contains (at any level of nesting) a function or arrow expression
@@ -25215,7 +25247,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2521525247
}
2521625248
}
2521725249
}
25218-
const result = createAnonymousType(type.symbol, members, emptyArray, emptyArray, sameMap(getIndexInfosOfType(type), info => createIndexInfo(info.keyType, getWidenedType(info.type), info.isReadonly)));
25250+
const result = createAnonymousType(type.symbol, members, emptyArray, emptyArray, sameMap(getIndexInfosOfType(type), info => createIndexInfo(info.keyType, getWidenedType(info.type), info.isReadonly, info.declaration, info.components)));
2521925251
result.objectFlags |= getObjectFlags(type) & (ObjectFlags.JSLiteral | ObjectFlags.NonInferrableType); // Retain js literal flag through widening
2522025252
return result;
2522125253
}
@@ -32625,9 +32657,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3262532657
isTypeAssignableToKind(checkComputedPropertyName(firstDecl.name), TypeFlags.ESSymbol));
3262632658
}
3262732659

32660+
function isSymbolWithComputedName(symbol: Symbol) {
32661+
const firstDecl = symbol.declarations?.[0];
32662+
return firstDecl && isNamedDeclaration(firstDecl) && isComputedPropertyName(firstDecl.name);
32663+
}
32664+
3262832665
// NOTE: currently does not make pattern literal indexers, eg `${number}px`
3262932666
function getObjectLiteralIndexInfo(isReadonly: boolean, offset: number, properties: Symbol[], keyType: Type): IndexInfo {
3263032667
const propTypes: Type[] = [];
32668+
let components: ElementWithComputedPropertyName[] | undefined;
3263132669
for (let i = offset; i < properties.length; i++) {
3263232670
const prop = properties[i];
3263332671
if (
@@ -32636,10 +32674,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3263632674
keyType === esSymbolType && isSymbolWithSymbolName(prop)
3263732675
) {
3263832676
propTypes.push(getTypeOfSymbol(properties[i]));
32677+
if (isSymbolWithComputedName(properties[i])) {
32678+
components = append(components, properties[i].declarations?.[0]! as ElementWithComputedPropertyName);
32679+
}
3263932680
}
3264032681
}
3264132682
const unionType = propTypes.length ? getUnionType(propTypes, UnionReduction.Subtype) : undefinedType;
32642-
return createIndexInfo(keyType, unionType, isReadonly);
32683+
return createIndexInfo(keyType, unionType, isReadonly, /*declaration*/ undefined, components);
3264332684
}
3264432685

3264532686
function getImmediateAliasedSymbol(symbol: Symbol): Symbol | undefined {
@@ -45721,9 +45762,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4572145762
const typeDeclaration = symbol.valueDeclaration;
4572245763
if (typeDeclaration && isClassLike(typeDeclaration)) {
4572345764
for (const member of typeDeclaration.members) {
45724-
// Only process instance properties with computed names here. Static properties cannot be in conflict with indexers,
45725-
// and properties with literal names were already checked.
45726-
if (!isStatic(member) && !hasBindableName(member)) {
45765+
// Only process instance properties against instance index signatures and static properties against static index signatures
45766+
if (
45767+
(
45768+
(!isStaticIndex && !isStatic(member)) ||
45769+
(isStaticIndex && isStatic(member))
45770+
) && !hasBindableName(member)
45771+
) {
4572745772
const symbol = getSymbolOfDeclaration(member);
4572845773
checkIndexConstraintForProperty(type, symbol, getTypeOfExpression((member as DynamicNamedDeclaration).name.expression), getNonMissingTypeOfSymbol(symbol));
4572945774
}
@@ -50288,6 +50333,31 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
5028850333
result ||= [];
5028950334
for (const info of infoList!) {
5029050335
if (info.declaration) continue;
50336+
if (info.components) {
50337+
let unusableResult = false;
50338+
const recreatedComponents = map(info.components, e => {
50339+
if (e.name && isComputedPropertyName(e.name) && isEntityNameExpression(e.name.expression)) {
50340+
trackComputedName(e.name.expression);
50341+
}
50342+
else {
50343+
// Computed name didn't take the form `[a.b.c]: something` - bail on using the computed name.
50344+
// TODO: Issue isolated declarations error on this fallback?
50345+
unusableResult = true;
50346+
}
50347+
const mods = infoList === staticInfos ? [factory.createModifier(SyntaxKind.StaticKeyword)] as Modifier[] : undefined;
50348+
return factory.createPropertyDeclaration(
50349+
append(mods, info.isReadonly ? factory.createModifier(SyntaxKind.ReadonlyKeyword) : undefined),
50350+
e.name,
50351+
/*questionOrExclamationToken*/ undefined,
50352+
nodeBuilder.typeToTypeNode(getTypeOfSymbol(e.symbol), enclosing, flags, internalFlags, tracker),
50353+
/*initializer*/ undefined,
50354+
);
50355+
});
50356+
if (!unusableResult) {
50357+
result.push(...recreatedComponents);
50358+
continue;
50359+
}
50360+
}
5029150361
const node = nodeBuilder.indexInfoToIndexSignatureDeclaration(info, enclosing, flags, internalFlags, tracker);
5029250362
if (node && infoList === staticInfos) {
5029350363
(((node as Mutable<typeof node>).modifiers ||= factory.createNodeArray()) as MutableNodeArray<Modifier>).unshift(factory.createModifier(SyntaxKind.StaticKeyword));
@@ -50298,6 +50368,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
5029850368
}
5029950369
}
5030050370
return result;
50371+
50372+
function trackComputedName(accessExpression: EntityNameOrEntityNameExpression) {
50373+
if (!tracker.trackSymbol) return;
50374+
// get symbol of the first identifier of the entityName
50375+
const firstIdentifier = getFirstIdentifier(accessExpression);
50376+
const name = resolveName(firstIdentifier, firstIdentifier.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue, /*nameNotFoundMessage*/ undefined, /*isUse*/ true);
50377+
if (name) {
50378+
tracker.trackSymbol(name, enclosing, SymbolFlags.Value);
50379+
}
50380+
}
5030150381
},
5030250382
};
5030350383

@@ -51750,7 +51830,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
5175051830
}
5175151831

5175251832
function checkGrammarForInvalidDynamicName(node: DeclarationName, message: DiagnosticMessage) {
51753-
if (isNonBindableDynamicName(node)) {
51833+
// 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
51834+
if (isNonBindableDynamicName(node) && !isEntityNameExpression(isElementAccessExpression(node) ? skipParentheses(node.argumentExpression) : (node as ComputedPropertyName).expression)) {
5175451835
return grammarErrorOnNode(node, message);
5175551836
}
5175651837
}

src/compiler/types.ts

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

58775877
// dprint-ignore
@@ -6998,11 +6998,14 @@ export const enum IndexKind {
69986998
Number,
69996999
}
70007000

7001+
export type ElementWithComputedPropertyName = (ClassElement | ObjectLiteralElement) & { name: ComputedPropertyName; };
7002+
70017003
export interface IndexInfo {
70027004
keyType: Type;
70037005
type: Type;
70047006
isReadonly: boolean;
70057007
declaration?: IndexSignatureDeclaration;
7008+
components?: ElementWithComputedPropertyName[];
70067009
}
70077010

70087011
/** @internal */

0 commit comments

Comments
 (0)