From b43f36d246dce830eff6f6a3f068f6c94a01dccd Mon Sep 17 00:00:00 2001 From: magic-akari Date: Sat, 19 Sep 2026 22:33:23 +0800 Subject: [PATCH] Deprecate literal computed property names in enums Report a deprecation suggestion for enum members named with computed string literals or template literals without substitutions. Preserve existing compiler behavior and avoid adding suggestions to names that already produce errors. Add a quick fix to replace the computed name with a plain string literal, with support for fix-all quick fixes and source.fixAll. Preserve the decoded member name, including escapes and multiline template contents. Add regression coverage for diagnostics, individual and batch fixes, and invalid dynamic or numeric names. Refs #42468 --- tsc/internal/checker/checker.go | 2 + .../diagnostics/diagnosticMessages.json | 13 +++ .../diagnostics/diagnostics_generated.go | 9 ++ ...codeFixEnumComputedPropertyNameAll_test.go | 60 +++++++++++++ .../codeFixEnumComputedPropertyName_test.go | 40 +++++++++ ...enumComputedPropertyNameDeprecated_test.go | 48 +++++++++++ .../enumComputedPropertyNameError_test.go | 43 ++++++++++ tsc/internal/ls/codeactions.go | 1 + ...deactions_convertcomputedenummembername.go | 85 +++++++++++++++++++ 9 files changed, 301 insertions(+) create mode 100644 tsc/internal/fourslash/tests/codeFixEnumComputedPropertyNameAll_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixEnumComputedPropertyName_test.go create mode 100644 tsc/internal/fourslash/tests/enumComputedPropertyNameDeprecated_test.go create mode 100644 tsc/internal/fourslash/tests/enumComputedPropertyNameError_test.go create mode 100644 tsc/internal/ls/codeactions_convertcomputedenummembername.go diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index f41ba01bee95a..54b2d4d168216 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -24367,6 +24367,8 @@ func (c *Checker) computeEnumMemberValue(member *ast.Node, autoValue *jsnum.Numb text := ast.GetTextOfPropertyName(member.Name()) if isNumericLiteralName(text) && !ast.IsInfinityOrNaNString(text) { c.error(member.Name(), diagnostics.An_enum_member_cannot_have_a_numeric_name) + } else if ast.IsComputedPropertyName(member.Name()) { + c.addSuggestionDiagnostic(NewDiagnosticForNode(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() != nil { diff --git a/tsc/internal/diagnostics/diagnosticMessages.json b/tsc/internal/diagnostics/diagnosticMessages.json index 8fef6d37ff389..fc432a7add7a3 100644 --- a/tsc/internal/diagnostics/diagnosticMessages.json +++ b/tsc/internal/diagnostics/diagnosticMessages.json @@ -7393,6 +7393,11 @@ "category": "Suggestion", "code": 80010 }, + "Using a string literal as an enum member name via a computed property is deprecated. Use a simple string literal instead.": { + "category": "Suggestion", + "code": 80011, + "reportsDeprecated": true + }, "Add missing 'super()' call": { "category": "Message", @@ -8383,6 +8388,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", diff --git a/tsc/internal/diagnostics/diagnostics_generated.go b/tsc/internal/diagnostics/diagnostics_generated.go index 5cf29e807e3ed..7599680531a6b 100644 --- a/tsc/internal/diagnostics/diagnostics_generated.go +++ b/tsc/internal/diagnostics/diagnostics_generated.go @@ -3802,6 +3802,8 @@ var JSDoc_typedef_may_be_converted_to_TypeScript_type = &Message{code: 80009, ca var JSDoc_typedefs_may_be_converted_to_TypeScript_types = &Message{code: 80010, category: CategorySuggestion, key: "JSDoc_typedefs_may_be_converted_to_TypeScript_types_80010", text: "JSDoc typedefs may be converted to TypeScript types."} +var Using_a_string_literal_as_an_enum_member_name_via_a_computed_property_is_deprecated_Use_a_simple_string_literal_instead = &Message{code: 80011, category: CategorySuggestion, key: "Using_a_string_literal_as_an_enum_member_name_via_a_computed_property_is_deprecated_Use_a_simple_str_80011", text: "Using a string literal as an enum member name via a computed property is deprecated. Use a simple string literal instead.", reportsDeprecated: true} + var Add_missing_super_call = &Message{code: 90001, category: CategoryMessage, key: "Add_missing_super_call_90001", text: "Add missing 'super()' call"} var Make_super_call_the_first_statement_in_the_constructor = &Message{code: 90002, category: CategoryMessage, key: "Make_super_call_the_first_statement_in_the_constructor_90002", text: "Make 'super()' call the first statement in the constructor"} @@ -4296,6 +4298,10 @@ var Add_resolution_mode_import_attribute = &Message{code: 95196, category: Categ var Add_resolution_mode_import_attribute_to_all_type_only_imports_that_need_it = &Message{code: 95197, category: CategoryMessage, key: "Add_resolution_mode_import_attribute_to_all_type_only_imports_that_need_it_95197", text: "Add 'resolution-mode' import attribute to all type-only imports that need it"} +var Remove_unnecessary_computed_property_name_syntax = &Message{code: 95198, category: CategoryMessage, key: "Remove_unnecessary_computed_property_name_syntax_95198", text: "Remove unnecessary computed property name syntax"} + +var Remove_all_unnecessary_computed_property_name_syntax = &Message{code: 95199, category: CategoryMessage, key: "Remove_all_unnecessary_computed_property_name_syntax_95199", text: "Remove all unnecessary computed property name syntax"} + var Do_not_print_diagnostics = &Message{code: 100000, category: CategoryMessage, key: "Do_not_print_diagnostics_100000", text: "Do not print diagnostics."} var Run_in_single_threaded_mode = &Message{code: 100001, category: CategoryMessage, key: "Run_in_single_threaded_mode_100001", text: "Run in single threaded mode."} @@ -6329,6 +6335,7 @@ var allMessages = [...]**Message{ &Numeric_literals_with_absolute_values_equal_to_2_53_or_greater_are_too_large_to_be_represented_accurately_as_integers, &JSDoc_typedef_may_be_converted_to_TypeScript_type, &JSDoc_typedefs_may_be_converted_to_TypeScript_types, + &Using_a_string_literal_as_an_enum_member_name_via_a_computed_property_is_deprecated_Use_a_simple_string_literal_instead, &Add_missing_super_call, &Make_super_call_the_first_statement_in_the_constructor, &Change_extends_to_implements, @@ -6576,6 +6583,8 @@ var allMessages = [...]**Message{ &Wrap_all_invalid_decorator_expressions_in_parentheses, &Add_resolution_mode_import_attribute, &Add_resolution_mode_import_attribute_to_all_type_only_imports_that_need_it, + &Remove_unnecessary_computed_property_name_syntax, + &Remove_all_unnecessary_computed_property_name_syntax, &Do_not_print_diagnostics, &Run_in_single_threaded_mode, &Generate_pprof_CPU_Slashmemory_profiles_to_the_given_directory, diff --git a/tsc/internal/fourslash/tests/codeFixEnumComputedPropertyNameAll_test.go b/tsc/internal/fourslash/tests/codeFixEnumComputedPropertyNameAll_test.go new file mode 100644 index 0000000000000..95c69eb3d606b --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixEnumComputedPropertyNameAll_test.go @@ -0,0 +1,60 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixEnumComputedPropertyNameAll(t *testing.T) { + t.Parallel() + const content = `enum CHAR { + /*first*/['\t'] = 0x09, + ['\n'] = 0x0A, + [` + "`\\r`" + `] = 0x0D, + Plain = 0x20, +} +const enum Other { + ["key"] = 1, +} +enum Invalid { + ["a" + "b"] = 1, + [42] = 2, +}` + const expected = `enum CHAR { + "\t" = 0x09, + "\n" = 0x0A, + "\r" = 0x0D, + Plain = 0x20, +} +const enum Other { + "key" = 1, +} +enum Invalid { + ["a" + "b"] = 1, + [42] = 2, +}` + for _, sourceFixAll := range []bool{false, true} { + name := "quickfix" + if sourceFixAll { + name = "source.fixAll" + } + t.Run(name, func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.GoToMarker(t, "first") + if sourceFixAll { + f.VerifySourceFixAll(t, expected) + } else { + f.VerifyCodeFixAll(t, fourslash.VerifyCodeFixAllOptions{ + FixID: "convertComputedEnumMemberName", + NewFileContent: expected, + }) + } + f.VerifySuggestionDiagnostics(t, nil) + }) + } +} diff --git a/tsc/internal/fourslash/tests/codeFixEnumComputedPropertyName_test.go b/tsc/internal/fourslash/tests/codeFixEnumComputedPropertyName_test.go new file mode 100644 index 0000000000000..bba6eab2bb8f8 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixEnumComputedPropertyName_test.go @@ -0,0 +1,40 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixEnumComputedPropertyName(t *testing.T) { + t.Parallel() + for _, test := range []struct { + name string + expression string + literal string + }{ + {"string", `'\t'`, `"\t"`}, + {"template", "`\\r`", `"\r"`}, + {"escapes", `'a"\\b\n'`, `"a\"\\b\n"`}, + {"multiline template", "`a\nb`", `"a\nb"`}, + {"unicode escape", `'\u0061'`, `"a"`}, + {"surrogate", `'\uD800'`, `"\uD800"`}, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + content := "enum CHAR {\n [|[" + test.expression + "]|] = 0x09,\n Other = 0x0A,\n}" + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.GoToRangeStart(t, f.Ranges()[0]) + f.VerifyCodeFixAvailableExact(t, []string{"Remove unnecessary computed property name syntax"}) + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: "Remove unnecessary computed property name syntax", + NewFileContent: "enum CHAR {\n " + test.literal + " = 0x09,\n Other = 0x0A,\n}", + ApplyChanges: true, + }) + f.VerifyDiagnostics(t, nil) + }) + } +} diff --git a/tsc/internal/fourslash/tests/enumComputedPropertyNameDeprecated_test.go b/tsc/internal/fourslash/tests/enumComputedPropertyNameDeprecated_test.go new file mode 100644 index 0000000000000..1fdc54ef4e3d3 --- /dev/null +++ b/tsc/internal/fourslash/tests/enumComputedPropertyNameDeprecated_test.go @@ -0,0 +1,48 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestEnumComputedPropertyNameDeprecated(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `enum CHAR { + [|['\t']|] = 0x09, + [|["\n"]|] = 0x0A, + [|[` + "`\\r`" + `]|] = 0x0D, + 'space' = 0x20, +} +enum Names { + A, + "quoted", + [|["key"]|], + [|["Infinity"]|], + [|["NaN"]|], +} +const enum Constants { + [|["constant"]|] = 1, +} +declare enum Ambient { + [|["ambient"]|], +} +const object = { ["key"]: 1 }; +class C { ["key"] = 1; }` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + var diagnostics []*lsproto.Diagnostic + for _, r := range f.Ranges() { + diagnostics = append(diagnostics, &lsproto.Diagnostic{ + Code: &lsproto.IntegerOrString{Integer: new(int32(80011))}, + Message: lsproto.StringOrMarkupContent{String: new("Using a string literal as an enum member name via a computed property is deprecated. Use a simple string literal instead.")}, + Tags: &[]lsproto.DiagnosticTag{lsproto.DiagnosticTagDeprecated}, + Range: r.LSRange, + }) + } + f.VerifyNonSuggestionDiagnostics(t, nil) + f.VerifySuggestionDiagnostics(t, diagnostics) +} diff --git a/tsc/internal/fourslash/tests/enumComputedPropertyNameError_test.go b/tsc/internal/fourslash/tests/enumComputedPropertyNameError_test.go new file mode 100644 index 0000000000000..1ea15a3fd7976 --- /dev/null +++ b/tsc/internal/fourslash/tests/enumComputedPropertyNameError_test.go @@ -0,0 +1,43 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestEnumComputedPropertyNameError(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `const key = "dynamic"; +enum Invalid { + [|[key]|] = 1, + [|["a" + "b"]|] = 2, + [|[{}]|] = 3, + [|[1]|] = 4, + [|[0x20]|] = 5, + [|["42"]|] = 6, + [|[` + "`43`" + `]|] = 7, +}` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + var diagnostics []*lsproto.Diagnostic + for i, r := range f.Ranges() { + code := int32(1164) + message := "Computed property names are not allowed in enums." + if i >= 3 { + code = 2452 + message = "An enum member cannot have a numeric name." + } + diagnostics = append(diagnostics, &lsproto.Diagnostic{ + Code: &lsproto.IntegerOrString{Integer: &code}, + Message: lsproto.StringOrMarkupContent{String: &message}, + Range: r.LSRange, + }) + } + f.VerifyNonSuggestionDiagnostics(t, diagnostics) + f.VerifySuggestionDiagnostics(t, nil) + f.VerifyCodeFixNotAvailable(t) +} diff --git a/tsc/internal/ls/codeactions.go b/tsc/internal/ls/codeactions.go index a95b8bbc9eb60..2155516dd9da7 100644 --- a/tsc/internal/ls/codeactions.go +++ b/tsc/internal/ls/codeactions.go @@ -71,6 +71,7 @@ type CombinedCodeActions struct { var codeFixProviders = []*CodeFixProvider{ ImportFixProvider, IsolatedDeclarationsFixProvider, + ConvertComputedEnumMemberNameProvider, FixClassIncorrectlyImplementsInterfaceProvider, // Add more code fix providers here as they are implemented } diff --git a/tsc/internal/ls/codeactions_convertcomputedenummembername.go b/tsc/internal/ls/codeactions_convertcomputedenummembername.go new file mode 100644 index 0000000000000..ece8d8b014a0b --- /dev/null +++ b/tsc/internal/ls/codeactions_convertcomputedenummembername.go @@ -0,0 +1,85 @@ +package ls + +import ( + "context" + + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/astnav" + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/ls/change" +) + +const convertComputedEnumMemberNameFixID = "convertComputedEnumMemberName" + +var convertComputedEnumMemberNameErrorCodes = []int32{ + diagnostics.Using_a_string_literal_as_an_enum_member_name_via_a_computed_property_is_deprecated_Use_a_simple_string_literal_instead.Code(), +} + +var ConvertComputedEnumMemberNameProvider = &CodeFixProvider{ + ErrorCodes: convertComputedEnumMemberNameErrorCodes, + GetCodeActions: getCodeActionsToConvertComputedEnumMemberName, + FixIds: []string{convertComputedEnumMemberNameFixID}, + GetAllCodeActions: getAllCodeActionsToConvertComputedEnumMemberName, +} + +func getCodeActionsToConvertComputedEnumMemberName(ctx context.Context, fixContext *CodeFixContext) ([]*CodeAction, error) { + name := getComputedEnumMemberName(fixContext.SourceFile, fixContext.Span.Pos()) + if name == nil { + return nil, nil + } + + tracker := change.NewTracker(ctx, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) + convertComputedEnumMemberName(tracker, fixContext.SourceFile, name) + changes := getChanges(tracker, nil, fixContext.SourceFile) + if len(changes) == 0 { + return nil, nil + } + + loc := locale.FromContext(ctx) + return []*CodeAction{{ + Description: diagnostics.Remove_unnecessary_computed_property_name_syntax.Localize(loc), + Changes: changes, + FixID: convertComputedEnumMemberNameFixID, + FixAllDescription: diagnostics.Remove_all_unnecessary_computed_property_name_syntax.Localize(loc), + }}, nil +} + +func getAllCodeActionsToConvertComputedEnumMemberName(ctx context.Context, fixContext *CodeFixContext) (*CombinedCodeActions, error) { + tracker := change.NewTracker(ctx, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) + for _, diagnostic := range getAllDiagnostics(ctx, fixContext.Program, fixContext.SourceFile) { + if !isFixableDiagnostic(diagnostic, convertComputedEnumMemberNameErrorCodes) { + continue + } + file := diagnostic.File() + name := getComputedEnumMemberName(file, diagnostic.Pos()) + if name != nil { + convertComputedEnumMemberName(tracker, file, name) + } + } + changes := getChanges(tracker, nil, fixContext.SourceFile) + if len(changes) == 0 { + return nil, nil + } + return &CombinedCodeActions{ + Description: diagnostics.Remove_all_unnecessary_computed_property_name_syntax.Localize(locale.FromContext(ctx)), + Changes: changes, + }, nil +} + +func getComputedEnumMemberName(file *ast.SourceFile, pos int) *ast.Node { + name := astnav.GetTokenAtPosition(file, pos) + for name != nil && !ast.IsComputedPropertyName(name) { + name = name.Parent + } + if name == nil || !ast.IsEnumMember(name.Parent) || !ast.IsStringLiteralLike(name.Expression()) { + return nil + } + return name +} + +func convertComputedEnumMemberName(tracker *change.Tracker, file *ast.SourceFile, name *ast.Node) { + literal := tracker.NewStringLiteral(name.Expression().Text(), ast.TokenFlagsNone) + tracker.AssignCommentRange(literal, name.Expression()) + tracker.ReplaceNode(file, name, literal, nil) +}