Support interpreting non-literal computed properties in classes as implicit index signatures (#59860)

This commit is contained in:
Wesley Wigham
2024-09-23 11:09:06 -07:00
committed by GitHub
parent e24cc01b08
commit fa0080f480
34 changed files with 400 additions and 81 deletions
+148 -30
View File
@@ -583,6 +583,7 @@ import {
isImportTypeNode,
isInCompoundLikeAssignment,
isIndexedAccessTypeNode,
isIndexSignatureDeclaration,
isInExpressionContext,
isInfinityOrNaNString,
isInitializedProperty,
@@ -878,6 +879,7 @@ import {
ModuleResolutionKind,
ModuleSpecifierResolutionHost,
Mutable,
MutableNodeArray,
NamedDeclaration,
NamedExports,
NamedImportsOrExports,
@@ -13345,12 +13347,25 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
* - The type of its expression is a string or numeric literal type, or is a `unique symbol` type.
*/
function isLateBindableName(node: DeclarationName): node is LateBoundName {
return isLateBindableAST(node)
&& isTypeUsableAsPropertyName(isComputedPropertyName(node) ? checkComputedPropertyName(node) : checkExpressionCached((node as ElementAccessExpression).argumentExpression));
}
function isLateBindableIndexSignature(node: DeclarationName): node is LateBoundName {
return isLateBindableAST(node)
&& isTypeUsableAsIndexSignature(isComputedPropertyName(node) ? checkComputedPropertyName(node) : checkExpressionCached((node as ElementAccessExpression).argumentExpression));
}
function isLateBindableAST(node: DeclarationName) {
if (!isComputedPropertyName(node) && !isElementAccessExpression(node)) {
return false;
}
const expr = isComputedPropertyName(node) ? node.expression : node.argumentExpression;
return isEntityNameExpression(expr)
&& isTypeUsableAsPropertyName(isComputedPropertyName(node) ? checkComputedPropertyName(node) : checkExpressionCached(expr));
return isEntityNameExpression(expr);
}
function isTypeUsableAsIndexSignature(type: Type): boolean {
return isTypeAssignableTo(type, stringNumberSymbolType);
}
function isLateBoundName(name: __String): boolean {
@@ -13367,6 +13382,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return !!name && isLateBindableName(name);
}
function hasLateBindableIndexSignature(node: Declaration) {
const name = getNameOfDeclaration(node);
return !!name && isLateBindableIndexSignature(name);
}
/**
* Indicates whether a declaration has an early-bound name or a dynamic name that can be late-bound.
*/
@@ -13474,6 +13494,31 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return links.resolvedSymbol;
}
function lateBindIndexSignature(parent: Symbol, earlySymbols: SymbolTable | undefined, lateSymbols: Map<__String, TransientSymbol>, decl: LateBoundDeclaration | LateBoundBinaryExpressionDeclaration) {
// First, late bind the index symbol itself, if needed
let indexSymbol = lateSymbols.get(InternalSymbolName.Index);
if (!indexSymbol) {
const early = earlySymbols?.get(InternalSymbolName.Index);
if (!early) {
indexSymbol = createSymbol(SymbolFlags.None, InternalSymbolName.Index, CheckFlags.Late);
}
else {
indexSymbol = cloneSymbol(early);
indexSymbol.links.checkFlags |= CheckFlags.Late;
}
lateSymbols.set(InternalSymbolName.Index, indexSymbol);
}
// Then just add the computed name as a late bound declaration
// (note: unlike `addDeclarationToLateBoundSymbol` we do not set up a `.lateSymbol` on `decl`'s links,
// since that would point at an index symbol and not a single property symbol, like most consumers would expect)
if (!indexSymbol.declarations) {
indexSymbol.declarations = [decl];
}
else if (!decl.symbol.isReplaceableByMethod) {
indexSymbol.declarations.push(decl);
}
}
function getResolvedMembersOrExportsOfSymbol(symbol: Symbol, resolutionKind: MembersOrExportsResolutionKind): Map<__String, Symbol> {
const links = getSymbolLinks(symbol);
if (!links[resolutionKind]) {
@@ -13497,6 +13542,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (hasLateBindableName(member)) {
lateBindMember(symbol, earlySymbols, lateSymbols, member);
}
else if (hasLateBindableIndexSignature(member)) {
lateBindIndexSignature(symbol, earlySymbols, lateSymbols, member as Node as LateBoundDeclaration | LateBoundBinaryExpressionDeclaration);
}
}
}
}
@@ -14160,7 +14208,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const indexSymbol = getIndexSymbolFromSymbolTable(members);
if (indexSymbol) {
indexInfos = getIndexInfosOfIndexSymbol(indexSymbol);
indexInfos = getIndexInfosOfIndexSymbol(indexSymbol, arrayFrom(members.values()));
}
else {
if (baseConstructorIndexInfo) {
@@ -16192,7 +16240,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
function getIndexSymbol(symbol: Symbol): Symbol | undefined {
return symbol.members ? getIndexSymbolFromSymbolTable(symbol.members) : undefined;
return symbol.members ? getIndexSymbolFromSymbolTable(getMembersOfSymbol(symbol)) : undefined;
}
function getIndexSymbolFromSymbolTable(symbolTable: SymbolTable): Symbol | undefined {
@@ -16205,24 +16253,67 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function getIndexInfosOfSymbol(symbol: Symbol): IndexInfo[] {
const indexSymbol = getIndexSymbol(symbol);
return indexSymbol ? getIndexInfosOfIndexSymbol(indexSymbol) : emptyArray;
return indexSymbol ? getIndexInfosOfIndexSymbol(indexSymbol, arrayFrom(getMembersOfSymbol(symbol).values())) : emptyArray;
}
function getIndexInfosOfIndexSymbol(indexSymbol: Symbol): IndexInfo[] {
// note intentional similarities to index signature building in `checkObjectLiteral` for parity
function getIndexInfosOfIndexSymbol(indexSymbol: Symbol, siblingSymbols: Symbol[] | undefined = indexSymbol.parent ? arrayFrom(getMembersOfSymbol(indexSymbol.parent).values()) : undefined): IndexInfo[] {
if (indexSymbol.declarations) {
const indexInfos: IndexInfo[] = [];
for (const declaration of (indexSymbol.declarations as IndexSignatureDeclaration[])) {
if (declaration.parameters.length === 1) {
const parameter = declaration.parameters[0];
if (parameter.type) {
forEachType(getTypeFromTypeNode(parameter.type), keyType => {
if (isValidIndexKeyType(keyType) && !findIndexInfo(indexInfos, keyType)) {
indexInfos.push(createIndexInfo(keyType, declaration.type ? getTypeFromTypeNode(declaration.type) : anyType, hasEffectiveModifier(declaration, ModifierFlags.Readonly), declaration));
let hasComputedNumberProperty = false;
let readonlyComputedNumberProperty = true;
let hasComputedSymbolProperty = false;
let readonlyComputedSymbolProperty = true;
let hasComputedStringProperty = false;
let readonlyComputedStringProperty = true;
const computedPropertySymbols: Symbol[] = [];
for (const declaration of indexSymbol.declarations) {
if (isIndexSignatureDeclaration(declaration)) {
if (declaration.parameters.length === 1) {
const parameter = declaration.parameters[0];
if (parameter.type) {
forEachType(getTypeFromTypeNode(parameter.type), keyType => {
if (isValidIndexKeyType(keyType) && !findIndexInfo(indexInfos, keyType)) {
indexInfos.push(createIndexInfo(keyType, declaration.type ? getTypeFromTypeNode(declaration.type) : anyType, hasEffectiveModifier(declaration, ModifierFlags.Readonly), declaration));
}
});
}
}
}
else if (hasLateBindableIndexSignature(declaration)) {
const declName = isBinaryExpression(declaration) ? declaration.left as ElementAccessExpression : (declaration as LateBoundDeclaration).name;
const keyType = isElementAccessExpression(declName) ? checkExpressionCached(declName.argumentExpression) : checkComputedPropertyName(declName);
if (findIndexInfo(indexInfos, keyType)) {
continue; // Explicit index for key type takes priority
}
if (isTypeAssignableTo(keyType, stringNumberSymbolType)) {
if (isTypeAssignableTo(keyType, numberType)) {
hasComputedNumberProperty = true;
if (!hasEffectiveReadonlyModifier(declaration)) {
readonlyComputedNumberProperty = false;
}
});
}
else if (isTypeAssignableTo(keyType, esSymbolType)) {
hasComputedSymbolProperty = true;
if (!hasEffectiveReadonlyModifier(declaration)) {
readonlyComputedSymbolProperty = false;
}
}
else {
hasComputedStringProperty = true;
if (!hasEffectiveReadonlyModifier(declaration)) {
readonlyComputedStringProperty = false;
}
}
computedPropertySymbols.push(declaration.symbol);
}
}
}
const allPropertySymbols = concatenate(computedPropertySymbols, filter(siblingSymbols, s => s !== indexSymbol));
// aggregate similar index infos implied to be the same key to the same combined index info
if (hasComputedStringProperty && !findIndexInfo(indexInfos, stringType)) indexInfos.push(getObjectLiteralIndexInfo(readonlyComputedStringProperty, 0, allPropertySymbols, stringType));
if (hasComputedNumberProperty && !findIndexInfo(indexInfos, numberType)) indexInfos.push(getObjectLiteralIndexInfo(readonlyComputedNumberProperty, 0, allPropertySymbols, numberType));
if (hasComputedSymbolProperty && !findIndexInfo(indexInfos, esSymbolType)) indexInfos.push(getObjectLiteralIndexInfo(readonlyComputedSymbolProperty, 0, allPropertySymbols, esSymbolType));
return indexInfos;
}
return emptyArray;
@@ -32790,7 +32881,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
isTypeAssignableToKind(checkComputedPropertyName(firstDecl.name), TypeFlags.ESSymbol));
}
function getObjectLiteralIndexInfo(node: ObjectLiteralExpression, offset: number, properties: Symbol[], keyType: Type): IndexInfo {
// NOTE: currently does not make pattern literal indexers, eg `${number}px`
function getObjectLiteralIndexInfo(isReadonly: boolean, offset: number, properties: Symbol[], keyType: Type): IndexInfo {
const propTypes: Type[] = [];
for (let i = offset; i < properties.length; i++) {
const prop = properties[i];
@@ -32803,7 +32895,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
const unionType = propTypes.length ? getUnionType(propTypes, UnionReduction.Subtype) : undefinedType;
return createIndexInfo(keyType, unionType, isConstContext(node));
return createIndexInfo(keyType, unionType, isReadonly);
}
function getImmediateAliasedSymbol(symbol: Symbol): Symbol | undefined {
@@ -33008,9 +33100,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function createObjectLiteralType() {
const indexInfos = [];
if (hasComputedStringProperty) indexInfos.push(getObjectLiteralIndexInfo(node, offset, propertiesArray, stringType));
if (hasComputedNumberProperty) indexInfos.push(getObjectLiteralIndexInfo(node, offset, propertiesArray, numberType));
if (hasComputedSymbolProperty) indexInfos.push(getObjectLiteralIndexInfo(node, offset, propertiesArray, esSymbolType));
const isReadonly = isConstContext(node);
if (hasComputedStringProperty) indexInfos.push(getObjectLiteralIndexInfo(isReadonly, offset, propertiesArray, stringType));
if (hasComputedNumberProperty) indexInfos.push(getObjectLiteralIndexInfo(isReadonly, offset, propertiesArray, numberType));
if (hasComputedSymbolProperty) indexInfos.push(getObjectLiteralIndexInfo(isReadonly, offset, propertiesArray, esSymbolType));
const result = createAnonymousType(node.symbol, propertiesTable, emptyArray, emptyArray, indexInfos);
result.objectFlags |= objectFlags | ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral;
if (isJSObjectLiteral) {
@@ -41575,18 +41668,21 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const indexSymbol = getIndexSymbol(getSymbolOfDeclaration(node));
if (indexSymbol?.declarations) {
const indexSignatureMap = new Map<TypeId, { type: Type; declarations: IndexSignatureDeclaration[]; }>();
for (const declaration of (indexSymbol.declarations as IndexSignatureDeclaration[])) {
if (declaration.parameters.length === 1 && declaration.parameters[0].type) {
forEachType(getTypeFromTypeNode(declaration.parameters[0].type), type => {
const entry = indexSignatureMap.get(getTypeId(type));
if (entry) {
entry.declarations.push(declaration);
}
else {
indexSignatureMap.set(getTypeId(type), { type, declarations: [declaration] });
}
});
for (const declaration of indexSymbol.declarations) {
if (isIndexSignatureDeclaration(declaration)) {
if (declaration.parameters.length === 1 && declaration.parameters[0].type) {
forEachType(getTypeFromTypeNode(declaration.parameters[0].type), type => {
const entry = indexSignatureMap.get(getTypeId(type));
if (entry) {
entry.declarations.push(declaration);
}
else {
indexSignatureMap.set(getTypeId(type), { type, declarations: [declaration] });
}
});
}
}
// Do nothing for late-bound index signatures: allow these to duplicate one another and explicit indexes
}
indexSignatureMap.forEach(entry => {
if (entry.declarations.length > 1) {
@@ -50433,6 +50529,28 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
},
isImportRequiredByAugmentation,
isDefinitelyReferenceToGlobalSymbolObject,
createLateBoundIndexSignatures: (cls, enclosing, flags, internalFlags, tracker) => {
const sym = cls.symbol;
const staticInfos = getIndexInfosOfType(getTypeOfSymbol(sym));
const instanceIndexSymbol = getIndexSymbol(sym);
const instanceInfos = instanceIndexSymbol && getIndexInfosOfIndexSymbol(instanceIndexSymbol, arrayFrom(getMembersOfSymbol(sym).values()));
let result;
for (const infoList of [staticInfos, instanceInfos]) {
if (!length(infoList)) continue;
result ||= [];
for (const info of infoList!) {
if (info.declaration) continue;
const node = nodeBuilder.indexInfoToIndexSignatureDeclaration(info, enclosing, flags, internalFlags, tracker);
if (node && infoList === staticInfos) {
(((node as Mutable<typeof node>).modifiers ||= factory.createNodeArray()) as MutableNodeArray<Modifier>).unshift(factory.createModifier(SyntaxKind.StaticKeyword));
}
if (node) {
result.push(node);
}
}
}
return result;
},
};
function isImportRequiredByAugmentation(node: ImportDeclaration) {
+1
View File
@@ -1167,6 +1167,7 @@ export const notImplementedResolver: EmitResolver = {
getDeclarationStatementsForSourceFile: notImplemented,
isImportRequiredByAugmentation: notImplemented,
isDefinitelyReferenceToGlobalSymbolObject: notImplemented,
createLateBoundIndexSignatures: notImplemented,
};
const enum PipelinePhase {
+3 -1
View File
@@ -12,6 +12,7 @@ import {
canHaveModifiers,
canProduceDiagnostics,
ClassDeclaration,
ClassElement,
compact,
concatenate,
ConditionalTypeNode,
@@ -1652,7 +1653,8 @@ export function transformDeclarations(context: TransformationContext): Transform
/*initializer*/ undefined,
),
] : undefined;
const memberNodes = concatenate(concatenate(privateIdentifier, parameterProperties), visitNodes(input.members, visitDeclarationSubtree, isClassElement));
const lateIndexes = resolver.createLateBoundIndexSignatures(input, enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, symbolTracker);
const memberNodes = concatenate(concatenate(concatenate<ClassElement>(privateIdentifier, lateIndexes), parameterProperties), visitNodes(input.members, visitDeclarationSubtree, isClassElement));
const members = factory.createNodeArray(memberNodes);
const extendsClause = getEffectiveBaseTypeNode(input);
+2 -1
View File
@@ -5048,7 +5048,7 @@ export interface TypeChecker {
/** @internal */ getTypeOfPropertyOfType(type: Type, propertyName: string): Type | undefined;
getIndexInfoOfType(type: Type, kind: IndexKind): IndexInfo | undefined;
getIndexInfosOfType(type: Type): readonly IndexInfo[];
getIndexInfosOfIndexSymbol: (indexSymbol: Symbol) => IndexInfo[];
getIndexInfosOfIndexSymbol: (indexSymbol: Symbol, siblingSymbols?: Symbol[] | undefined) => IndexInfo[];
getSignaturesOfType(type: Type, kind: SignatureKind): readonly Signature[];
getIndexTypeOfType(type: Type, kind: IndexKind): Type | undefined;
/** @internal */ getIndexType(type: Type): Type;
@@ -5867,6 +5867,7 @@ export interface EmitResolver {
getDeclarationStatementsForSourceFile(node: SourceFile, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): Statement[] | undefined;
isImportRequiredByAugmentation(decl: ImportDeclaration): boolean;
isDefinitelyReferenceToGlobalSymbolObject(node: Node): boolean;
createLateBoundIndexSignatures(cls: ClassLikeDeclaration, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): IndexSignatureDeclaration[] | undefined;
}
// dprint-ignore