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

Computed names in declarations files are resolved even when non-literal, preserve computed names when expressions are entity names #60052

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
104 changes: 91 additions & 13 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ import {
ElementAccessChain,
ElementAccessExpression,
ElementFlags,
ElementWithComputedPropertyName,
EmitFlags,
EmitHint,
emitModuleKindIsNonNodeESM,
Expand Down Expand Up @@ -6927,7 +6928,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
function shouldWriteTypeOfFunctionSymbol() {
const isStaticMethodSymbol = !!(symbol.flags & SymbolFlags.Method) && // typeof static method
some(symbol.declarations, declaration => isStatic(declaration));
some(symbol.declarations, declaration => isStatic(declaration) && !isLateBindableIndexSignature(getNameOfDeclaration(declaration)!));
const isNonLocalFunctionSymbol = !!(symbol.flags & SymbolFlags.Function) &&
(symbol.parent || // is exported function symbol
forEach(symbol.declarations, declaration => declaration.parent.kind === SyntaxKind.SourceFile || declaration.parent.kind === SyntaxKind.ModuleBlock));
Expand Down Expand Up @@ -7284,6 +7285,33 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return ids;
}

function indexInfoToObjectComputedNamesOrSignatureDeclaration(indexInfo: IndexInfo, context: NodeBuilderContext, typeNode: TypeNode | undefined): [IndexSignatureDeclaration] | PropertySignature[] {
if (indexInfo.components) {
// 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
const allComponentComputedNamesSerializable = every(indexInfo.components, e => {
return !!(e.name && isComputedPropertyName(e.name) && isEntityNameExpression(e.name.expression) && context.enclosingDeclaration && isEntityNameVisible(e.name.expression, context.enclosingDeclaration, /*shouldComputeAliasToMakeVisible*/ false)?.accessibility === SymbolAccessibility.Accessible);
});
if (allComponentComputedNamesSerializable) {
// Only use computed name serialization form if all components are visible and take the `a.b.c` form
return map(indexInfo.components, e => {
// Still need to track visibility even if we've already checked it to paint references as used
trackComputedName(e.name.expression as EntityNameExpression, context.enclosingDeclaration, context);
return setTextRange(
context,
factory.createPropertySignature(
indexInfo.isReadonly ? [factory.createModifier(SyntaxKind.ReadonlyKeyword)] : undefined,
e.name,
(isPropertySignature(e) || isPropertyDeclaration(e) || isMethodSignature(e) || isMethodDeclaration(e) || isGetAccessor(e) || isSetAccessor(e)) && e.questionToken ? factory.createToken(SyntaxKind.QuestionToken) : undefined,
typeNode || typeToTypeNodeHelper(getTypeOfSymbol(e.symbol), context),
),
e,
);
});
}
}
return [indexInfoToIndexSignatureDeclarationHelper(indexInfo, context, typeNode)];
}

function createTypeNodesFromResolvedType(resolvedType: ResolvedType): TypeElement[] | undefined {
if (checkTruncationLength(context)) {
if (context.flags & NodeBuilderFlags.NoTruncation) {
Expand All @@ -7300,7 +7328,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
typeElements.push(signatureToSignatureDeclarationHelper(signature, SyntaxKind.ConstructSignature, context) as ConstructSignatureDeclaration);
}
for (const info of resolvedType.indexInfos) {
typeElements.push(indexInfoToIndexSignatureDeclarationHelper(info, context, resolvedType.objectFlags & ObjectFlags.ReverseMapped ? createElidedInformationPlaceholder(context) : undefined));
typeElements.push(...indexInfoToObjectComputedNamesOrSignatureDeclaration(info, context, resolvedType.objectFlags & ObjectFlags.ReverseMapped ? createElidedInformationPlaceholder(context) : undefined));
}

const properties = resolvedType.properties;
Expand Down Expand Up @@ -8041,10 +8069,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (!context.tracker.canTrackSymbol) return;
// get symbol of the first identifier of the entityName
const firstIdentifier = getFirstIdentifier(accessExpression);
const name = resolveName(firstIdentifier, firstIdentifier.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue, /*nameNotFoundMessage*/ undefined, /*isUse*/ true);
const name = resolveName(enclosingDeclaration, firstIdentifier.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue, /*nameNotFoundMessage*/ undefined, /*isUse*/ true);
if (name) {
context.tracker.trackSymbol(name, enclosingDeclaration, SymbolFlags.Value);
}
else {
// Name does not resolve at target location, track symbol at dest location (should be inaccessible)
const fallback = resolveName(firstIdentifier, firstIdentifier.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue, /*nameNotFoundMessage*/ undefined, /*isUse*/ true);
if (fallback) {
context.tracker.trackSymbol(fallback, enclosingDeclaration, SymbolFlags.Value);
}
}
}

function lookupSymbolChain(symbol: Symbol, context: NodeBuilderContext, meaning: SymbolFlags, yieldModuleSymbol?: boolean) {
Expand Down Expand Up @@ -16032,8 +16067,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return symbolTable.get(InternalSymbolName.Index);
}

function createIndexInfo(keyType: Type, type: Type, isReadonly: boolean, declaration?: IndexSignatureDeclaration): IndexInfo {
return { keyType, type, isReadonly, declaration };
function createIndexInfo(keyType: Type, type: Type, isReadonly: boolean, declaration?: IndexSignatureDeclaration, components?: ElementWithComputedPropertyName[]): IndexInfo {
return { keyType, type, isReadonly, declaration, components };
}

function getIndexInfosOfSymbol(symbol: Symbol): IndexInfo[] {
Expand Down Expand Up @@ -19647,7 +19682,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function getIndexInfoWithReadonly(info: IndexInfo, readonly: boolean) {
return info.isReadonly !== readonly ? createIndexInfo(info.keyType, info.type, readonly, info.declaration) : info;
return info.isReadonly !== readonly ? createIndexInfo(info.keyType, info.type, readonly, info.declaration, info.components) : info;
}

function createLiteralType(flags: TypeFlags, value: string | number | PseudoBigInt, symbol?: Symbol, regularType?: LiteralType) {
Expand Down Expand Up @@ -20555,7 +20590,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function instantiateIndexInfo(info: IndexInfo, mapper: TypeMapper) {
return createIndexInfo(info.keyType, instantiateType(info.type, mapper), info.isReadonly, info.declaration);
return createIndexInfo(info.keyType, instantiateType(info.type, mapper), info.isReadonly, info.declaration, info.components);
}

// Returns true if the given expression contains (at any level of nesting) a function or arrow expression
Expand Down Expand Up @@ -25341,7 +25376,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
}
const result = createAnonymousType(type.symbol, members, emptyArray, emptyArray, sameMap(getIndexInfosOfType(type), info => createIndexInfo(info.keyType, getWidenedType(info.type), info.isReadonly)));
const result = createAnonymousType(type.symbol, members, emptyArray, emptyArray, sameMap(getIndexInfosOfType(type), info => createIndexInfo(info.keyType, getWidenedType(info.type), info.isReadonly, info.declaration, info.components)));
result.objectFlags |= getObjectFlags(type) & (ObjectFlags.JSLiteral | ObjectFlags.NonInferrableType); // Retain js literal flag through widening
return result;
}
Expand Down Expand Up @@ -32795,9 +32830,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
isTypeAssignableToKind(checkComputedPropertyName(firstDecl.name), TypeFlags.ESSymbol));
}

function isSymbolWithComputedName(symbol: Symbol) {
const firstDecl = symbol.declarations?.[0];
return firstDecl && isNamedDeclaration(firstDecl) && isComputedPropertyName(firstDecl.name);
}

// 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[] = [];
let components: ElementWithComputedPropertyName[] | undefined;
for (let i = offset; i < properties.length; i++) {
const prop = properties[i];
if (
Expand All @@ -32806,10 +32847,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
keyType === esSymbolType && isSymbolWithSymbolName(prop)
) {
propTypes.push(getTypeOfSymbol(properties[i]));
if (isSymbolWithComputedName(properties[i])) {
components = append(components, properties[i].declarations?.[0]! as ElementWithComputedPropertyName);
}
}
}
const unionType = propTypes.length ? getUnionType(propTypes, UnionReduction.Subtype) : undefinedType;
return createIndexInfo(keyType, unionType, isReadonly);
return createIndexInfo(keyType, unionType, isReadonly, /*declaration*/ undefined, components);
}

