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) +}