From 2136bef65227bc0dd28b6972baeb2eb91e15374a Mon Sep 17 00:00:00 2001 From: Oleksandr T Date: Thu, 20 Jul 2023 23:51:32 +0300 Subject: [PATCH] fix(54694): Class incorrectly implements interface generated with template string literal mapped type (#54715) --- src/compiler/checker.ts | 22 ++-------------- src/compiler/utilities.ts | 25 +++++++++++++++++++ src/services/codefixes/helpers.ts | 23 ++++++++++++++--- ...eFixClassImplementInterfaceMappedType1.ts} | 0 ...deFixClassImplementInterfaceMappedType2.ts | 20 +++++++++++++++ 5 files changed, 67 insertions(+), 23 deletions(-) rename tests/cases/fourslash/{codeFixClassImplementInterfaceMappedType.ts => codeFixClassImplementInterfaceMappedType1.ts} (100%) create mode 100644 tests/cases/fourslash/codeFixClassImplementInterfaceMappedType2.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 6ec408af5b0..3cd42bd6008 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -335,6 +335,7 @@ import { getParseTreeNode, getPropertyAssignmentAliasLikeExpression, getPropertyNameForPropertyNameNode, + getPropertyNameFromType, getResolutionDiagnostic, getResolutionModeOverrideForClause, getResolvedExternalModuleName, @@ -729,6 +730,7 @@ import { isTypeQueryNode, isTypeReferenceNode, isTypeReferenceType, + isTypeUsableAsPropertyName, isUMDExportSymbol, isValidBigIntString, isValidESSymbolDeclaration, @@ -12286,13 +12288,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return type as InterfaceTypeWithDeclaredMembers; } - /** - * Indicates whether a type can be used as a property name. - */ - function isTypeUsableAsPropertyName(type: Type): type is StringLiteralType | NumberLiteralType | UniqueESSymbolType { - return !!(type.flags & TypeFlags.StringOrNumberLiteralOrUnique); - } - /** * Indicates whether a declaration name is definitely late-bindable. * A declaration name is only late-bindable if: @@ -12338,19 +12333,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return isDynamicName(node) && !isLateBindableName(node); } - /** - * Gets the symbolic name for a member from its type. - */ - function getPropertyNameFromType(type: StringLiteralType | NumberLiteralType | UniqueESSymbolType): __String { - if (type.flags & TypeFlags.UniqueESSymbol) { - return (type as UniqueESSymbolType).escapedName; - } - if (type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) { - return escapeLeadingUnderscores("" + (type as StringLiteralType | NumberLiteralType).value); - } - return Debug.fail(); - } - /** * Adds a declaration to a late-bound dynamic member. This performs the same function for * late-bound members that `addDeclarationToSymbol` in binder.ts performs for early-bound diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 251f8a4f163..150f3a066b9 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -416,6 +416,7 @@ import { noop, normalizePath, NoSubstitutionTemplateLiteral, + NumberLiteralType, NumericLiteral, ObjectFlags, ObjectFlagsType, @@ -494,6 +495,7 @@ import { stringContains, StringLiteral, StringLiteralLike, + StringLiteralType, stringToToken, SuperCall, SuperExpression, @@ -544,6 +546,7 @@ import { TypeReferenceNode, unescapeLeadingUnderscores, UnionOrIntersectionTypeNode, + UniqueESSymbolType, UserPreferences, ValidImportTypeNode, VariableDeclaration, @@ -10313,3 +10316,25 @@ export function getTextOfJsxNamespacedName(node: JsxNamespacedName) { export function intrinsicTagNameToString(node: Identifier | JsxNamespacedName) { return isIdentifier(node) ? idText(node) : getTextOfJsxNamespacedName(node); } + +/** + * Indicates whether a type can be used as a property name. + * @internal + */ +export function isTypeUsableAsPropertyName(type: Type): type is StringLiteralType | NumberLiteralType | UniqueESSymbolType { + return !!(type.flags & TypeFlags.StringOrNumberLiteralOrUnique); +} + +/** + * Gets the symbolic name for a member from its type. + * @internal + */ +export function getPropertyNameFromType(type: StringLiteralType | NumberLiteralType | UniqueESSymbolType): __String { + if (type.flags & TypeFlags.UniqueESSymbol) { + return (type as UniqueESSymbolType).escapedName; + } + if (type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) { + return escapeLeadingUnderscores("" + (type as StringLiteralType | NumberLiteralType).value); + } + return Debug.fail(); +} diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 18db6cf68a9..1116d70c19c 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -6,27 +6,32 @@ import { Block, CallExpression, CharacterCodes, + CheckFlags, ClassLikeDeclaration, CodeFixContextBase, combine, Debug, + Declaration, Diagnostics, emptyArray, EntityName, Expression, factory, find, + firstOrUndefined, flatMap, FunctionDeclaration, FunctionExpression, GetAccessorDeclaration, getAllAccessorDeclarations, + getCheckFlags, getEffectiveModifierFlags, getEmitScriptTarget, getFirstIdentifier, getModuleSpecifierResolverHost, getNameForExportedSymbol, getNameOfDeclaration, + getPropertyNameFromType, getQuotePreference, getSetAccessorValueParameter, getSynthesizedDeepClone, @@ -52,6 +57,7 @@ import { isSetAccessorDeclaration, isStringLiteral, isTypeNode, + isTypeUsableAsPropertyName, isYieldExpression, LanguageServiceHost, length, @@ -91,6 +97,7 @@ import { textChanges, TextSpan, textSpanEnd, + TransientSymbol, tryCast, TsConfigSourceFile, Type, @@ -98,6 +105,7 @@ import { TypeFlags, TypeNode, TypeParameterDeclaration, + unescapeLeadingUnderscores, UnionType, UserPreferences, visitEachChild, @@ -174,7 +182,7 @@ export function addNewNodeForMemberSymbol( isAmbient = false, ): void { const declarations = symbol.getDeclarations(); - const declaration = declarations?.[0]; + const declaration = firstOrUndefined(declarations); const checker = context.program.getTypeChecker(); const scriptTarget = getEmitScriptTarget(context.program.getCompilerOptions()); @@ -193,7 +201,7 @@ export function addNewNodeForMemberSymbol( * In such cases, we assume the declaration to be a `PropertySignature`. */ const kind = declaration?.kind ?? SyntaxKind.PropertySignature; - const declarationName = getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName; + const declarationName = createDeclarationName(symbol, declaration); const effectiveModifierFlags = declaration ? getEffectiveModifierFlags(declaration) : ModifierFlags.None; let modifierFlags = effectiveModifierFlags & ModifierFlags.Static; modifierFlags |= @@ -310,7 +318,6 @@ export function addNewNodeForMemberSymbol( if (method) addClassElement(method); } - function createModifiers(): NodeArray | undefined { let modifiers: Modifier[] | undefined; @@ -344,6 +351,16 @@ export function addNewNodeForMemberSymbol( function createTypeNode(typeNode: TypeNode | undefined) { return getSynthesizedDeepClone(typeNode, /*includeTrivia*/ false); } + + function createDeclarationName(symbol: Symbol, declaration: Declaration | undefined): PropertyName { + if (getCheckFlags(symbol) & CheckFlags.Mapped) { + const nameType = (symbol as TransientSymbol).links.nameType; + if (nameType && isTypeUsableAsPropertyName(nameType)) { + return factory.createIdentifier(unescapeLeadingUnderscores(getPropertyNameFromType(nameType))); + } + } + return getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName; + } } /** @internal */ diff --git a/tests/cases/fourslash/codeFixClassImplementInterfaceMappedType.ts b/tests/cases/fourslash/codeFixClassImplementInterfaceMappedType1.ts similarity index 100% rename from tests/cases/fourslash/codeFixClassImplementInterfaceMappedType.ts rename to tests/cases/fourslash/codeFixClassImplementInterfaceMappedType1.ts diff --git a/tests/cases/fourslash/codeFixClassImplementInterfaceMappedType2.ts b/tests/cases/fourslash/codeFixClassImplementInterfaceMappedType2.ts new file mode 100644 index 00000000000..7d22bef3770 --- /dev/null +++ b/tests/cases/fourslash/codeFixClassImplementInterfaceMappedType2.ts @@ -0,0 +1,20 @@ +/// + +////type ListenerTemplate = { +//// [K in keyof T as K extends string +//// ? S extends `${infer F}${I}${infer R}` ? `${F}${K}${R}` : K : K] +//// : (listener: (payload: T[K]) => void) => void; +////}; +////type ListenActionable = ListenerTemplate; +////type ClickEventSupport = ListenActionable<{ Click: 'some-click-event-payload' }>; +//// +////[|class C implements ClickEventSupport { }|] + +verify.codeFix({ + description: "Implement interface 'ClickEventSupport'", + newRangeContent: +`class C implements ClickEventSupport { + addClickListener: (listener: (payload: "some-click-event-payload") => void) => void; + removeClickListener: (listener: (payload: "some-click-event-payload") => void) => void; +}`, +});