From 708b4a76a71e7ecf66308e39e3c5b80ae0bd6a0b Mon Sep 17 00:00:00 2001 From: magic-akari Date: Mon, 1 Dec 2025 12:20:53 +0800 Subject: [PATCH 1/2] Add deprecation warning for computed property names in enums This adds a suggestion diagnostic for using computed property names with literal expressions in enum members (e.g., `["key"]`). This syntax is rarely used, not supported by Babel/typescript-eslint/SWC, and will be disallowed in a future version (typescript-go). Changes: - Add suggestion diagnostic (code 1550) for computed property names - Add quick fix to convert `["key"]` to `"key"` - Add "Fix All" support for batch conversion --- src/compiler/checker.ts | 7 ++ src/compiler/diagnosticMessages.json | 15 +++- src/services/_namespaces/ts.codefix.ts | 1 + .../convertComputedEnumMemberName.ts | 83 +++++++++++++++++++ .../codeFixEnumComputedPropertyName.ts | 17 ++++ .../codeFixEnumComputedPropertyNameAll.ts | 19 +++++ .../enumComputedPropertyNameDeprecated.ts | 23 +++++ .../enumComputedPropertyNameError.ts | 15 ++++ 8 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 src/services/codefixes/convertComputedEnumMemberName.ts create mode 100644 tests/cases/fourslash/codeFixEnumComputedPropertyName.ts create mode 100644 tests/cases/fourslash/codeFixEnumComputedPropertyNameAll.ts create mode 100644 tests/cases/fourslash/enumComputedPropertyNameDeprecated.ts create mode 100644 tests/cases/fourslash/enumComputedPropertyNameError.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 8e5c03560db3e..25ddc8cd0d158 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -47833,6 +47833,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { error(member.name, Diagnostics.An_enum_member_cannot_have_a_numeric_name); } } + if (isComputedPropertyName(member.name)) { + // Computed property name with a literal expression (e.g., ['key'] or [`key`]) + // This is deprecated and will be disallowed in a future version + suggestionDiagnostics.add( + createDiagnosticForNode(member.name, Diagnostics.Using_a_string_literal_as_an_enum_member_name_via_a_computed_property_is_deprecated_Use_a_simple_string_literal_instead), + ); + } if (member.initializer) { return computeConstantEnumMemberValue(member); } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index c21fd11e5c7bf..fa37e5afb3b10 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -1861,7 +1861,11 @@ "category": "Message", "code": 1549 }, - + "Using a string literal as an enum member name via a computed property is deprecated. Use a simple string literal instead.": { + "category": "Suggestion", + "code": 1550, + "reportsDeprecated": true + }, "The types of '{0}' are incompatible between these types.": { "category": "Error", "code": 2200 @@ -8348,7 +8352,14 @@ "category": "Message", "code": 95197 }, - + "Remove unnecessary computed property name syntax": { + "category": "Message", + "code": 95198 + }, + "Remove all unnecessary computed property name syntax": { + "category": "Message", + "code": 95199 + }, "No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": { "category": "Error", "code": 18004 diff --git a/src/services/_namespaces/ts.codefix.ts b/src/services/_namespaces/ts.codefix.ts index 3cf05630388de..8b07510aa9ca3 100644 --- a/src/services/_namespaces/ts.codefix.ts +++ b/src/services/_namespaces/ts.codefix.ts @@ -74,3 +74,4 @@ export * from "../codefixes/splitTypeOnlyImport.js"; export * from "../codefixes/convertConstToLet.js"; export * from "../codefixes/fixExpectedComma.js"; export * from "../codefixes/fixAddVoidToPromise.js"; +export * from "../codefixes/convertComputedEnumMemberName.js"; diff --git a/src/services/codefixes/convertComputedEnumMemberName.ts b/src/services/codefixes/convertComputedEnumMemberName.ts new file mode 100644 index 0000000000000..3d911f8c10426 --- /dev/null +++ b/src/services/codefixes/convertComputedEnumMemberName.ts @@ -0,0 +1,83 @@ +import { + createCodeFixActionMaybeFixAll, + createCombinedCodeActions, + eachDiagnostic, + registerCodeFix, +} from "../_namespaces/ts.codefix.js"; +import { + ComputedPropertyName, + Diagnostics, + factory, + getTokenAtPosition, + isComputedPropertyName, + isEnumMember, + isNoSubstitutionTemplateLiteral, + isStringLiteral, + SourceFile, + textChanges, +} from "../_namespaces/ts.js"; + +const fixId = "convertComputedEnumMemberName"; +const errorCodes = [Diagnostics.Using_a_string_literal_as_an_enum_member_name_via_a_computed_property_is_deprecated_Use_a_simple_string_literal_instead.code]; + +registerCodeFix({ + errorCodes, + getCodeActions(context) { + const { sourceFile, span } = context; + const info = getInfo(sourceFile, span.start); + if (info === undefined) return; + + const changes = textChanges.ChangeTracker.with(context, t => doChange(t, sourceFile, info)); + return [createCodeFixActionMaybeFixAll(fixId, changes, Diagnostics.Remove_unnecessary_computed_property_name_syntax, fixId, Diagnostics.Remove_all_unnecessary_computed_property_name_syntax)]; + }, + getAllCodeActions(context) { + return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => { + eachDiagnostic(context, errorCodes, diag => { + const info = getInfo(diag.file, diag.start); + if (info) { + return doChange(changes, diag.file, info); + } + return undefined; + }); + })); + }, + fixIds: [fixId], +}); + +interface Info { + computedName: ComputedPropertyName; + literalText: string; +} + +function getInfo(sourceFile: SourceFile, pos: number): Info | undefined { + const token = getTokenAtPosition(sourceFile, pos); + + // Navigate to find the computed property name + let node = token; + while (node && !isComputedPropertyName(node)) { + node = node.parent; + } + + if (!node || !isComputedPropertyName(node)) return undefined; + if (!isEnumMember(node.parent)) return undefined; + + const expression = node.expression; + let literalText: string; + + if (isStringLiteral(expression)) { + literalText = expression.text; + } + else if (isNoSubstitutionTemplateLiteral(expression)) { + literalText = expression.text; + } + else { + return undefined; + } + + return { computedName: node, literalText }; +} + +function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, info: Info) { + // Replace ['\t'] with '\t' (or ["key"] with "key") + changes.replaceNode(sourceFile, info.computedName, factory.createStringLiteral(info.literalText)); +} diff --git a/tests/cases/fourslash/codeFixEnumComputedPropertyName.ts b/tests/cases/fourslash/codeFixEnumComputedPropertyName.ts new file mode 100644 index 0000000000000..879efab64be39 --- /dev/null +++ b/tests/cases/fourslash/codeFixEnumComputedPropertyName.ts @@ -0,0 +1,17 @@ +/// + +// @Filename: test.ts +////enum CHAR { +//// [|['\t']|] = 0x09, +//// ['\n'] = 0x0A, +////} + +goTo.file("test.ts"); +verify.codeFix({ + description: "Remove unnecessary computed property name syntax", + newFileContent: `enum CHAR { + "\\t" = 0x09, + ['\\n'] = 0x0A, +}`, + index: 0, +}); diff --git a/tests/cases/fourslash/codeFixEnumComputedPropertyNameAll.ts b/tests/cases/fourslash/codeFixEnumComputedPropertyNameAll.ts new file mode 100644 index 0000000000000..622cf931a68d1 --- /dev/null +++ b/tests/cases/fourslash/codeFixEnumComputedPropertyNameAll.ts @@ -0,0 +1,19 @@ +/// + +// @Filename: test.ts +////enum CHAR { +//// ['\t'] = 0x09, +//// ['\n'] = 0x0A, +//// [`\r`] = 0x0D, +////} + +goTo.file("test.ts"); +verify.codeFixAll({ + fixId: "convertComputedEnumMemberName", + fixAllDescription: "Remove all unnecessary computed property name syntax", + newFileContent: `enum CHAR { + "\\t" = 0x09, + "\\n" = 0x0A, + "\\r" = 0x0D, +}`, +}); diff --git a/tests/cases/fourslash/enumComputedPropertyNameDeprecated.ts b/tests/cases/fourslash/enumComputedPropertyNameDeprecated.ts new file mode 100644 index 0000000000000..3a06bcd722abe --- /dev/null +++ b/tests/cases/fourslash/enumComputedPropertyNameDeprecated.ts @@ -0,0 +1,23 @@ +/// +// @Filename: a.ts +////enum CHAR { +//// [|['\t']|] = 0x09, +//// [|['\n']|] = 0x0A, +//// [|[`\r`]|] = 0x0D, +//// 'space' = 0x20, // no warning for simple string literal +////} +//// +////enum NoWarning { +//// A = 1, +//// B = 2, +//// "quoted" = 3, +////} + +goTo.file("a.ts") +const diagnostics = test.ranges().map(range => ({ + code: 1550, + message: "Using a string literal as an enum member name via a computed property is deprecated. Use a simple string literal instead.", + reportsDeprecated: true, + range, +})); +verify.getSuggestionDiagnostics(diagnostics) diff --git a/tests/cases/fourslash/enumComputedPropertyNameError.ts b/tests/cases/fourslash/enumComputedPropertyNameError.ts new file mode 100644 index 0000000000000..e3b1e962e2efe --- /dev/null +++ b/tests/cases/fourslash/enumComputedPropertyNameError.ts @@ -0,0 +1,15 @@ +/// +// @Filename: a.ts +////const key = "dynamic"; +////enum Test { +//// [|[key]|] = 1, // error: non-literal computed property name +//// [|["a" + "b"]|] = 2, // error: binary expression +////} + +goTo.file("a.ts") +const diagnostics = test.ranges().map(range => ({ + code: 1164, + message: "Computed property names are not allowed in enums.", + range, +})); +verify.getSemanticDiagnostics(diagnostics) From c03efe894784f3f2c19acd1f823843c8d9369144 Mon Sep 17 00:00:00 2001 From: magic-akari Date: Mon, 1 Dec 2025 18:15:25 +0800 Subject: [PATCH 2/2] Skip deprecation warning when enum member already has an error --- src/compiler/checker.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 25ddc8cd0d158..2b0632f6cfb88 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -47821,19 +47821,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function computeEnumMemberValue(member: EnumMember, autoValue: number | undefined, previous: EnumMember | undefined): EvaluatorResult { + let errorReported = false; if (isComputedNonLiteralName(member.name)) { + errorReported = true; error(member.name, Diagnostics.Computed_property_names_are_not_allowed_in_enums); } else if (isBigIntLiteral(member.name)) { + errorReported = true; error(member.name, Diagnostics.An_enum_member_cannot_have_a_numeric_name); } else { const text = getTextOfPropertyName(member.name); if (isNumericLiteralName(text) && !isInfinityOrNaNString(text)) { + errorReported = true; error(member.name, Diagnostics.An_enum_member_cannot_have_a_numeric_name); } } - if (isComputedPropertyName(member.name)) { + if (!errorReported && isComputedPropertyName(member.name)) { // Computed property name with a literal expression (e.g., ['key'] or [`key`]) // This is deprecated and will be disallowed in a future version suggestionDiagnostics.add(