function getImmediateAliasedSymbol(symbol: Symbol): Symbol | undefined {
Expand Down Expand Up @@ -46213,9 +46257,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const typeDeclaration = symbol.valueDeclaration;
if (typeDeclaration && isClassLike(typeDeclaration)) {
for (const member of typeDeclaration.members) {
// Only process instance properties with computed names here. Static properties cannot be in conflict with indexers,
// and properties with literal names were already checked.
if (!isStatic(member) && !hasBindableName(member)) {
// Only process instance properties against instance index signatures and static properties against static index signatures
if (
(
(!isStaticIndex && !isStatic(member)) ||
(isStaticIndex && isStatic(member))
) && !hasBindableName(member)
) {
const symbol = getSymbolOfDeclaration(member);
checkIndexConstraintForProperty(type, symbol, getTypeOfExpression((member as DynamicNamedDeclaration).name.expression), getNonMissingTypeOfSymbol(symbol));
}
Expand Down Expand Up @@ -50800,6 +50848,25 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
for (const info of infoList!) {
if (info.declaration) continue;
if (info === anyBaseTypeIndexInfo) continue; // inherited, but looks like a late-bound signature because it has no declarations
if (info.components) {
const allComponentComputedNamesSerializable = every(info.components, e => {
return !!(e.name && isComputedPropertyName(e.name) && isEntityNameExpression(e.name.expression) && enclosing && isEntityNameVisible(e.name.expression, enclosing, /*shouldComputeAliasToMakeVisible*/ false)?.accessibility === SymbolAccessibility.Accessible);
});
if (allComponentComputedNamesSerializable) {
result.push(...map(info.components, e => {
trackComputedName(e.name.expression as EntityNameExpression);
const mods = infoList === staticInfos ? [factory.createModifier(SyntaxKind.StaticKeyword)] as Modifier[] : undefined;
return factory.createPropertyDeclaration(
append(mods, info.isReadonly ? factory.createModifier(SyntaxKind.ReadonlyKeyword) : undefined),
e.name,
(isPropertySignature(e) || isPropertyDeclaration(e) || isMethodSignature(e) || isMethodDeclaration(e) || isGetAccessor(e) || isSetAccessor(e)) && e.questionToken ? factory.createToken(SyntaxKind.QuestionToken) : undefined,
nodeBuilder.typeToTypeNode(getTypeOfSymbol(e.symbol), enclosing, flags, internalFlags, tracker),
/*initializer*/ undefined,
);
}));
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));
Expand All @@ -50810,6 +50877,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
return result;

function trackComputedName(accessExpression: EntityNameOrEntityNameExpression) {
if (!tracker.trackSymbol) return;
// get symbol of the first identifier of the entityName
const firstIdentifier = getFirstIdentifier(accessExpression);
const name = resolveName(firstIdentifier, firstIdentifier.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue, /*nameNotFoundMessage*/ undefined, /*isUse*/ true);
if (name) {
tracker.trackSymbol(name, enclosing, SymbolFlags.Value);
}
}
},
};

Expand Down Expand Up @@ -52262,7 +52339,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function checkGrammarForInvalidDynamicName(node: DeclarationName, message: DiagnosticMessage) {
if (isNonBindableDynamicName(node)) {
// 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)) {
return grammarErrorOnNode(node, message);
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5881,7 +5881,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;
createLateBoundIndexSignatures(cls: ClassLikeDeclaration, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): (IndexSignatureDeclaration | PropertyDeclaration)[] | undefined;
}

// dprint-ignore
Expand Down Expand Up @@ -7015,11 +7015,14 @@ export const enum IndexKind {
Number,
}

export type ElementWithComputedPropertyName = (ClassElement | ObjectLiteralElement) & { name: ComputedPropertyName; };

export interface IndexInfo {
keyType: Type;
type: Type;
isReadonly: boolean;
declaration?: IndexSignatureDeclaration;
components?: ElementWithComputedPropertyName[];
}

/** @internal */
Expand Down
12 changes: 6 additions & 6 deletions tests/baselines/reference/ES5SymbolProperty1.types
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ var Symbol: SymbolConstructor;
> : ^^^^^^^^^^^^^^^^^

var obj = {
>obj : { [x: string]: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^
>{ [Symbol.foo]: 0} : { [x: string]: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^
>obj : { [Symbol.foo]: number; }
> : ^^ ^^^^^^ ^^
>{ [Symbol.foo]: 0} : { [Symbol.foo]: number; }
> : ^^ ^^^^^^ ^^

[Symbol.foo]: 0
>[Symbol.foo] : number
Expand All @@ -32,8 +32,8 @@ var obj = {
obj[Symbol.foo];
>obj[Symbol.foo] : number
> : ^^^^^^
>obj : { [x: string]: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^
>obj : { [Symbol.foo]: number; }
> : ^^ ^^^^^^ ^^
>Symbol.foo : string
> : ^^^^^^
>Symbol : SymbolConstructor
Expand Down
4 changes: 4 additions & 0 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6849,11 +6849,15 @@ declare namespace ts {
String = 0,
Number = 1,
}
type ElementWithComputedPropertyName = (ClassElement | ObjectLiteralElement) & {
name: ComputedPropertyName;
};
interface IndexInfo {
keyType: Type;
type: Type;
isReadonly: boolean;
declaration?: IndexSignatureDeclaration;
components?: ElementWithComputedPropertyName[];
}
enum InferencePriority {
None = 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,12 @@ function foo8(y = (async () => z)(), z = 1) {

// error - used as computed name of method
function foo9(y = {[z]() { return z; }}, z = 1) {
>foo9 : (y?: { [x: number]: () => number; }, z?: number) => void
> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
>y : { [x: number]: () => number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>{[z]() { return z; }} : { [x: number]: () => number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo9 : (y?: { [z]: () => number; }, z?: number) => void
> : ^ ^^^^^ ^^^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^
>y : { [z]: () => number; }
> : ^^ ^^^^^^^^^^^^ ^^
>{[z]() { return z; }} : { [z]: () => number; }
> : ^^ ^^^^^^^^^^^^ ^^
>[z] : () => number
> : ^^^^^^^^^^^^
>z : number
Expand Down
Loading
Loading